Slash Commands
Add commands to Discord's / picker.
Your commands show in Discord's / picker under Unbound. They run on the phone, so they work in every channel and DM without a bot.
Quick Start
{
"SHRUG_DESC": "Send a message with a shrug on the end",
"SHRUG_MESSAGE_DESC": "What to say"
}import { commands } from '@unbound-app/api';
export function startShrug() {
commands.register({
shrug: {
description: 'SHRUG_DESC',
options: {
message: commands.option.string({ description: 'SHRUG_MESSAGE_DESC', required: true }),
},
execute: ({ message }) => `${message} ¯\\_(ツ)_/¯`,
},
});
}descriptionis a message key. Literal text or a missing key is a type error.- Command and option names stay literal: Discord accepts only lowercase ASCII.
executegets typed options. An optional option left out isundefined.
Unbound removes your commands when the plugin stops. The picker follows Discord's language, falling back to en-US.
Reference
| Export | Signature | What it does |
|---|---|---|
commands.register | register(tree) => () => void | Registers commands keyed by name. Returns a remover |
commands.unregister | unregister(name) => void | Removes one command |
commands.option | string, integer, number, boolean, user, channel, role, choice | Option builders |
| Command field | Type | Meaning |
|---|---|---|
description | A key of i18n/en-US.json | Text under the name in the picker |
options | Record<string, OptionSpec> | Options, keyed by name |
execute | (options, { channel, guild }) => Reply | Promise<Reply> | Runs on send |
subcommands | A tree of commands | Groups commands. Replaces execute |
Full signatures: commands in the SDK reference.
Replies
execute may return any of these, or a promise of one.
| Return | Result |
|---|---|
| A string | Sent as a normal message from the user |
{ content } | The same. tts: true sends text-to-speech |
{ ephemeral: true, content } | Shown only to the user, as a bot message. Can add embeds |
| Nothing | Nothing is sent |
If execute throws, the user sees the error as an ephemeral message. The second argument holds the channel and its guild (undefined in DMs):
commands.register({
'channel-id': {
description: 'CHANNEL_ID_DESC',
execute: (_options, { channel }) => ({ ephemeral: true, content: channel.id }),
},
});Options
Every builder takes { description, required }. Without a description, the picker shows the option's name.
| Builder | Handler gets | Extra settings |
|---|---|---|
string() | A string | minLength, maxLength, autocomplete |
integer(), number() | A number | minValue, maxValue, autocomplete |
boolean() | true or false | None |
user(), channel(), role() | The picked user, channel or role | None |
choice(map, settings) | One of the map's values | None |
Autocomplete labels are plain strings, not keys.
Choices
choice maps each value to its label key, in picker order. Values are strings. For numbers, use integer() or number() with minValue and maxValue.
{
"COIN_DESC": "Flip a coin",
"COIN_CALL_DESC": "Heads or tails",
"COIN_HEADS": "Heads",
"COIN_TAILS": "Tails"
}commands.register({
coin: {
description: 'COIN_DESC',
options: {
call: commands.option.choice(
{ heads: 'COIN_HEADS', tails: 'COIN_TAILS' },
{ description: 'COIN_CALL_DESC', required: true },
),
},
// `call` is 'heads' | 'tails'.
execute: ({ call }) => (Math.random() < 0.5 ? call : `Not ${call}`),
},
});Subcommands
Use subcommands instead of execute for /notes add and /notes list:
const notes: string[] = [];
commands.register({
notes: {
description: 'NOTES_DESC',
subcommands: {
add: {
description: 'NOTES_ADD_DESC',
options: { text: commands.option.string({ required: true }) },
execute: ({ text }) => {
notes.push(text);
},
},
list: {
description: 'NOTES_LIST_DESC',
execute: () => ({ ephemeral: true, content: notes.join('\n') }),
},
},
},
});Limits
- Names: 1 to 32 lowercase letters, digits,
-or_. - Descriptions and choice labels: keys whose en-US text is 1 to 100 characters.
- Labels within one
choicediffer in en-US. - At most 25 options per command, 1 to 25 subcommands per group, 1 to 25 values per
choice. - A name another plugin registered is taken.
register throws a CommandDefinitionError naming the command and key:
Invalid command "/coin" from my-plugin: description "COIN_EDGE" is not a key in i18n/en-US.json. Use a key, and add it there.Build Errors
ubd build checks every commands.register({ ... }) it can read and lists all problems, then fails. ubd dev prints them and keeps the last good build. Trees built at runtime are checked at register time.
Command text must be message keys from i18n/en-US.json (3 problems):
src/index.ts:3:23 /coin description "Flip a coin" is literal text where a message key is required. Fix: use its key "COIN_DESC".
src/index.ts:9:32 /coin option "call" choice "edge" label key "COIN_EDGE" is missing from i18n/en-US.json. Fix: add "COIN_EDGE": "<en-US text>" to i18n/en-US.json.
src/notes.ts:2:1 /notes registers commands, but the plugin has no i18n/ folder. Fix: add i18n/en-US.json with a key for each command description.It reports missing keys, literal text, en-US text over 100 characters, option.choice called with an array (the old form), and commands without an i18n/ folder.