Skip to main content

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:

TriggerWhere matchedExecutionLatency
CommandBot (prefix match)Sync HTTP (500ms deadline)~50-200ms
KeywordBot (substring)Async (fire-and-forget)Fire-and-forget
PatternBot (compiled regex)Async (fire-and-forget)Fire-and-forget
EventWorker (event subscription)AsyncFire-and-forget
TimerWorker (internal clock)Async (self-triggered)N/A
ModerationBot (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​

FieldTypeRequiredDescription
namestringYesCommand name (without ! prefix). Alphanumeric + underscore, 1-32 characters.
descriptionstringYesShort description shown in the dashboard and Discord slash command registration.
cooldown_globalintegerNoGlobal cooldown in seconds (0 = no cooldown). Applies to all users.
cooldown_userintegerNoPer-user cooldown in seconds (0 = no cooldown). Applies individually.
min_rolestringNoMinimum role required to use the command. Default: "everyone".
discord_optionsarrayNoDiscord 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​

RuleConstraint
Name format^[a-zA-Z0-9_]+$, 1-32 characters
Max commands per extension20
Cooldown valuesInteger >= 0

min_role values​

ValueWho 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​

RuleConstraint
Minimum length3 characters
Max keywords per extension50

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​

RuleConstraint
Max length200 characters
Max patterns per extension10
Regex engineRust regex crate (linear-time, no backreferences)
ReDoS safetyGuaranteed 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​

FieldTypeRequiredDescription
namestringYesUnique identifier for this timer
intervalintegerYesFiring interval in seconds (minimum: 60)
handlerstringYesName 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​

RuleConstraint
Minimum interval60 seconds
HandlerMust 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: true runs 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 } (a defineTimer default export) runs one timer named timer at the install's intervalSeconds setting 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.