Skip to main content

Bot Modules

Built-in 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-cli package 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:

  • category must be "bot_module"
  • platforms declares which chat platforms this module supports
  • triggers defines when handlers fire (commands, keywords, patterns, events, timers)
  • Read permissions gate the triggers that see data. Keywords, patterns and moderation need chat:read; events need events:read. Without the permission Lumio drops those triggers and your handlers are never called. Commands and timers need neither (Permissions)
  • targets is optional; omit it for a server-only module, or list editor and layer to 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​

WrapperTriggerExecutionReturn
command(name, handler)Chat prefix (!name)Sync (500ms deadline)\{ reply?, actions? \} or null
keyword(word, handler)Substring matchAsync (fire-and-forget)\{ reply?, actions? \} or null
pattern(regex, handler)Regex matchAsync (fire-and-forget)\{ reply?, actions? \} or null
event(type, handler)Stream event listed in triggers.eventsAsync (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: true runs 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:

DecisionAction Lumio runsPermission to declare
allownone-
deletechat:delete on the messagechat:delete
timeoutchat:timeout for the sender, duration seconds (default 600)chat:timeout
banchat:ban for the senderchat:ban
warnchat:send with message (or reason); nothing if both are emptychat: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 allow decision 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​

  1. A chat message arrives at the platform bot (Twitch, YouTube, Kick or Discord)
  2. On Twitch, Kick and YouTube, every installed moderation module (the built-in ones included) gets the message in the background
  3. The bot matches the message against installed extension triggers (commands, keywords, patterns)
  4. On match, the bot dispatches to the Bot Module Worker via HTTP (sync) or Redis (async)
  5. The Worker executes your handler in a V8 isolate
  6. The reply is delivered to chat
  7. 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​