Skip to content

Commands

Commands are the primary way users interact with your bot. When someone types /warn or /mod ban, Warden routes the interaction to the right class, runs any middleware and permission checks, and calls your execute() method.

This page is the reference. If you’d rather build a real command first and read the details later, start with Your first command.

At its simplest, a command is a class with a @command() decorator:

import {command, Command, CommandInteraction, Params, reply} from '@warden/core';
@command({name: 'ping', description: 'Ping!'})
export default class PingCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const latency = Math.abs(Date.now() - interaction.createdTimestamp);
await reply(interaction, `Pong! (${latency}ms)`);
}
}

Save the file, the bot restarts, and /ping is available in Discord. There are no registration files to update — Warden discovers decorated classes automatically via scan().

The @command() decorator accepts the following parameters:

ParameterTypeRequiredDescription
namestringYesThe slash command name (lowercase, no spaces)
descriptionstringYesShown in Discord’s command picker
parentstringNoName of the parent entrypoint, for subcommands
middlewareMiddleware[]NoTyped middleware to run before execute()

The execute() method receives two arguments: the discord.js interaction, and a Params that may be populated by middleware or plugins. If nothing populates it, it’s simply empty.

Most commands need input from the user. You can define options using the Options builder, which provides a fluent, type-safe API:

@command({name: 'warn', description: 'Warn a user'})
export default class WarnCommand implements Command {
public options = [
Options.user('user', 'User to warn').required(),
Options.string('reason', 'Reason for the warning').required().maxLength(500),
Options.string('severity', 'Severity level').choices([
{name: 'Low', value: 'low'},
{name: 'Medium', value: 'medium'},
{name: 'High', value: 'high'},
]),
];
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const target = interaction.options.getUser('user')!;
const reason = interaction.options.getString('reason')!;
const severity = interaction.options.getString('severity') ?? 'low';
await reply(interaction, Embed.success(`Warned ${target.username} (${severity})`)
.addFields({name: 'Reason', value: reason})
.setTimestamp()
);
}
}

The full list of available option types:

Options.string('name', 'description') // text input
Options.integer('name', 'description') // whole number
Options.number('name', 'description') // decimal number
Options.boolean('name', 'description') // true/false toggle
Options.user('name', 'description') // user picker
Options.channel('name', 'description') // channel picker
Options.role('name', 'description') // role picker
Options.mentionable('name', 'description') // user or role
Options.attachment('name', 'description') // file upload

Each type supports relevant modifiers that you may chain fluently:

Options.string('reason', 'Reason').required().minLength(5).maxLength(500)
Options.integer('duration', 'Minutes').required().min(1).max(1440)
Options.string('severity', 'Level').choices([...])
Options.string('query', 'Search').autocomplete()

As your bot grows, you’ll likely want to group related commands under one name — /mod warn, /mod mute, /mod ban, /mod history. Warden supports this through the entrypoint pattern.

The parent command extends CommandGroup and defines the subcommand structure. It has no execute() method — its only job is to declare the options and route to the right subcommand:

@command({name: 'mod', description: 'Moderation commands'})
export default class ModCommand extends CommandGroup {
public options = [
Options.subcommand('warn', 'Warn a user', [
Options.user('user', 'User to warn').required(),
Options.string('reason', 'Reason').required().maxLength(500),
]),
Options.subcommand('mute', 'Mute a user', [
Options.user('user', 'User to mute').required(),
Options.integer('duration', 'Duration in minutes').required().min(1).max(1440),
Options.string('reason', 'Reason').required(),
]),
Options.subcommand('ban', 'Ban a user', [
Options.user('user', 'User to ban').required(),
Options.string('reason', 'Reason').required(),
]),
Options.subcommand('history', 'View mod history', [
Options.user('user', 'User to look up'),
]),
];
}

Each subcommand is then its own class. The parent field in the decorator tells Warden which entrypoint it belongs to — the wiring happens automatically at boot:

@command({parent: 'mod', name: 'warn', description: 'Warn a user'})
@permissions({user: ['ModerateMembers']})
export default class ModWarnCommand extends Subcommand {
constructor(private caseService: CaseService) {}
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const target = interaction.options.getUser('user')!;
const reason = interaction.options.getString('reason')!;
const caseNumber = await this.caseService.createWarning(
interaction.guildId!, target.id, reason, interaction.user.id,
);
await reply(interaction, Embed.success(`Warned ${target.username}`)
.addFields(
{name: 'Reason', value: reason},
{name: 'Case', value: `#${caseNumber}`},
)
.setTimestamp()
);
}
}

