Your first command
Building a warn command
Section titled “Building a warn command”Every moderation bot needs a way to warn users. Let’s build one. Start by generating a new command:
pnpm warden make:command WarnCommandOpen the generated file and fill it in:
import {command, Command, Options, CommandInteraction, Params, reply, Embed} from '@warden/core';
@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), ];
public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const reason = interaction.options.getString('reason')!;
await reply(interaction, Embed.success(`${target.username} has been warned.`) .addFields( {name: 'Reason', value: reason}, {name: 'Moderator', value: interaction.user.username}, ) .setTimestamp() ); }}Save the file, wait for the dev server to restart, and try /warn @someone Being disruptive in Discord. You should see a nicely formatted embed confirming the warning.
Defining options
Section titled “Defining options”The Options builder provides a fluent, type-safe way to define what input your command accepts. You’ve already seen Options.user() and Options.string() — here’s the full picture:
Options.string('reason', 'Reason for the action') // text inputOptions.integer('duration', 'Duration in minutes') // whole numberOptions.number('score', 'Trust score') // decimal numberOptions.boolean('silent', 'Perform silently') // true/falseOptions.user('target', 'Target user') // user pickerOptions.channel('channel', 'Log channel') // channel pickerOptions.role('role', 'Mute role') // role pickerOptions.mentionable('who', 'User or role') // user or roleOptions.attachment('evidence', 'Screenshot') // file uploadEach option type supports relevant modifiers that you can chain fluently:
Options.string('reason', 'Reason') .required() // must be provided .minLength(5) // at least 5 characters .maxLength(500) // at most 500 characters
Options.integer('duration', 'Minutes') .required() .min(1) // at least 1 .max(1440) // at most 1440 (24 hours)
Options.string('severity', 'Severity level') .choices([ // present fixed choices {name: 'Low', value: 'low'}, {name: 'Medium', value: 'medium'}, {name: 'High', value: 'high'}, ])Working with embeds
Section titled “Working with embeds”Plain text replies work fine, but embeds give your moderation actions a professional look. Warden ships with preset embeds for the most common scenarios:
import {Embed} from '@warden/core';
Embed.success('User has been warned.') // greenEmbed.error('You lack permission.') // redEmbed.info('Showing case #42.') // blueEmbed.warning('Are you sure?') // yellowThese are just convenience wrappers around discord.js’s EmbedBuilder, so you can chain any method you’d normally use:
await reply(interaction, Embed.success(`Warned ${target.username}`) .setTitle('Moderation Action') .addFields( {name: 'Reason', value: reason}, {name: 'Case', value: `#${caseNumber}`}, ) .setFooter({text: `Moderator: ${interaction.user.username}`}) .setTimestamp());The reply helper
Section titled “The reply helper”You may have noticed we’ve been using reply() instead of calling interaction.reply() directly. This is a small but important convenience — the reply() helper automatically picks the right method based on the interaction’s current state:
import {reply} from '@warden/core';
await reply(interaction, 'Done!');await reply(interaction, {content: 'Only you can see this', ephemeral: true});await reply(interaction, Embed.success('Warning issued.'));If the interaction hasn’t been replied to yet, it calls reply(). If it’s been deferred, it calls editReply(). If it’s already been replied to, it calls followUp(). This means you never have to think about the interaction’s reply state — and you’ll never see “interaction has already been replied to” errors again.
Confirmation dialogs
Section titled “Confirmation dialogs”For destructive actions like banning a user, it’s good practice to ask for confirmation first. The confirm() helper makes this a one-liner:
import {confirm} from '@warden/core';
@command({name: 'ban', description: 'Ban a user'})export default class BanCommand implements Command { public options = [ Options.user('user', 'User to ban').required(), Options.string('reason', 'Reason').required(), ];
public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const reason = interaction.options.getString('reason')!;
const confirmed = await confirm(interaction, `Are you sure you want to ban ${target.username}?`);
if (confirmed) { await interaction.guild?.members.ban(target, {reason}); await reply(interaction, Embed.success(`${target.username} has been banned.`)); } }}Under the hood, confirm() sends a warning embed with Confirm and Cancel buttons, waits for the user’s response, cleans up the buttons, and returns a simple boolean. The timeout defaults to 30 seconds.
Requiring permissions
Section titled “Requiring permissions”Moderation commands should obviously be restricted to users with the right permissions. Warden makes this declarative with the @permissions() decorator:
import {command, permissions} from '@warden/core';
@command({name: 'ban', description: 'Ban a user'})@permissions({ user: ['BanMembers'], // the user running the command needs this bot: ['BanMembers'], // the bot needs this too})export default class BanCommand implements Command { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { // this only runs if both the user AND the bot have BanMembers }}If either check fails, the framework replies with a clear error message and execute() is never called. You don’t need any permission checking logic in your command.
Rate limiting with cooldowns
Section titled “Rate limiting with cooldowns”To prevent users from spamming moderation commands, you can add a cooldown with the @cooldown() decorator:
import {command, cooldown} from '@warden/core';
@command({name: 'warn', description: 'Warn a user'})@cooldown({duration: 5, scope: 'user'})export default class WarnCommand implements Command { // can only be used once every 5 seconds, per user}The available scopes are user (per user), channel (per channel), guild (per server), and global (everyone). You may also customize the message shown when the cooldown is active:
@cooldown({ duration: 5, scope: 'user', message: 'Easy there! You can warn again in {remaining}s.',})Grouping commands with subcommands
Section titled “Grouping commands with subcommands”As your mod bot grows, you’ll probably want to group related commands together: /mod warn, /mod mute, /mod ban, /mod history. Warden supports this through the entrypoint pattern.
First, create the parent command. This class defines the subcommand structure but has no execute() method itself:
import {command, Options, CommandGroup} from '@warden/core';
@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 moderation history', [ Options.user('user', 'User to look up'), ]), ];}Then, each subcommand gets 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 calls deferReply() before routing, but the reply() helper handles this transparently — you don’t need to think about it.
Discord also supports an extra level of nesting through subcommand groups (/mod config set-log-channel). The Commands reference covers the full pattern, plus a couple of edge cases like opting out of the auto-deferral when your subcommand needs to show a modal.
Context menu commands
Section titled “Context menu commands”Sometimes it’s more natural to right-click a user than to type a command. Context menu commands let you add actions to Discord’s right-click menu:
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 context menus to messages — great for a “Flag Message” action:
@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.')); }}Autocomplete
Section titled “Autocomplete”For commands where users need to search through existing data — like looking up past moderation cases — you can enable autocomplete. This shows suggestions as the user types:
@command({parent: 'mod', name: 'history', description: 'View moderation 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... }}Just mark the option with .autocomplete() and add an autocomplete() method to your command. 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.
You’ve stacked @command, @permissions, and @cooldown on the same class throughout this guide. That’s the design — each decorator owns one concern, and the order you stack them in doesn’t matter. The shape of a command’s responsibilities is visible at a glance, right above the class name. The Commands reference walks through every built-in decorator and what it gives you.
Next steps
Section titled “Next steps”You’ve now built a command that accepts input, shows rich embeds, asks for confirmation, requires permissions, has rate limiting, supports subcommands, and provides autocomplete. That’s a lot of ground covered!
Next, let’s make your bot reactive:
- Your First Event — log member joins, detect auto-mod violations, and react to moderation actions
- Project Structure — organize your growing mod bot