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

i18n/en-US.json
{
	"SHRUG_DESC": "Send a message with a shrug on the end",
	"SHRUG_MESSAGE_DESC": "What to say"
}
src/shrug/index.ts
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} ¯\\_(ツ)_/¯`,
		},
	});
}
  • description is a message key. Literal text or a missing key is a type error.
  • Command and option names stay literal: Discord accepts only lowercase ASCII.
  • execute gets typed options. An optional option left out is undefined.

Unbound removes your commands when the plugin stops. The picker follows Discord's language, falling back to en-US.

Reference

ExportSignatureWhat it does
commands.registerregister(tree) => () => voidRegisters commands keyed by name. Returns a remover
commands.unregisterunregister(name) => voidRemoves one command
commands.optionstring, integer, number, boolean, user, channel, role, choiceOption builders
Command fieldTypeMeaning
descriptionA key of i18n/en-US.jsonText under the name in the picker
optionsRecord<string, OptionSpec>Options, keyed by name
execute(options, { channel, guild }) => Reply | Promise<Reply>Runs on send
subcommandsA tree of commandsGroups commands. Replaces execute

Full signatures: commands in the SDK reference.

Replies

execute may return any of these, or a promise of one.

ReturnResult
A stringSent 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
NothingNothing 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.

BuilderHandler getsExtra settings
string()A stringminLength, maxLength, autocomplete
integer(), number()A numberminValue, maxValue, autocomplete
boolean()true or falseNone
user(), channel(), role()The picked user, channel or roleNone
choice(map, settings)One of the map's valuesNone

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.

i18n/en-US.json
{
	"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 choice differ 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.