🎯 Command Registration
Robo.js automatically keeps your Slash and Context commands in sync with Discord during robo dev and robo build. For teams that need finer control, Robo now ships an imperative API to register commands on demand. This unlocks workflows like:
- Testing command changes in specific guilds before global rollout
- Registering different command subsets to different servers
- Custom deployment steps in CI/CD pipelines
- Conditional registration based on environment or configuration
Automatic Registration
By default, Robo handles command registration for you during robo dev and robo build. This is the recommended path for most projects and requires no extra setup. Learn how commands work in the main guide: Slash Commands.
Manual Registration
For advanced control, use the registerSlashCommands() API. You decide what to register, where to register it (global vs specific guilds), and when to run it. The most common use case is registering to a test guild first, then promoting globally once verified.
API Reference
registerSlashCommands(entries, options)
- Entries parameter
commands?: Record<string, CommandEntry>– Slash commands from the manifestmessageContext?: Record<string, ContextEntry>– Message context commandsuserContext?: Record<string, ContextEntry>– User context commands
- Options parameter
guildIds?: string[]– Target guild IDs for scoped registration (omit for global)force?: boolean– Force clean re-registration (default:false)clientId?: string– Discord client ID override (falls back to env)token?: string– Discord bot token override (falls back to env)
- Return value
success: boolean– Overall success indicatorregistered: number– Count of successfully registered commandserrors: RegisterCommandsError[]– Array of errors{ command, error, type }wheretypeisvalidation | api | timeoutretries?: RegisterCommandsRetry[]– Optional retry info for rate-limited requests
Basic Usage
Use the manifest plus the registration API for full control. The following examples use the Compiler API to load the manifest.
- JavaScript
- TypeScript
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const result = await registerSlashCommands(
{
commands: manifest.commands,
messageContext: manifest.context.message,
userContext: manifest.context.user
},
{
// Global registration: omit guildIds
}
)
logger.info('Registration result:', result)
logger.info(`Registered: ${result.registered}`)
if (!result.success) {
logger.warn('Some commands failed to register', { errors: result.errors })
await logger.flush()
process.exit(1)
}
await logger.flush()
}
main().catch(async (err) => {
logger.error(err)
await logger.flush()
process.exit(1)
})
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const TEST_GUILDS = [process.env.DISCORD_TEST_GUILD_ID, process.env.DISCORD_BACKUP_GUILD_ID].filter(Boolean)
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const result = await registerSlashCommands(
{
commands: manifest.commands,
messageContext: manifest.context.message,
userContext: manifest.context.user
},
{
guildIds: TEST_GUILDS
}
)
logger.info('Registered to guilds:', TEST_GUILDS)
logger.info('Registration result:', result)
logger.info(`Registered count: ${result.registered}`)
await logger.flush()
}
main().catch(async (err) => {
logger.error(err)
await logger.flush()
process.exit(1)
})
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const isAdmin = (entry) => entry.meta?.roles?.includes?.('admin')
const adminCommands = Object.fromEntries(
Object.entries(manifest.commands).filter(([_, entry]) => isAdmin(entry))
)
const publicCommands = Object.fromEntries(
Object.entries(manifest.commands).filter(([_, entry]) => !isAdmin(entry))
)
// 1) Register admin-only commands to a test guild
await registerSlashCommands({ commands: adminCommands }, { guildIds: [process.env.DISCORD_TEST_GUILD_ID] })
// 2) Register all public commands globally
await registerSlashCommands({ commands: publicCommands }, {})
logger.info('Admin/public registration completed')
await logger.flush()
}
main().catch(async (err) => {
logger.error(err)
await logger.flush()
process.exit(1)
})
import 'dotenv/config'
import type { RegisterCommandsResult } from 'robo.js'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const result: RegisterCommandsResult = await registerSlashCommands(
{
commands: manifest.commands,
messageContext: manifest.context.message,
userContext: manifest.context.user
},
{}
)
logger.info('Registration result:', result)
if (!result.success) {
for (const err of result.errors) {
logger.warn(`[${err.type}] ${err.command}: ${err.error}`)
}
await logger.flush()
process.exit(1)
}
await logger.flush()
}
main()
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const result = await registerSlashCommands(
{ commands: manifest.commands, messageContext: manifest.context.message, userContext: manifest.context.user },
{ guildIds: ['123456789012345678', '234567890123456789'] }
)
logger.info('Registration result:', result)
}
main()
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function main() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const startsWith = (prefix: string) => (name: string) => name.startsWith(prefix)
const pick = (pred: (k: string) => boolean) => (obj: Record<string, any>) =>
Object.fromEntries(Object.entries(obj).filter(([k]) => pred(k)))
const admin = pick(startsWith('admin.'))(manifest.commands)
const publicCommands = pick((k) => !k.startsWith('admin.'))(manifest.commands)
await registerSlashCommands({ commands: admin }, { guildIds: ['123456789012345678'] })
await registerSlashCommands({ commands: publicCommands }, {})
logger.info('Admin/public registration completed')
await logger.flush()
}
main()
Lifecycle Integration
You can call registerSlashCommands() inside the Robo lifecycle to keep your flow automated.
Using the _start Event
The _start event runs before the bot logs in, making it ideal for setup like command registration. Keep in mind lifecycle events have a soft 5-second timeout, so keep work concise or spawn background tasks if needed.
- JavaScript
- TypeScript
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
export default async function _start() {
try {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.warn('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
return
}
const testGuilds = process.env.DISCORD_TEST_GUILD_ID ? [process.env.DISCORD_TEST_GUILD_ID] : undefined
const res = await registerSlashCommands(
{ commands: manifest.commands, messageContext: manifest.context.message, userContext: manifest.context.user },
{ guildIds: testGuilds }
)
logger.info('Registration result:', res)
logger.info(`Slash commands registered: ${res.registered}`)
if (!res.success) {
logger.warn('Some commands failed to register', { errors: res.errors })
}
} catch (err) {
logger.error('Command registration failed', { err })
}
}
import type { RegisterCommandsResult } from 'robo.js'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
export default async function _start(): Promise<void> {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.warn('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
return
}
const result: RegisterCommandsResult = await registerSlashCommands(
{
commands: manifest.commands,
messageContext: manifest.context.message,
userContext: manifest.context.user
},
{
guildIds: process.env.DISCORD_TEST_GUILD_ID ? [process.env.DISCORD_TEST_GUILD_ID] : undefined
}
)
logger.info('Registration result:', result)
logger.info(`Registered ${result.registered} commands`)
if (!result.success) {
logger.warn('Some commands failed', { errors: result.errors })
}
}
Timing Considerations
- The manifest must be built before calling the API.
Compiler.useManifest()reads from the compiled manifest file produced byrobo build/robo dev. - Commands, context menus, and related metadata are indexed at build time.
- For standalone scripts, ensure environment variables are loaded (e.g., via
dotenv) and that a build has produced.robo/manifest.jsonbefore callingCompiler.useManifest().
Disabling Auto-Registration
There are two ways to turn off automatic registration if you want full manual control.
Using Config
Add autoRegisterCommands: false to your Robo config.
export default {
// ...other options
autoRegisterCommands: false
}
Using CLI Flag
Skip registration for a single build using the CLI flag.
npx robo build --no-registerThis is useful for temporary tests or one-off deployments without changing config. The CLI flag takes precedence over the config option; when neither is specified, auto-registration is enabled by default.
Standalone Scripts
You can also run registration as a standalone script outside the lifecycle. This is perfect for CI/CD.
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function run() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
const result = await registerSlashCommands(
{
commands: manifest.commands,
messageContext: manifest.context.message,
userContext: manifest.context.user
},
{
guildIds: process.argv.includes('--test') && process.env.DISCORD_TEST_GUILD_ID
? [process.env.DISCORD_TEST_GUILD_ID]
: undefined,
force: process.argv.includes('--force')
}
)
logger.info('Registration result:', result)
await logger.flush()
process.exit(result.success ? 0 : 1)
}
run().catch(async (e) => {
logger.error(e)
await logger.flush()
process.exit(1)
})
Run with:
npx node scripts/register-commands.js --testExample: Test Guild First, Then Global
import 'dotenv/config'
import { registerSlashCommands, logger } from 'robo.js'
import { Compiler } from 'robo.js/unstable.js'
async function stagedRegister() {
const manifest = await Compiler.useManifest()
const hasEntries =
Object.keys(manifest?.commands ?? {}).length > 0 ||
Object.keys(manifest?.context?.message ?? {}).length > 0 ||
Object.keys(manifest?.context?.user ?? {}).length > 0
if (!hasEntries) {
logger.error('Manifest appears empty. Run `robo build` or `robo dev` to generate .robo/manifest.json')
await logger.flush()
process.exit(1)
}
// 1) Test guild
const testGuild = process.env.DISCORD_TEST_GUILD_ID
if (testGuild) {
const res = await registerSlashCommands({ commands: manifest.commands }, { guildIds: [testGuild] })
logger.info('Test guild registration result:', res)
logger.info(`Test guild registered: ${res.registered}`)
if (!res.success) {
throw new Error('Test guild registration failed')
}
}
// 2) Global
const res = await registerSlashCommands(
{ commands: manifest.commands, messageContext: manifest.context.message, userContext: manifest.context.user },
{
force: true
}
)
logger.info('Global registration result:', res)
logger.info(`Global registered: ${res.registered}`)
}
stagedRegister().catch(async (e) => {
logger.error(e)
await logger.flush()
process.exit(1)
})
Error Handling
Errors are returned in a structured format so you can react programmatically.
import type { RegisterCommandsError } from 'robo.js'
import { logger } from 'robo.js'
function handleErrors(errors: RegisterCommandsError[]) {
for (const err of errors) {
switch (err.type) {
case 'validation': {
logger.error(`Validation failed for ${err.command}: ${err.error}`)
break
}
case 'api': {
logger.error(`Discord API error for ${err.command}: ${err.error}`)
break
}
case 'timeout': {
logger.error(`Timeout registering ${err.command}: ${err.error}`)
break
}
}
}
}
Rate Limiting
Discord enforces rate limits on registration. The API automatically retries with exponential backoff. You can inspect the retries array in the result for details like scope, attempt number, reason, and delay.
import type { RegisterCommandsRetry } from 'robo.js'
import { logger } from 'robo.js'
function logRetries(retries?: RegisterCommandsRetry[]) {
for (const r of retries ?? []) {
logger.info(`[retry] scope=${r.scope} attempt=${r.attempt} delay=${r.delay} reason=${r.reason}`)
}
}
Migration Guide
For Projects Using Auto-Registration
If auto-registration works for you, keep it! To gradually adopt manual registration:
- Set
autoRegisterCommands: falsein your config. - Add an
_startevent handler that callsregisterSlashCommands(). - Test with
robo devon a test guild. - Deploy to production when ready.
For Projects With Custom Registration
Replace ad-hoc Discord.js REST calls with registerSlashCommands() for built-in validation, rate-limit handling, and clear results.
Best Practices
- Always check
result.successand logresult.errors. - Prefer guild-specific registration for testing before global.
- Use
forcesparingly; it fully refreshes registrations. - Keep registration in lifecycle events for automation.
- Use standalone scripts for CI/CD and staged rollouts.
- Test subsets in a test guild before promoting globally.
Troubleshooting
- Commands not appearing: Try refreshing Discord or relaunching the client.
- Validation errors: Review command structure and required fields.
- Authentication errors: Verify
DISCORD_CLIENT_IDandDISCORD_TOKEN. - Timeout errors: Check your network and Discord API status.
- Rate limits: Observe retry delays; avoid frequent re-registration.