Back to Copilotkit

Commands and reactions

showcase/shell-docs/src/content/docs/channels/commands-and-reactions.mdx

1.64.25.9 KB
Original Source

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.

Register a command

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.

ts
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">
ts
const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "slack",
  agent: makeAgent,
  commands: [triage],
});
</FrontendOnly> <FrontendOnly frontend="teams">
ts
const channel = createChannel({
  name: required("CHANNEL_CODE"),
  provider: "teams",
  agent: makeAgent,
  commands: [triage],
});
</FrontendOnly>

You can also add a command before the listener starts with channel.onCommand(command). Unregistered command names are ignored.

Configure the native command entry point

<FrontendOnly frontend="slack">

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:

ts
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.

</FrontendOnly> <FrontendOnly frontend="teams">

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:

text
/triage focus on customer impact

The optional bot mention is stripped before command matching. Arguments arrive as ctx.text; ctx.options remains empty.

</FrontendOnly>

Managed command contexts do not expose modal opening. Use a posted card and a later interaction for structured follow-up.

Handle incoming reactions

Known Slack shortcodes and Teams reaction tokens are normalized to portable names such as eyes, thumbs_up, and heart.

ts
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:

ts
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.

<Callout type="info" title="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. </Callout> <FrontendOnly frontend="slack">

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.

</FrontendOnly> <FrontendOnly frontend="teams">

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.

</FrontendOnly>

Attach a reaction handler to one message

Use Message.onReaction when only one posted component should react:

tsx
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.