Bot Modules
Lumio's built-in bot modules (Link Protection, Spam Protection, Word Filter, Timed Messages) are system extensions that run through the same V8 execution model as third-party bot modules. All bot modules -- built-in and custom -- use the same SDK handlers (defineCommand, defineModerate, etc.).
Bot modules are extensions that handle chat messages, platform events, and timed actions across Twitch, YouTube, Kick, and Discord. They run server-side in a V8 isolate - no browser required.
This guide walks through creating a bot module from scratch, writing handlers, testing locally, and deploying.
Prerequisites
- Node.js 22.13+
- A Lumio developer account
- The
@zaflun/lumio-clipackage installed globally
npm install -g @zaflun/lumio-cli
lumio login
Create a bot module project
Use lumio init and select the bot_module category:
lumio init my-bot-module
When prompted:
- Category:
bot_module - Platforms: Select one or more:
twitch,youtube,kick,discord - Server functions: Yes (auto-enabled for bot modules)
This generates the following project structure:
my-bot-module/
├── lumio.config.json
├── server/
│ ├── schema.ts
│ └── functions.ts
├── package.json
└── tsconfig.json
A bot module needs no src/ directory: without targets it is a server-only extension with no browser UI surface. To show what viewers send through chat or channel point rewards, add editor and layer to targets and a src/ directory; the streamer then places the bot module's layer on an overlay like a widget. See Viewer input.
Configure the manifest
Open lumio.config.json and define your triggers and permissions:
{
"$schema": "https://lumio.vision/schemas/lumio.config.schema.json",
"extension_id": "your-uuid-here",
"name": "My Bot Module",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["twitch", "youtube"],
"server": true,
"permissions": ["chat:send", "chat:read", "events:read"],
"triggers": {
"commands": [
{
"name": "hello",
"description": "Greet the user",
"cooldown_global": 5,
"cooldown_user": 10,
"min_role": "everyone"
}
],
"keywords": ["gg"],
"events": ["twitch:subscribe"],
"timers": [
{
"name": "reminder",
"interval": 300,
"handler": "periodicReminder"
}
]
},
"config_schema": [
{
"key": "greeting",
"type": "string",
"label": "Greeting message",
"default_value": "Hello",
"required": true
}
]
}
Key differences from widget/overlay extensions:
categorymust be"bot_module"platformsdeclares which chat platforms this module supportstriggersdefines when handlers fire (commands, keywords, patterns, events, timers)- Read permissions gate the triggers that see data. Keywords, patterns and
moderationneedchat:read;eventsneedevents:read. Without the permission Lumio drops those triggers and your handlers are never called. Commands and timers need neither (Permissions) targetsis optional; omit it for a server-only module, or listeditorandlayerto display results in the dashboard and overlay
Define the database schema
If your module needs persistent storage, define tables in server/schema.ts:
// server/schema.ts
import { defineSchema, defineTable, column } from "@zaflun/lumio-sdk/server";
export default defineSchema({
greetings: defineTable({
user_id: column.string(),
count: column.number(),
last_greeted: column.number(),
}),
});
Write handlers
Bot modules use handler wrappers from @zaflun/lumio-sdk/server. Each wrapper corresponds to a trigger type:
// server/functions.ts
import { command, keyword, event, timer } from "@zaflun/lumio-sdk/server";
// Command handler — fires when a user types !hello
export const hello = command("hello", async (ctx, args) => {
const greeting = ctx.config.greeting ?? "Hello";
return { reply: `${greeting}, ${ctx.user.name}!` };
});
// Keyword handler — fires when "gg" appears in chat
export const hypeDetector = keyword("gg", async (ctx, message) => {
await ctx.cache.increment("hype_count", 1);
return null; // no reply
});
// Event handler — fires on Twitch subscriptions
export const welcomeSub = event("twitch:subscribe", async (ctx, evt) => {
return {
reply: `Welcome ${evt.user_name}! Thanks for subscribing!`,
};
});
// Timer handler — fires every 300 seconds
export const periodicReminder = timer("reminder", async (ctx) => {
return { reply: "Remember to follow the channel!" };
});
Every handler receives a ctx object with access to the database, cache, secrets, config, and more. See the Bot Module Context reference for the full API.
Handler types
| Wrapper | Trigger | Execution | Return |
|---|---|---|---|
command(name, handler) | Chat prefix (!name) | Sync (500ms deadline) | \{ reply?, actions? \} or null |
keyword(word, handler) | Substring match | Async (fire-and-forget) | \{ reply?, actions? \} or null |
pattern(regex, handler) | Regex match | Async (fire-and-forget) | \{ reply?, actions? \} or null |
event(type, handler) | Stream event listed in triggers.events | Async (fire-and-forget) | \{ reply?, actions? \} or null |
| timer(name, handler) / defineTimer | Interval clock (triggers.timers) | Async (self-triggered) | \{ reply?, actions? \} or null |
| defineModerate(handler) / moderate(handler) | Every chat message on Twitch, Kick, YouTube (triggers.moderation: true) | Sync (500ms budget, fail-open) | \{ action: "allow" \| "delete" \| "timeout" \| "ban" \| "warn", duration?, reason?, message? \} |
Test events
An event handler receives the event row with event.raw.isTest: true and event.raw.showOnStream when a test is sent. The install's reserved config.test_events setting is prefix by default: the handler runs, and its reply and chat:send messages start with [Test] once. ignore prevents the handler from running for tests. The setting is outside your manifest's config_schema and takes only prefix or ignore; an installer changes it through the module's On test events control. A configure request replaces the whole install config, so preserve your other keys.
Moderation actions (chat:ban, chat:unban, chat:timeout, chat:delete) never run on a test. Stream picture and sound actions (overlay:update_layer, obs:*, spotify:*, events:emit) need showOnStream: true; other actions can run. Inspect event.raw.isTest before your handler performs an external side effect.
Where replies go:
- command, keyword, pattern - the chat the message came from.
- event - the chat of the platform the event came from (Twitch, Kick, YouTube). Discord events have no chat channel, so a reply is dropped.
- timer - every chat platform the install runs on, except Discord. An install whose config sets
onlyWhenLive: trueruns its timer only while at least one of those platforms is live, and posts only on the live ones.
Each timer runs once per interval, however many worker replicas Lumio runs. A missed interval (for example during a deploy) is skipped, not caught up.
Moderation
A module with "triggers": { "moderation": true } and a moderate handler is called for every chat message of the account on Twitch, Kick and YouTube. Messages from the broadcaster and moderators are not passed to it. On Discord moderate handlers are not called, because no chat action supports Discord.
import { defineModerate } from "@zaflun/lumio-sdk/server";
export default defineModerate(async (ctx) => {
if (ctx.message.text.includes("buy-followers")) {
return { action: "timeout", duration: 300, reason: "Spam link" };
}
return { action: "allow" };
});
Chat never waits for your handler. Each call has a 500 ms budget; a timeout, an error, or more than 16 checks in flight for the account lets the message through (fail-open). The decision becomes a chat action, and your manifest must declare that action as a permission or it is refused:
| Decision | Action Lumio runs | Permission to declare |
|---|---|---|
allow | none | - |
delete | chat:delete on the message | chat:delete |
timeout | chat:timeout for the sender, duration seconds (default 600) | chat:timeout |
ban | chat:ban for the sender | chat:ban |
warn | chat:send with message (or reason); nothing if both are empty | chat:send |
A chat:delete from a bot module removes the message on the platform. Lumio marks it as deleted in its own chat history only if the message is at most 6 hours old. Moderate handlers delete messages seconds after they arrive, so this limit only affects deletes of older message ids.
Actions
Any handler can return actions next to its reply - the same actions a widget runs with useLumioAction (chat:send, chat:timeout, obs:set_scene, spotify:skip, …):
export const shoutout = command("so", async (ctx, args) => ({
reply: `Go follow ${args[0]}!`,
actions: [{ action: "obs:set_scene", params: { scene_name: "Shoutout" } }],
}));
- Actions run after the reply has been sent, at most once. A failed action is not retried (a retried ban is not safe).
- Each action passes the same checks as a widget action: your manifest must declare the action name in
permissions, the account's plan must include the action's feature, and the params must be valid. A refused action does not stop the next one. - At most 20 actions per handler result, and at most 120 actions per minute per install. Actions beyond the cap are refused until the next minute.
- Every outcome - success, refusal or failure, including moderation decisions - is written to the install's log (shown with the install in the streamer's dashboard), at most one entry per second per install. An
allowdecision writes nothing. Log entries carry the action name and, for chat actions, the platform and target ids - never message text. - Bans, unbans and timeouts are also recorded in the streamer's audit log; message deletions by a bot module appear only in the install log.
Test locally
Start the bot module dev server:
lumio dev --bot-module
This opens the chat panel in the same mock-host session that runs any editor and layer surfaces. Type a chat message in the terminal, or use a second terminal:
lumio trigger chat "!hello" --user alice --role subscriber
lumio trigger twitch:subscribe
lumio trigger --timer reminder
The local V8 replica runs the matched handler, then applies returned actions through the mock permission and plan-feature gates. The sandbox shows replies, actions, server calls, and local ctx.db rows. Use --no-cooldown on lumio dev to focus on handler logic. See lumio trigger for scenarios and paced loads, and Local Development for the bot-module workflow.
You can also test individual handlers:
lumio run command:hello --args "" --user "testuser"
Deploy
Build and deploy like any other extension:
lumio deploy
After deployment, the bot module appears in the Lumio Store under the Bot Module category. Users install it from the Store and configure triggers, cooldowns, and settings in their dashboard.
What happens at runtime
- A chat message arrives at the platform bot (Twitch, YouTube, Kick or Discord)
- On Twitch, Kick and YouTube, every installed moderation module (the built-in ones included) gets the message in the background
- The bot matches the message against installed extension triggers (commands, keywords, patterns)
- On match, the bot dispatches to the Bot Module Worker via HTTP (sync) or Redis (async)
- The Worker executes your handler in a V8 isolate
- The reply is delivered to chat
- Any returned actions run afterwards, through the permission checks above
Stream events and timers do not start at the bot: the Bot Module Worker receives the account's stream events and runs its own timer clock, then follows steps 5-7.
Next steps
- Local Development - chat simulator, hot-reload, mock users
- Bot Module Context - full
ctxAPI reference - Triggers - trigger configuration reference
- Security - rate limits, permissions, audit log
- Cache & Background Jobs -
ctx.cacheandctx.defer()usage - Examples - complete bot module examples