JSX callbacks

Reference for portable component handlers and their managed Slack and Teams interaction context.


Portable Channels components can carry callbacks. Slack and Teams receive opaque action ids; the SDK resolves the original handler when the interaction returns.

Slack renders the controls as Block Kit. With Slack app Interactivity enabled, clicks arrive as block_actions through the signed managed webhook.

Teams renders the controls in Adaptive Cards. Buttons use Action.Submit, and clicks return through the managed connection as message activities. A Teams Action.Submit activity can arrive without a source message ref; in that case, message.ref.id is empty.

Callback props

ComponentPropHandler value
ButtononClickInteractionContext<TValue>; read the Button's value from action.value
SelectonSelectInteractionContext<string | string[]>; arrays are used when multi is set
InputonSubmitInteractionContext<string> containing the submitted text
MessageonReaction(emoji, reaction)

Managed Teams control boundary

Managed Teams dispatches Button submissions. Named Select and Input fields from the same Adaptive Card are available on that Button callback as ctx.values, but Teams does not dispatch their onSelect or onSubmit callbacks independently.

Portable component props

Import these components from @copilotkit/channels/ui. Renderers may simplify or omit a component when the native provider has no equivalent.

ComponentKey props
Messageaccent?, onReaction?, children?
Header, Section, Markdown, Context, Actionschildren?
Fields / Fieldchildren?; Field also accepts label?
Buttonvalue?, onClick?, url?, style?: "primary" | "danger", children?
Selectname?, options, placeholder?, multi?, onSelect?
Inputname?, placeholder?, multiline?, onSubmit?
Imageurl, alt?
DividerNo props or children
Table / Row / CellTable.columns?; nested rows and cells are children
Chartdata, plus optional type, title, xAxisTitle, and yAxisTitle

Button.url creates a link button and ignores value and onClick. Select.options is an array of { label, value }. Chart.data is an array of { label, value }; unsupported native chart surfaces omit the chart.

InteractionContext

Button, Select, and Input callbacks receive:

FieldTypeDescription
threadThreadPost, update, run, or resume the conversation.
messageIncomingMessageMessage containing the control. Its ref is updateable only when the interaction carries a non-empty message.ref.id.
action.idstringOpaque action identifier.
action.valueTValue | undefinedThe control's round-tripped value. Guard it before use even when the component declares value.
valuesRecord<string, unknown>Submitted form values.
userApplicationUser | nullApplication user selected by identifyUser.
actorProviderActorProvider account that caused the interaction.
platformstringNormalized source provider, such as "slack" or "teams".
openModalfunction or undefinedCapability-gated modal opener. Not part of the managed Slack/Teams realtime path.
decision.tsx
import { Actions, Button, Message, Section } from "@copilotkit/channels/ui";

export function Decision({ id }: { id: string }) {
  return (
    <Message>
      <Section>{`Approve ${id}?`}</Section>
      <Actions>
        <Button
          value={{ id, approved: true }}
          onClick={async ({ thread, message, action }) => {
            const value = action.value;
            if (value === undefined) return;

            if (message.ref.id) {
              try {
                await thread.update(message.ref, `Approved ${value.id}.`);
              } catch {
                // Updating the card is best-effort; always resume the agent.
              }
            }
            await thread.resume(value);
          }}
        >
          Approve
        </Button>
      </Actions>
    </Message>
  );
}

An interaction ref is updateable only when message.ref.id is non-empty. Always resume outside the update guard and error handler so an omitted ref or failed card update cannot strand the interrupted run.

Reaction callback

Message.onReaction receives (emoji, reaction). The reaction contains added, user, messageId, messageRef, and thread. Managed reaction delivery is provider-dependent; do not require it for a cross-platform approval. An output-free managed callback is finalized and acknowledged, but application side effects should remain idempotent because managed delivery is at-least-once.

Registration and persistence

Register every named component that carries a callback:

channel.tsx
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";
import { Decision } from "./decision.js";
import { durableStateStore } from "./state-store.js";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  identifyUser: "platform",
  agent: makeAgent,
  components: [Decision],
  store: { adapter: durableStateStore },
});

The same Channel declaration handles managed Slack and Teams deliveries.

Registration allows the action registry to rebuild the callback from the component name and serializable props. A durable StateStore is still required for clicks on messages posted before a process restart.

See the interactive messages and approvals guide for Slack or Teams for the full post-and-resume flow.