Skip to content

Dynamic Commands

SlashCommandHandler and ContextCommandHandler require a fixed name set at load time. That works for commands your bot code defines up front, but not for commands whose name is only known at runtime — for example, per-guild custom commands stored in a database, where the application command is registered with Discord but Honocord has no matching named handler for it.

Honocord’s dynamic handlers catch exactly this case. They’re fallback handlers with no name of their own — instead of being looked up by name, one is invoked whenever an incoming application command interaction doesn’t match any registered SlashCommandHandler or ContextCommandHandler.

For every application command interaction, Honocord first checks the normal handler registry (global commands, then guild-specific commands by name). Only if nothing matches does it fall back to a dynamic handler.

Which dynamic handler runs depends on the interaction’s scope:

  • Invoked in a guild (guild_id present): the guild-scoped dynamic handler is tried first. If none is registered, Honocord falls back to the global-scoped one.
  • Invoked in a DM (no guild_id): only the global-scoped dynamic handler is ever tried.

This mirrors the fact that Discord’s interaction payload doesn’t tell you whether a command was originally registered as guild- or global-scoped — only where it was invoked.

Class Catches
DynamicGuildSlashCommandHandler Unmatched chat input commands invoked in a guild
DynamicGlobalSlashCommandHandler Unmatched chat input commands invoked in a DM, or as fallback
DynamicGuildContextCommandHandler Unmatched User/Message context commands invoked in a guild
DynamicGlobalContextCommandHandler Unmatched User/Message context commands invoked in a DM, or as fallback

Unlike SlashCommandHandler/ContextCommandHandler, these classes don’t extend a Discord builder — there’s no .setName() or .setDescription(), since the command is expected to be registered with Discord elsewhere (e.g. by your own admin flow that creates per-guild custom commands via the REST API directly).

import { DynamicGuildSlashCommandHandler } from "honocord";
const customCommand = new DynamicGuildSlashCommandHandler().addHandler(async (ctx) => {
// ctx.commandName is the name Discord sent, unknown to Honocord ahead of time
const definition = await db.customCommands.findOne({ guildId: ctx.guildId, name: ctx.commandName });
if (!definition) {
// reply() is ephemeral by default
await ctx.reply("This command is no longer configured.");
return;
}
await ctx.reply(definition.response);
});

Dynamic slash handlers support autocomplete the same way SlashCommandHandler does:

customCommand.addAutocompleteHandler(async (ctx) => {
const focused = ctx.options.getFocused();
const results = await db.customCommandOptions.search(ctx.commandName, focused.value);
await ctx.respond(results.map((r) => ({ name: r, value: r })));
});

A single dynamic context handler receives both User and Message context interactions — narrow with isOfType:

import { DynamicGuildContextCommandHandler } from "honocord";
import { ApplicationCommandType } from "discord-api-types/v10";
const customContextCommand = new DynamicGuildContextCommandHandler().addHandler(async (ctx) => {
if (ctx.isOfType(ApplicationCommandType.User)) {
await ctx.reply(`Ran on user ${ctx.targetUser.username}`);
} else if (ctx.isOfType(ApplicationCommandType.Message)) {
await ctx.reply(`Ran on message ${ctx.targetMessage.id}`);
}
});

Register a global dynamic handler to catch anything the guild-scoped one (or a DM invocation) doesn’t:

import { DynamicGlobalSlashCommandHandler } from "honocord";
const globalFallback = new DynamicGlobalSlashCommandHandler().addHandler(async (ctx) => {
console.warn(`Unrecognized command "${ctx.commandName}"`);
await ctx.reply("This command isn't available right now.");
});

Load dynamic handlers like any other handler:

bot.loadHandlers(customCommand, customContextCommand, globalFallback);