Discord Slash Commands
This example's ctx.db calls do not match the shipped API: ctx.db.get(table, id) takes a row's _id (a UUID Lumio generates), not a platform user ID, and there is no ctx.db.set. Look rows up by a column with ctx.db.list(table, [{ field, op: "eq", value }]), and declare the tables in server/schema.ts (see Schema). See Bot Module Context.
When a bot module declares "discord" in its platforms array, its commands are automatically registered as Discord slash commands. This page explains the mapping, discord_options, and the registration lifecycle.
How it works
Extension developers declare commands once in lumio.config.json. Lumio handles the platform-specific dispatch:
| Platform | User types | Matching method |
|---|---|---|
| Twitch | !points 100 | IRC prefix match |
| YouTube | !points 100 | Chat prefix match |
| Kick | !points 100 | Chat prefix match |
| Discord | /points amount:100 | Slash command interaction |
Your handler code is identical for all platforms. The args array is populated the same way regardless of where the command originated:
- Prefix command
!points 100producesargs: ["100"] - Discord slash command
/points amount:100producesargs: ["100"](positional from option order)
Registration flow
When an extension with Discord support is installed on an account whose server is linked under Connections → Discord through the global Lumio bot's verified invite:
- The API reads the extension manifest commands
- The API registers Discord slash commands via the Discord API (
POST /applications/\{app_id\}/guilds/\{guild_id\}/commands) - The Discord bot receives interactions via the Discord Gateway
- The bot dispatches to the Worker using the same sync HTTP path as prefix commands
Discord may take up to 1 hour to propagate guild-level command changes.
Adding discord_options
By default, commands are registered as simple slash commands with no options. To add typed parameters, use the discord_options field:
{
"triggers": {
"commands": [
{
"name": "points",
"description": "Show a user's points balance",
"cooldown_global": 5,
"min_role": "everyone",
"discord_options": [
{
"name": "user",
"type": "user",
"description": "User to check",
"required": false
}
]
},
{
"name": "give",
"description": "Give points to another user",
"cooldown_global": 10,
"min_role": "moderator",
"discord_options": [
{
"name": "user",
"type": "user",
"description": "Recipient",
"required": true
},
{
"name": "amount",
"type": "integer",
"description": "Points to give",
"required": true
}
]
}
]
}
}
Option types
| Type | Discord type | Description |
|---|---|---|
"string" | STRING | Text input |
"integer" | INTEGER | Whole number |
"boolean" | BOOLEAN | True/false toggle |
"user" | USER | Discord user mention picker |
"channel" | CHANNEL | Channel picker |
"role" | ROLE | Role picker |
Non-Discord platforms ignore the discord_options field entirely.
Args mapping
Discord options are mapped to the args array by position (order of declaration):
{
"discord_options": [
{ "name": "user", "type": "user", "required": true },
{ "name": "amount", "type": "integer", "required": true }
]
}
When a user runs /give user:@jane amount:50, the handler receives args: ["jane_user_id", "50"] - matching the declaration order.
For prefix platforms, !give @jane 50 produces the same args: ["@jane", "50"].
Handling Discord-specific behavior
Your handler can check ctx.user.platform to detect which platform triggered the command:
Declare the table in server/schema.ts (the default instance scope keeps rows on the install's account):
import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";
export default defineSchema({
user_points: defineTable({
user_id: v.string(),
platform: v.string(),
balance: v.number(),
}),
});
import { command } from "@zaflun/lumio-sdk/server";
export const points = command("points", async (ctx, args) => {
const targetUser = args[0] ?? ctx.user.id;
const [points] = await ctx.db.list("user_points", [
{ field: "user_id", op: "eq", value: targetUser },
{ field: "platform", op: "eq", value: ctx.user.platform },
]);
if (ctx.user.platform === "discord") {
// Discord supports richer formatting
return { reply: `**${ctx.user.displayName}** has **${points?.balance ?? 0}** points` };
}
return { reply: `${ctx.user.displayName} has ${points?.balance ?? 0} points` };
});
Sync on update
When you publish a new version that changes commands:
- Old Discord slash commands are removed
- New commands are registered with the Discord API
- Discord propagates changes (up to 1 hour for guild commands)
During the propagation window, users may see stale commands in the Discord autocomplete. This is a Discord API limitation.
Discord-only triggers
Keywords and patterns work in Discord the same as other platforms - they match against message content in text channels. Events use Discord-specific event types if available.
Timers fire to Discord channels the same way they fire to other platforms.