showcase/shell-docs/src/content/docs/channels/commands-and-reactions.mdx
Commands give users a deliberate entry point into a workflow. Reaction handlers let an agent respond to lightweight feedback without treating every emoji as a new chat message.
defineChannelCommand normalizes the command name by removing a leading slash
and lowercasing it. Slack and Teams currently deliver command arguments as one
free-text string, so read ctx.text.
import { defineChannelCommand } from "@copilotkit/channels";
export const triage = defineChannelCommand({
name: "triage",
description: "Summarize the current conversation and suggest an owner.",
async handler({ thread, text, user, platform }) {
await thread.runAgent({
prompt: text
? `Triage this conversation with this instruction: ${text}`
: "Triage this conversation.",
context: [
{ description: "Command provider", value: platform },
{
description: "Invoking user",
value: user?.name ?? user?.id ?? "unknown",
},
],
});
},
});
Register it at Channel creation:
<FrontendOnly frontend="slack">const channel = createChannel({
name: required("CHANNEL_CODE"),
provider: "slack",
agent: makeAgent,
commands: [triage],
});
const channel = createChannel({
name: required("CHANNEL_CODE"),
provider: "teams",
agent: makeAgent,
commands: [triage],
});
You can also add a command before the listener starts with
channel.onCommand(command). Unregistered command names are ignored.
The Intelligence-generated Slack manifest creates one slash command derived
from the Channel Display name. The handler name must match the command shown in
the Slack setup recap. For example, a generated /support-agent command needs:
export const supportAgent = defineChannelCommand({
name: "support-agent",
async handler({ thread, text }) {
await thread.runAgent({ prompt: text || "How can you help?" });
},
});
If you add more command names in code, add the same slash commands to the Slack app configuration and reinstall the app when Slack changes its requested permissions. Slack sends the invocation to the signed managed command webhook; your Channels runner does not host that request URL.
Slack supplies arguments as ctx.text. ctx.options remains empty on this
managed path.
Teams has no separate slash-command registration in this integration. An
addressed message whose text is exactly /name or /name arguments is routed
to the matching command handler:
/triage focus on customer impact
The optional bot mention is stripped before command matching. Arguments arrive
as ctx.text; ctx.options remains empty.
Managed command contexts do not expose modal opening. Use a posted card and a later interaction for structured follow-up.
Known Slack shortcodes and Teams reaction tokens are normalized to portable
names such as eyes, thumbs_up, and heart.
channel.onReaction("eyes", async ({ added, thread, user }) => {
await thread.post(
added
? `${user?.name ?? "Someone"} marked this response for follow-up.`
: `${user?.name ?? "Someone"} removed the follow-up marker.`,
);
});
Omit the emoji argument to receive every added and removed reaction:
channel.onReaction(async ({ emoji, rawEmoji, added, thread }) => {
audit.record({ emoji, rawEmoji, added });
await thread.post("Reaction recorded.");
});
emoji is the normalized value when the SDK recognizes it. rawEmoji retains
the provider token for custom or unmapped emoji.
The generated Slack manifest subscribes to reaction_added and
reaction_removed. Managed ingress routes reactions to messages authored by
the configured bot; reactions on unrelated workspace messages are ignored.
Teams messageReaction activities are normalized through the same handler.
Classic reactions such as Like and Heart, plus recognized modern emoji tokens,
map to the SDK's portable names.
Use Message.onReaction when only one posted component should react:
import { Message, Section } from "@copilotkit/channels/ui";
export function FeedbackCard({ answerId }: { answerId: string }) {
return (
<Message
onReaction={async (emoji, reaction) => {
if (reaction.added && emoji === "thumbs_up") {
await feedback.record(answerId, reaction.user?.id);
await reaction.thread.post("Thanks for the feedback.");
return;
}
await reaction.thread.post("Reaction update received.");
}}
>
<Section>React with 👍 if this solved the problem.</Section>
</Message>
);
}
Register FeedbackCard in createChannel({ components: [FeedbackCard] }).
Surviving a process restart also requires a durable StateStore, because the
SDK must reload the component snapshot that owns the callback.
Adding or removing the agent's own reactions with thread.react() and
thread.unreact() is not supported by the managed Slack or Teams adapter
today. Treat reactions as inbound signals.
See the command reference and Message reference for the complete handler shapes.