The main difference from regular commands is that subcommands extend Subcommand instead of implementing Command. The entrypoint automatically calls deferReply() before routing, which buys the subcommand time for async work — the reply() helper detects this and uses editReply() under the hood.

Discord also supports an extra level of nesting through subcommand groups. For example, you might want /mod config set-log-channel and /mod config set-mute-role grouped separately from the main moderation commands:

@command({name: 'mod', description: 'Moderation commands'})
export default class ModCommand extends CommandGroup {
public options = [
Options.subcommand('warn', 'Warn a user', [/* ... */]),
Options.subcommand('ban', 'Ban a user', [/* ... */]),
Options.subcommandGroup('config', 'Moderation settings', [
Options.subcommand('set-log-channel', 'Set the mod log channel', [
Options.channel('channel', 'Channel for mod logs').required(),
]),
Options.subcommand('set-mute-role', 'Set the mute role', [
Options.role('role', 'Role to assign when muting').required(),
]),
]),
];
}

Sometimes it’s more natural to right-click a user or message than to type a command. Warden supports Discord’s context menu commands with the @context() decorator:

import {context, ContextMenu, Params} from '@warden/core';
import {UserContextMenuCommandInteraction} from 'discord.js';
@context({name: 'Warn User', type: 'user'})
@permissions({user: ['ModerateMembers']})
export default class WarnUserContext implements ContextMenu {
public async execute(interaction: UserContextMenuCommandInteraction, params: Params): Promise<void> {
const target = interaction.targetUser;
// you might show a modal here asking for the reason
}
}

You can also add actions to messages — a “Flag Message” context menu is particularly useful:

@context({name: 'Flag Message', type: 'message'})
@permissions({user: ['ModerateMembers']})
export default class FlagMessageContext implements ContextMenu {
constructor(private caseService: CaseService) {}
public async execute(interaction: MessageContextMenuCommandInteraction, params: Params): Promise<void> {
const message = interaction.targetMessage;
await this.caseService.createFlag(message);
await reply(interaction, Embed.success('Message flagged for review.'));
}
}

Context menu commands support the same middleware, permissions, cooldowns, and audit logging as slash commands.

For commands where the user needs to search through existing data, you can enable autocomplete to show suggestions as they type. This is especially useful for looking up past moderation cases:

@command({parent: 'mod', name: 'history', description: 'View mod history'})
export default class ModHistoryCommand extends Subcommand {
public options = [
Options.string('query', 'Search cases').autocomplete(),
];
constructor(private caseService: CaseService) {}
public async autocomplete(interaction: AutocompleteInteraction): Promise<void> {
const query = interaction.options.getFocused();
const cases = await this.caseService.search(query);
await interaction.respond(
cases.map(c => ({
name: `#${c.id} - ${c.type} - ${c.targetUsername}`,
value: String(c.id),
})),
);
}
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const caseId = interaction.options.getString('query');
// show case details...
}
}

To enable autocomplete, mark the option with .autocomplete() and add an autocomplete() method to your command class. The framework handles routing the autocomplete interaction to the right method. If you forget to add the method, Warden will log a helpful warning at boot.

When a command has multiple autocomplete options, you can define a named method per option instead of branching manually — autocompleteUser(value), autocompleteReason(value), etc. See the Autocomplete guide for details.

All commands support constructor injection. You may inject any registered service — Warden automatically handles the resolution:

@command({name: 'mod-stats', description: 'Moderation statistics'})
export default class ModStatsCommand implements Command {
constructor(
private caseService: CaseService,
private logger: Logger,
private config: Config,
) {}
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const stats = await this.caseService.getStats(interaction.guildId!);
this.logger.info('Stats requested', {guild: interaction.guildId});
// ...
}
}

There’s no need to add @injectable() — the @command() decorator registers the class for dependency injection automatically.

You may have noticed that permissions, cooldowns, and other cross-cutting concerns each have their own decorator. This is by design — each decorator handles exactly one thing, and you compose them by stacking:

@command({name: 'ban', description: 'Ban a user'})
@permissions({user: ['BanMembers'], bot: ['BanMembers']})
@cooldown({duration: 10, scope: 'user'})
@audit({action: 'moderation.ban', trackTarget: 'user'})
export default class BanCommand implements Command { ... }

This makes it immediately clear what a command requires just by looking at the class declaration. For a deeper look at each of these decorators, see the Middleware, Configuration, and plugin documentation.