Creating Your Own $ Commands
The Streaming Tool has an event hook system that lets developers write custom $ commands — without modifying main.py.
You create a .py file in the src/event_hooks/ folder, define a register(api) function inside it, and add the corresponding $ command to the actions.mca file. When the bot starts next, your hook is loaded automatically and ready to use.
[!WARNING] Custom imports are restricted.
Hook scripts run inside the bundled application (
app.exe). You are not allowed to import arbitrary modules — only the following are permitted:
randomtime- (more in the future)
All other functionality is available through the
apiobject (e.g. sending RCON commands, firing triggers, logging, reading config). Do not use your ownimportstatements for external packages likerequests,flask,aiohttp, etc. — they are not available inside hook scripts and will cause a load error.
How It Works
When the bot starts, all .py files in the event_hooks/ folder are automatically loaded and integrated into the running process. No separate process and no separate executable is started per hook — everything runs directly inside the main application.
[!NOTE] Development vs. Release: During development, the folder is located at
src/event_hooks/. The build script copies it toevent_hooks/in the release build. The bot always loads hooks from the release path (event_hooks/), not fromsrc/.
The loading sequence in detail:
- Parsing:
generate_datapack()readsactions.mcaand collects all$command names (e.g.$welcome_message→welcome_message) - Import: All
.pyfiles inevent_hooks/are dynamically imported viaimportlib - Registration: For each loaded module,
register(api)is called — this is where handlers are registered - Execution: When a TikTok event arrives and its trigger points to a
$command, the matching handler is called immediately
Creating a Hook
Step 1: Create the Hook Script
Create a new .py file in the src/event_hooks/ folder.
Example: src/event_hooks/welcome_message.py
def register(api):
def welcome_message(user, trigger, context):
api.rcon_enqueue([
f"say {user} just followed!",
"effect give @a minecraft:glowing 5 0 true",
])
api.register_action("welcome_message", welcome_message)
What's happening here?
register(api)is the mandatory function called by the system. Without it, the script is ignored.- Inside
register(), you define your handler as a closure — this gives it automatic access toapi. api.register_action("welcome_message", welcome_message)registers the handler under the namewelcome_message. This name must exactly match the$command inactions.mca.
Step 2: Add to actions.mca
The user (or you as a developer for testing) adds the command to actions.mca. The trigger key on the left side of : determines which TikTok event fires the hook:
follow: $welcome_message
In this example: every time someone follows on TikTok, the welcome_message hook is called.
Step 3: Start or Restart the Bot
Start the bot — or restart it if it's already running. The hook is loaded automatically and active immediately.
The register(api) Function
Every hook file must provide a register(api) function at the top level. If it's missing, the script is skipped during loading and an error message is printed.
register() is called exactly once when the bot starts. Define all your handlers inside this function as closures — this way they automatically have access to the api object without needing global variables.
def register(api):
def my_handler(user, trigger, context):
# Your logic here
api.log(f"{user} triggered {trigger}")
api.register_action("my_action", my_handler)
[!WARNING] Files without a
register()function are not loaded. The console will show:[HOOK] [ERROR] filename.py has no register() function — skipped.
Handler Signature
Every handler must accept exactly three arguments:
| Argument | Type | Description |
|---|---|---|
user | str | The TikTok username that triggered the event (e.g. "max_mustermann") |
trigger | str | The name of the $ command currently being executed (e.g. "welcome_message") |
context | dict | Reserved for future extensions — currently always an empty {} |
def my_handler(user, trigger, context):
api.rcon_enqueue([f"say Hello {user}, trigger was: {trigger}"])
Missing or extra arguments will cause the handler to throw an error at call time (but the bot stays stable — see Error Handling).
The HookAPI
The api object passed to register() is the only interface between your hook and the main system. It provides the following methods:
api.register_action(name, fn)
Registers a handler under the given name. The name must exactly match the $ command in actions.mca.
api.register_action("welcome_message", welcome_message)
[!WARNING] If the same name is registered twice (e.g. in two different hook files), the first registration wins. The second is ignored and a warning is printed:
[HOOK] [WARN] Duplicate action 'welcome_message' — first registration kept.
api.rcon_enqueue(commands)
Sends a list of Minecraft commands to the RCON queue. The commands are pushed into the queue in order and processed by the RCON worker from there.
api.rcon_enqueue([
"effect give @a minecraft:speed 10 2 true",
f"say {user} triggered a speed boost!",
])
Each entry is a complete command as a string — without a leading /.
[!NOTE] Both vanilla and plugin commands are allowed.
Since everything goes through RCON, you can send not only vanilla Minecraft commands but also commands from installed server plugins (e.g. Bukkit/Paper/Spigot plugins). The server receives them exactly as if you had typed them into the server console.
api.rcon_enqueue([ "tnt 2 0.1 2 Notch", # Example: plugin command "say Boost active!", # Vanilla command ])
api.enqueue_trigger(trigger, user)
Pushes a trigger into the trigger queue. The bot processes it exactly as if a TikTok event with that trigger had arrived — including all actions assigned to it in actions.mca (vanilla, RCON, overlay, further $ commands).
[!WARNING] The first argument is a trigger — not a
$command name.A trigger is what stands left of the
:inactions.mca. That includes gift IDs (5655), reserved event names (follow,likes), or custom triggers you defined yourself.What stands right of the
:after the$— i.e. the command name likewelcome_messageorsuperjump— is not a valid trigger.follow: $welcome_message │ │ │ └── $-command name (NOT a valid trigger) └──────────── trigger (this is what you pass to enqueue_trigger)
api.enqueue_trigger("welcome_message", user)does nothing — silently ignored, no error, no warning.api.enqueue_trigger("follow", user)works —followis on the left side ofactions.mca.
[!NOTE] The trigger is placed in the queue asynchronously — it is not processed immediately within the same handler call. The RCON commands from your current handler run first, then the forwarded trigger is picked up.
Variant A — Forwarding to an existing trigger
You can fire a trigger from inside a hook that is already registered in actions.mca for a different TikTok event.
actions.mca:
follow: $welcome_message; /give @a minecraft:golden_apple 7
5655: $big_gift
def register(api):
def big_gift(user, trigger, context):
api.rcon_enqueue([f"say Huge gift from {user}!"])
# Also fire the "follow" trigger.
# → The user gets the welcome message + golden apples on top.
api.enqueue_trigger("follow", user)
def welcome_message(user, trigger, context):
api.rcon_enqueue([f"say Welcome {user}!"])
api.register_action("big_gift", big_gift)
api.register_action("welcome_message", welcome_message)
What happens on gift 5655?
- TikTok reports gift
5655→ trigger5655is processed execute_global_command("5655", …)finds$big_gift→ calls your handler- Handler sends the RCON message and pushes
"follow"into the queue - Shortly after:
execute_global_command("follow", …)runs — executes$welcome_messageand/give …
This way a gift sender automatically gets the same treatment as a new follower, without duplicating the follow logic.
[!WARNING] Watch out for infinite loops!
Never forward to the trigger that fired your own handler:
follow: $welcome_messagedef welcome_message(user, trigger, context): api.enqueue_trigger("follow", user) # ← Loop!As soon as someone follows on TikTok, the
followtrigger fires. The handler pushesfollowback into the queue, the handler fires again, pushesfollowagain — and so on.The system detects this automatically at runtime. After 3 chain steps the trigger is blocked and permanently banned for the running session:
[HOOK] [ERROR] enqueue_trigger('follow') blocked — chain depth 4 exceeds maximum (3). Trigger 'follow' is now permanently banned for this session. Possible infinite loop.Every further
enqueue_trigger("follow", ...)call — from any hook — is then immediately rejected:[HOOK] [ERROR] enqueue_trigger('follow') permanently blocked — trigger was banned after loop detection.What still executes?
enqueue_triggerdoes not throw an exception — it simply does a silentreturnback to the caller. That means:
The rest of the handler continues normally. If
welcome_messagehas more code after theenqueue_triggercall (e.g. morercon_enqueuecalls, logging, etc.), it runs in full. Only that oneenqueue_triggercall is blocked.The remaining actions from the
actions.mcaline also run normally. Given:follow: $welcome_message; /give @a minecraft:golden_apple 7The handler
welcome_messageis called (including all code after the blocked call), and the/givecommand is executed afterwards as normal — the ban only affects theenqueue_triggercall, nothing else.
Variant B — Creating your own trigger
Triggers in actions.mca do not have to be real TikTok events. You can define your own triggers — they are just as valid as follow or a gift ID, but are never fired automatically by TikTok. They only fire when you push them via enqueue_trigger.
actions.mca:
5655: $small_gift
8913: $big_gift
thank_you: $thank_you
Here thank_you is a custom key. No TikTok event is named that — it exists purely as an internal chain step that your hooks call via enqueue_trigger.
def register(api):
def small_gift(user, trigger, context):
api.rcon_enqueue(["effect give @a minecraft:speed 5 1 true"])
api.enqueue_trigger("thank_you", user)
def big_gift(user, trigger, context):
api.rcon_enqueue(["effect give @a minecraft:speed 20 3 true"])
api.enqueue_trigger("thank_you", user)
def thank_you(user, trigger, context):
api.rcon_enqueue([f"say Thank you {user} for the gift!"])
api.register_action("small_gift", small_gift)
api.register_action("big_gift", big_gift)
api.register_action("thank_you", thank_you)
What happens on gift 5655?
execute_global_command("5655", …)→ finds$small_gift→ calls your handler- Handler gives the speed effect and pushes
"thank_you"into the queue execute_global_command("thank_you", …)→ finds$thank_you→ calls thethank_youhandler- Handler sends the thank-you message
On gift 8913 the same happens via big_gift, but the same thank_you action runs at the end. The logic is written only once.
Why create your own trigger?
A custom trigger is a full actions.mca entry. That means you can assign it not just a $ hook, but the entire palette of actions.mca syntax — vanilla commands, RCON, overlay text, all at once:
thank_you: $thank_you; /playsound minecraft:entity.player.levelup master @a; >>Thanks!|{user} just donated!|4
When a hook calls api.enqueue_trigger("thank_you", user), everything happens together: your Python handler runs, the sound plays via the datapack, and the overlay text appears in the stream.
The concrete benefits:
- Reusability: Any number of hooks can call the same trigger. The shared logic lives in one place — in the hook and/or in
actions.mca. - Separation of code and configuration: The streamer can add commands, sounds, or overlay text to the trigger in
actions.mcawithout touching the Python file. You write the logic, the streamer configures the rest. - Chaining: Triggers can fire other triggers — letting you build complex sequences from simple, testable building blocks.
- Extensible later: If the streamer wants to add a firework effect later, they change one line in
actions.mca— done. No code deployment needed.
[!TIP] Want to test if your triggers are working? Take a look at the GUIDE.md, chapter "Test Your Triggers Without TikTok". There you’ll find a test tool that lets you try out your triggers without any TikTok connection.
If you have Python installed, you can also run the file
tests/send_trigger.pydirectly in the development structure to test triggers conveniently from the console—without using the .exe. Even when testing withsend_trigger.py, the project must be built and all other components must be running in the release environment.
api.log(msg)
Prints a message to the console with an automatic [HOOK] prefix. Useful for debugging.
api.log("Hook loaded successfully")
# Output: [HOOK] Hook loaded successfully
api.config
Read-only access to the loaded values from config.yaml. Returns a nested dictionary.
port = api.config.get("RCON", {}).get("Port", 25575)
Using One Handler for Multiple $ Commands
Sometimes multiple $ commands should react similarly but with slight differences. In this case, register the same function under multiple names and use the trigger argument in the handler to distinguish which command is currently active.
def register(api):
def power_up(user, trigger, context):
effects = {
"superjump": "minecraft:jump_boost",
"superrun": "minecraft:speed",
"superheal": "minecraft:regeneration",
}
effect = effects.get(trigger)
if effect:
api.rcon_enqueue([f"effect give @a {effect} 10 5 true"])
api.register_action("superjump", power_up)
api.register_action("superrun", power_up)
api.register_action("superheal", power_up)
The user then adds to actions.mca:
5655: $superjump
16111: $superrun
7934: $superheal
This way you only need one handler for any number of related commands.
Multiple Actions in One File
A single .py file can register as many actions as needed. You are not limited to one handler per file:
def register(api):
def on_follow(user, trigger, context):
api.rcon_enqueue([f"say {user} is now following!"])
def on_big_gift(user, trigger, context):
api.rcon_enqueue([
"summon minecraft:firework_rocket ~ ~ ~",
f"say Thank you {user}!",
])
api.register_action("follow_effect", on_follow)
api.register_action("big_gift_effect", on_big_gift)
Error Handling
Errors inside a hook handler do not crash the bot. They are caught and printed to the console with a [HOOK] prefix:
[HOOK] [WARN] Error in action 'welcome_message': name 'undefined_var' is not defined
There is also a safety net during loading:
- Syntax error in a hook file → only that file is skipped, all others load normally
- Missing
register()function → file is skipped with an error message - Exception in
register()→ file is skipped, error is logged - Exception in handler at runtime → error is logged, bot keeps running
Built-in Commands Cannot Be Overridden
[!WARNING] Certain $-commands are built into the system and cannot be overridden by your own hooks.
Currently reserved names:
randomIf you try to register one of these names with
api.register_action("random", ...), you will see this error at load time:[HOOK] [ERROR] 'random' is a reserved built-in command — cannot be overridden by a hook.These commands are handled internally by main.py and are blocked for custom hooks.