Triggers
The triggers field in lumio.config.json defines when your bot module handlers fire. Triggers are only allowed when category is "bot_module" - other extension categories with a triggers field are rejected at validation.
Overview
{
"category": "bot_module",
"triggers": {
"commands": [...],
"keywords": [...],
"patterns": [...],
"events": [...],
"timers": [...]
}
}
Each trigger type has different matching behavior and execution characteristics:
| Trigger | Where matched | Execution | Latency |
|---|---|---|---|
| Command | Bot (prefix match) | Sync HTTP (500ms deadline) | ~50-200ms |
| Keyword | Bot (substring) | Async (fire-and-forget) | Fire-and-forget |
| Pattern | Bot (compiled regex) | Async (fire-and-forget) | Fire-and-forget |
| Event | Worker (event subscription) | Async | Fire-and-forget |
| Timer | Worker (internal clock) | Async (self-triggered) | N/A |
| Moderation | Bot (every message, Twitch/Kick/YouTube) | Sync HTTP (500ms budget, fail-open) | Never delays chat |
Every handler's returned actions run after its reply, at most once, through the manifest-permission gate (see Bot Modules → Actions).
Read permissions. Keywords, patterns and moderation see chat the viewer did not address to your module, so they run only when permissions contains "chat:read". Events run only with "events:read". Without the permission Lumio drops the trigger and your handler is never called; lumio dev warns about it. Commands and timers need neither (Permissions).
Commands
Chat commands triggered by a prefix (e.g., !points).
{
"triggers": {
"commands": [
{
"name": "points",
"description": "Show your current points balance",
"cooldown_global": 5,
"cooldown_user": 10,
"min_role": "everyone"
}
]
}
}
Command fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Command name (without ! prefix). Alphanumeric + underscore, 1-32 characters. |
description | string | Yes | Short description shown in the dashboard and Discord slash command registration. |
cooldown_global | integer | No | Global cooldown in seconds (0 = no cooldown). Applies to all users. |
cooldown_user | integer | No | Per-user cooldown in seconds (0 = no cooldown). Applies individually. |
min_role | string | No | Minimum role required to use the command. Default: "everyone". |
discord_options | array | No | Discord slash command options. Ignored on other platforms. See Discord Slash Commands. |
Lumio enforces min_role, cooldown_global and cooldown_user before your
handler runs: the bot checks the sender's role and the cooldowns (the same check
dashboard commands get), and the Bot Module Worker checks min_role again. A sender
below min_role, or a command still on cooldown, is ignored silently and your
handler is not called. A streamer can tighten or relax these values per install in
the dashboard; the install's value wins over your default.
Command validation rules
| Rule | Constraint |
|---|---|
| Name format | ^[a-zA-Z0-9_]+$, 1-32 characters |
| Max commands per extension | 20 |
| Cooldown values | Integer >= 0 |
min_role values
| Value | Who can use |
|---|---|
"everyone" | All viewers |
"follower" | Followers and above |
"subscriber" | Subscribers and above |
"vip" | VIPs and above |
"moderator" | Moderators and above |
"broadcaster" | Broadcaster only |
Users can raise (but not lower) the minimum role in their dashboard override settings.
Keywords
Substring matches against chat messages. When a keyword appears anywhere in a message, the associated handler fires.
{
"triggers": {
"keywords": ["gg", "nice shot", "pog"]
}
}
Keyword validation rules
| Rule | Constraint |
|---|---|
| Minimum length | 3 characters |
| Max keywords per extension | 50 |
Keywords are matched as case-insensitive substrings. A keyword of "gg" matches "gg wp", "lol gg", and "gg".
Patterns
Regex patterns matched against chat messages. When a pattern matches, the handler receives the matched string.
{
"triggers": {
"patterns": ["https?://clips\\.twitch\\.tv/\\S+"]
}
}
Pattern validation rules
| Rule | Constraint |
|---|---|
| Max length | 200 characters |
| Max patterns per extension | 10 |
| Regex engine | Rust regex crate (linear-time, no backreferences) |
| ReDoS safety | Guaranteed by construction (Rust regex crate) |
Pattern matching runs in Rust on the bot side. JavaScript RegExp is never used for trigger matching. The matched substring is passed to the handler as the matched parameter:
import { pattern } from "@zaflun/lumio-sdk/server";
export const clipLogger = pattern(
"https?://clips\\.twitch\\.tv/\\S+",
async (ctx, message, matched) => {
// matched = "https://clips.twitch.tv/FunnyClip123"
return { reply: `Clip from ${ctx.user.name}: ${matched}` };
}
);
Events
Platform events that trigger handlers when they occur.
{
"permissions": ["events:read"],
"triggers": {
"events": ["twitch:subscribe", "twitch:raid", "youtube:superchat"]
}
}
Each event type must be a valid Lumio event type. The handler runs once for every matching event of the installing account and receives the event payload - the same payload widgets receive. A reply goes to the chat of the platform the event came from (Twitch, Kick, YouTube); Discord events have no chat channel, so a reply to them is dropped.
import { event } from "@zaflun/lumio-sdk/server";
export const welcomeSub = event("twitch:subscribe", async (ctx, evt) => {
return { reply: `Welcome ${evt.user_name}!` };
});
Available event types
Twitch: twitch:follower, twitch:subscribe, twitch:gift_sub, twitch:resub, twitch:cheer, twitch:raid, twitch:reward, twitch:reward_status, twitch:reward_add, twitch:reward_update, twitch:reward_remove, twitch:hype_train, twitch:hype_train_end, twitch:poll, twitch:poll_end, twitch:prediction, twitch:prediction_lock, twitch:prediction_end, twitch:goal, twitch:goal_end, twitch:ad_break, twitch:ban, twitch:unban, twitch:stream_online, twitch:stream_offline, twitch:stream_update
YouTube: youtube:subscribe, youtube:member, youtube:superchat, youtube:supersticker, youtube:gift_membership, youtube:gift_membership_received, youtube:poll, youtube:stream_online, youtube:stream_offline
Kick: kick:follower, kick:subscribe, kick:resub, kick:gift, kick:kicks, kick:reward, kick:reward_status, kick:stream_online, kick:stream_offline, kick:stream_update
Spotify: spotify:track, spotify:play, spotify:pause, spotify:skip, spotify:volume, spotify:queue_add, spotify:device, spotify:playlist_add, spotify:playlist_remove, spotify:playlist_create, spotify:playlist_edit, spotify:playlist_delete
StreamElements: streamelements:tip
Shopify: shopify:order, shopify:order_paid, shopify:product
Timers
Recurring handlers that fire at a fixed interval.
{
"triggers": {
"timers": [
{
"name": "reminder",
"interval": 300,
"handler": "periodicReminder"
},
{
"name": "adDisclaimer",
"interval": 1800,
"handler": "adMessage"
}
]
}
}
Timer fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for this timer |
interval | integer | Yes | Firing interval in seconds (minimum: 60) |
handler | string | Yes | Name of the exported server function. The worker selects the export by the timer name (the first argument of timer(name, …)), so keep timer(name, …) equal to name |
Timer validation rules
| Rule | Constraint |
|---|---|
| Minimum interval | 60 seconds |
| Handler | Must match an exported server function name |
Timer behavior
- Timers are per-install, not per-account. Two accounts installing the same extension have independent timers.
- Each timer runs once per interval, however many Bot Module Worker replicas run. A missed interval (for example during a deploy) is skipped, not caught up.
- Timer replies are sent to every chat platform the install runs on, except Discord.
- If a platform bot is offline, the timer reply for that platform is silently dropped. There is no retry - timers are best-effort.
- An install whose config sets
onlyWhenLive: trueruns its timers only while at least one of its chat platforms is live, and replies only on the live ones. - Disabling the install, setting the kill switch or uninstalling stops its timers at once; a changed interval takes effect at once.
- The interval comes from the manifest. The single-timer form
"triggers": { "timer": true }(adefineTimerdefault export) runs one timer namedtimerat the install'sintervalSecondssetting instead (clamped to 60 s - 24 h, default 300 s), so streamers can change it in the dashboard.
Moderation
A moderation handler is switched on with "moderation": true in triggers, next to the handler in your code:
{
"permissions": ["chat:read", "chat:delete", "chat:timeout"],
"triggers": { "moderation": true }
}
import { defineModerate } from "@zaflun/lumio-sdk/server";
export default defineModerate(async (ctx) => {
if (ctx.message.text.includes("banned-site.com")) {
return { action: "timeout", duration: 60, reason: "Banned link" };
}
return { action: "allow" };
});
The bot calls it for every chat message of the account on Twitch, Kick and YouTube, except messages from the broadcaster. Moderator messages are passed on with user.isMod: true; skip them in the handler if the module should exempt moderators. Discord messages are not moderated, because no chat action supports Discord. Each call has a 500ms budget and runs beside chat, never in front of it: a timeout, an error or more than 16 checks in flight for the account lets the message through.
The decision becomes a chat action, which runs only if the manifest declares it: delete → chat:delete, timeout → chat:timeout (duration seconds, default 600), ban → chat:ban, warn → chat:send (message, else reason). allow does nothing. The chat:ban permission triggers enhanced manual review during store approval.