Message commands
While slash commands are the modern Discord standard, Knub still supports classic prefix-based message commands. This
page explains how to declare message command blueprints, customise their behaviour, and take advantage of the helpers in
PluginMessageCommandManager.
Declaring message commands
Message command blueprints are created with guildPluginMessageCommand() or globalPluginMessageCommand() and attached
to a plugin blueprint via the messageCommands array.
import { guildPlugin, guildPluginMessageCommand } from "knub";
import z from "zod";
const pingCommand = guildPluginMessageCommand({
trigger: "ping",
permission: null,
run: async ({ message }) => {
await message.reply("Pong!");
},
});
export const pingPlugin = guildPlugin({
name: "ping",
configSchema: z.strictObject({}),
messageCommands: [pingCommand],
});
The minimal blueprint specifies a trigger (string or array of triggers), a permission name, and a run handler. The
handler receives a MessageCommandMeta object with parsed arguments, the triggering message, the command definition, and
pluginData.
Signatures and argument parsing
Message commands accept optional argument signatures from knub-command-manager.
You can describe positional parameters, options, or rest arguments, and Knub will parse them before invoking your
handler.
const sayCommand = guildPluginMessageCommand({
trigger: "say",
permission: "can.say",
signature: {
channel: channel(),
text: rest({ required: true }),
},
run: async ({ args, pluginData }) => {
const config = pluginData.config.get();
await args.channel.send({ content: args.text, allowedMentions: { parse: [] } });
},
});
See the messageCommandUtils
module for ready-made converters and helpers.
Permissions and filters
Knub automatically runs a pre-filter pipeline before executing the command:
restrictCommandSource– Respects the blueprint’ssourceoption ("guild","dm", or both).checkCommandPermission– Resolves thepermissionstring using the plugin configuration.- Custom
preFiltersdefined on the blueprint.
Similarly, post-filters enforce cooldowns and locks after the command runs. You can append additional filters through the
blueprints config.preFilters/config.postFilters` arrays.
Deleted commands
The optional deletedMessageCommands array on a plugin blueprint lets you remove legacy triggers from the command
manager when the plugin loads. This is useful when you rename commands or consolidate aliases without leaving stale
entries behind.
export const moderationPlugin = guildPlugin({
name: "moderation",
configSchema: z.strictObject({}),
messageCommands: [kickCommand, banCommand],
deletedMessageCommands: ["warn", "softban"],
});
When the plugin loads, any existing message commands with triggers warn or softban are removed before new commands
are registered. The removal emits a lifecycle event (see below), which enables analytics or migration logging.
Command lifecycle events
PluginMessageCommandManager emits notifications whenever commands are added or removed. Subscribe to these events from
within your plugin:
pluginData.messageCommands.onCommandAdded(({ command }) => {
console.log(`[${pluginData.pluginName}] registered`, command.originalTriggers[0]);
});
pluginData.messageCommands.onCommandDeleted(({ command, reason }) => {
console.log(`[${pluginData.pluginName}] removed`, command.originalTriggers[0], reason);
});
Removal reasons are one of:
"manual"– Removed viaremove(id)."deleted"– Removed throughremoveByTrigger()or a matching entry indeletedMessageCommands."replaced"– A new blueprint replaced an existing command with the same trigger.
These hooks are a convenient place to keep metrics up to date or synchronise external registries.
Manual command dispatch
Knub automatically attaches a messageCreate listener for each plugin that registers message commands. Sometimes you
need more control – for example, when implementing aliases or rewriting the command content before execution.
Use knub.dispatchMessageCommands(message) to run the standard dispatch pipeline manually. The helper ensures the
message is processed only once by marking it internally. Subsequent calls (including the default event listener) do
nothing.
client.on("messageCreate", async (message) => {
if (message.author.bot) return;
if (message.content.startsWith("!alias")) {
const rewrittenContent = message.content.replace("!alias", "!realcommand");
const rewrittenMessage = Object.create(message, {
content: { value: rewrittenContent },
});
await knub.dispatchMessageCommands(rewrittenMessage);
return;
}
await knub.dispatchMessageCommands(message);
});
Important: Always call
dispatchMessageCommandsinstead of invoking a plugin’smessageCommands.runFromMessagedirectly. The dispatcher handles global plugins, dependency-only plugins, and the “already processed” guard for you.
If you need to know whether dispatch already happened, use hasMessageCommandBeenDispatched(message) from
messageCommandUtils. This is the same check Knub performs internally.
Advanced manager helpers
PluginMessageCommandManager also exposes a few utility methods:
getAll()– Inspect currently registered command definitions.removeByTrigger(trigger)– Remove the first command matching the trigger string. Returnstruewhen a command was removed.remove(id, reason?)– Remove a command by ID. Generally you should call this only from within your plugin.onCommandAdded(listener)/onCommandDeleted(listener)– Subscribe to lifecycle events. They return an unsubscribe function.
You rarely construct the manager yourself – Knub injects it into pluginData – but these helpers give you the tools to
implement migrations, analytics, and other custom behaviours without digging into internals.
With these tools you can keep legacy message commands running smoothly, gradually migrate to slash commands, or build hybrid experiences that combine both.