Rich messages and components

Build portable channel UI that renders as Slack Block Kit or Microsoft Teams Adaptive Cards, with explicit provider fallbacks.


Channels JSX lets application code describe a message once and lets the selected provider renderer turn it into native UI. The same component can produce Slack Block Kit or a Microsoft Teams Adaptive Card.

Enable Channels JSX#

Channels JSX does not use React at runtime. Point the TypeScript JSX transform at @copilotkit/channels:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "jsx": "react-jsx",
    "jsxImportSource": "@copilotkit/channels",
    "strict": true,
    "noEmit": true
  },
  "include": ["*.ts", "*.tsx"]
}

Keep "type": "module" in package.json and use .tsx for files containing JSX.

Build a portable status card#

incident-card.tsx
import {
  Context,
  Divider,
  Field,
  Fields,
  Header,
  Message,
  Section,
} from "@copilotkit/channels/ui";

export function IncidentCard(props: {
  id: string;
  summary: string;
  owner: string;
  status: string;
}) {
  return (
    <Message>
      <Header>{`${props.id} ยท ${props.status}`}</Header>
      <Section>{props.summary}</Section>
      <Divider />
      <Fields>
        <Field>{`Owner: ${props.owner}`}</Field>
        <Field>{`Status: ${props.status}`}</Field>
      </Fields>
      <Context>Updated from the incident service</Context>
    </Message>
  );
}

Post the component from a message handler or tool:

channel.tsx
channel.onMessage(async ({ thread }) => {
  await thread.post(
    <IncidentCard
      id="INC-421"
      summary="Checkout latency is above the SLO."
      owner="Payments"
      status="Investigating"
    />,
  );
});

Use named components with JSON-serializable props when they contain callbacks, then register them in createChannel({ components: [...] }). Pure display components do not need registration.

Know what each provider renders#

Channels componentSlackMicrosoft Teams
MessageFlattens to Block Kit; the current managed connector does not forward accentFlattens into an Adaptive Card; accent has no native mapping
HeaderHeader blockLarge bold text block
Section, MarkdownSlack mrkdwn sectionWrapped Adaptive Card text
Fields, FieldSection fields; honors Field.labelFact set; derives a label from Label: value text
ContextContext blockSmall, subtle text
Actions, ButtonActions block and buttonTop-level Action.Submit or Action.OpenUrl
Select, InputNative Block Kit controlsAdaptive Card inputs
Image, DividerNative image and divider blocksImage and separator
Table, Row, CellNative Slack table blockAdaptive Card 1.5 table
ChartOmitted by the Slack rendererTeams chart extension where the client supports it

Unknown or unsupported nodes are omitted instead of failing the whole message. That makes portable rendering resilient, but it also means the text around an optional visual must still carry the important result.

Select and Input have different interaction behavior by provider. Managed Slack dispatches those controls directly. Managed Teams renders Adaptive Card fields and submits them with a Button's Action.Submit; that Button callback receives the other named fields in ctx.values. Teams does not dispatch Select.onSelect or Input.onSubmit independently.

Slack text is translated from GitHub-flavored Markdown to mrkdwn. Collections and strings are clamped to Slack's native limits. If a message exceeds the top-level block budget, the renderer adds a truncation notice.

Although the direct Slack renderer can return an attachment accent, the current managed Intelligence post and update path forwards only blocks. Do not rely on Message.accent in a managed Slack workflow.

Do not put essential information only in Chart: the Slack renderer currently skips chart nodes. Pair a chart with a Section, Fields, or Table.

Update a message safely#

thread.post() returns a MessageRef that can be updated during the current managed delivery:

update.tsx
const ref = await thread.post(<IncidentCard {...incident} status="Loading" />);

await thread.update(ref, <IncidentCard {...incident} status="Investigating" />);

Do not persist that ref and assume it remains updateable in a later delivery. For a later button click, use the interaction's message.ref, check that message.ref.id is non-empty, and treat the update as best-effort.

Managed Slack supports post, update, and delete frames.

Keep provider-specific payloads at the edge#

The { raw: nativePayload, provider } escape hatch is part of the low-level renderable type, but it is not portable. Tag Slack Block Kit with provider: "slack" and a Teams Adaptive Card object with provider: "teams". A provider mismatch fails explicitly before the effect reaches the provider. Prefer Channels components in shared managed code.

Use interactive messages and approvals for callbacks, or browse the Channels UI reference for component props.