Commands and Reactions
Slash commands and react-to-any-message handlers at the channel level.
Two channel-level shortcuts that are not tied to a message you rendered: slash commands the user types, and reactions on any message in the channel.
Slash commands#
A command runs when the user types /name. Define it with
defineChannelCommand and wire it into the channel.
import { defineChannelCommand } from "@copilotkit/channels";
export const commands = [
defineChannelCommand({
name: "ask", // no leading slash, matched case-insensitively
description: "Ask the agent anything, no mention needed.",
async handler({ thread, text, user }) {
if (!text) {
await thread.post("Usage: `/ask <your question>`");
return;
}
await thread.runAgent({ prompt: text });
},
}),
];import { createChannel } from "@copilotkit/channels";
import { agent } from "./agent";
import { commands } from "./commands";
const channel = createChannel({
name: "support-bot",
agent,
commands,
});The user's words after the command arrive as text:
/ask how do I reset my password
-> text = "how do I reset my password"Slack needs the command declared too
On Slack, register the same command in your Slack app manifest so Slack sends
it to your bot. The name in the manifest must match the name here.
Typed arguments#
Platforms with native structured arguments (Discord) can parse args for you.
Add an options schema, and read options in the handler:
import { z } from "zod";
defineChannelCommand({
name: "weather",
description: "Get the weather for a city.",
options: z.object({ city: z.string() }),
async handler({ thread, options }) {
await thread.runAgent({ prompt: `What's the weather in ${options.city}?` });
},
});On text-only surfaces options is empty and the raw string is in text, so
handle both if a command targets several platforms.
The command handler also has thread.postEphemeral(...) for a reply only the
caller sees, and openModal(...) on platforms that support dialogs.
Global reactions#
channel.onReaction fires when a user reacts to any message in the channel,
not just a card you rendered. Use it for channel-wide shortcuts, like a
refresh emoji that re-runs the last request.
channel.onReaction(async ({ added, emoji, thread, user }) => {
if (!added) return; // ignore un-reactions
if (emoji !== "refresh") return; // 🔄
await thread.post(`On it, ${user?.name ?? "there"}.`);
});Scope it to one emoji so you skip the check:
channel.onReaction("fire", async ({ thread }) => {
await thread.post("🔥");
});The event carries emoji (the canonical name), rawEmoji (the platform
token), added, user, thread, and messageRef for
thread.update(messageRef, ...).
Want to handle a reaction on one specific card instead of the whole channel?
Use the per-message <Message onReaction>
handler.