Commands and reactions
Understand managed command routing and handle provider reactions through typed Channels APIs.
Managed Slack and Teams treat leading-slash text as ordinary message content. Reaction handlers let an agent respond to lightweight feedback without treating every emoji as a new chat message.
Managed commands are not dispatched#
The generated Slack manifest does not register slash commands, and managed
Teams does not interpret /name prefixes. A message such as /triage customer
therefore reaches onMessage or onMention as normal text.
defineChannelCommand, commands, and onCommand remain available for direct
adapters that emit native command events. They do not opt a managed Slack or
Teams Channel into command routing.
Route mentions and ordinary messages#
Register onMessage for ordinary provider messages. A bot mention selects
onMention when that handler is registered; otherwise the mention falls back
to onMessage. Non-mention messages select onMessage, and one inbound message
runs exactly one of those handler paths.
The current generated manifest subscribes to ordinary channel, private-channel,
group-DM, and direct-message events in addition to app_mention. A Slack app
created from an older manifest may need the current event subscriptions and bot
scopes applied, followed by an app reinstall.
Handle incoming reactions#
Known Slack shortcodes and Teams reaction tokens are normalized to portable
names. The portable outbound set is thumbs_up, thumbs_down, heart, fire,
eyes, refresh, thinking, and tada.
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.
Output-free handlers are supported
The SDK finalizes and acknowledges a managed turn even when its handler posts no message. Managed delivery is still at-least-once, so make silent audit or database writes idempotent.
The generated Slack manifest subscribes to reaction_added and
reaction_removed. Managed ingress routes reactions on bot output and on
user-authored roots and replies back to their existing managed conversation;
reactions on unrelated workspace messages are ignored.
Attach a reaction handler to one message#
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.
Add or remove the bot's reaction with the delivery-scoped reference from the current message or transcript:
channel.onMessage(async ({ message, thread }) => {
await thread.react(message.ref, "eyes");
await thread.unreact(message.ref, "eyes");
});The portable set works on managed Slack and Teams. A provider-native string is
allowed when your code branches on the native provider: use message.platform
in a message handler or reaction.thread.platform in onReaction.
Unsupported values fail explicitly.
See the command reference and Message reference for the complete handler shapes.