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).
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.
{
"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
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: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 and 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.