# Thread API reference

> The per-conversation thread handle: post, update, delete, stream, postFile, postEphemeral, runAgent (prompt, context, tools, transcript, memory grants), resume, awaitChoice, subscribe/unsubscribe/isSubscribed, getMessages, setTitle, setSuggestedPrompts, react, state/setState, and lookupUser — including which methods are capability-gated and degrade instead of throwing.

- Repository: CopilotKit/channels-sdk
- GitHub: https://github.com/CopilotKit/channels-sdk
- Human docs: https://grok-wiki.com/public/docs/copilotkit-channels-sdk-4c947e3e6161
- Complete Markdown: https://grok-wiki.com/public/docs/copilotkit-channels-sdk-4c947e3e6161/llms-full.txt

## Source Files

- `.agents/skills/build-channels-agent/SKILL.md`
- `.agents/skills/build-channels-agent/references/hitl-patterns.md`
- `examples/minimal-channel/lib/channel.ts`
- `examples/minimal-channel/README.md`

---

---
title: "Thread API reference"
description: "The per-conversation thread handle: post, update, delete, stream, postFile, postEphemeral, runAgent (prompt, context, tools, transcript, memory grants), resume, awaitChoice, subscribe/unsubscribe/isSubscribed, getMessages, setTitle, setSuggestedPrompts, react, state/setState, and lookupUser — including which methods are capability-gated and degrade instead of throwing."
---

Every channel handler — `onMention`, `onMessage`, `onCommand`, `onInterrupt`, tool handlers, inline `onClick`/`onSelect` handlers — receives a `thread`: the per-conversation handle for `@copilotkit/channels`. All rendering, agent runs, human-in-the-loop pauses, and per-conversation state go through it. The same `Thread` methods work on every platform an adapter supports; where a surface cannot express an operation, the capability-gated methods degrade to an empty or `undefined` result rather than throwing.

<Note>
`onMention` and `onMessage` handlers receive `{ thread, message }` only — there is no `user` on those contexts. Reach the caller through the message, or use a context that exposes `user` (`onThreadStarted`, `onWelcome`, `ChannelToolContext`).
</Note>

## Method inventory

| Method | Purpose | Returns |
| --- | --- | --- |
| `thread.post(ui)` | Render a JSX message | `MessageRef` |
| `thread.update(ref, ui)` | Replace a previously posted message | — |
| `thread.delete(ref)` | Remove a message | — |
| `thread.stream(src)` | Stream a `string` / `AsyncIterable<string>` live | — |
| `thread.postFile({ ... })` | Upload a file | — |
| `thread.postEphemeral(user, ui, opts)` | Message visible to one user only; `opts` (with `fallbackToDM`) is required | — |
| `thread.runAgent(input?)` | Run the agent's run / tool-call / interrupt loop | — |
| `thread.resume(value, opts?)` | Re-enter the run loop after an interrupt | `MessageRef \| undefined` |
| `thread.awaitChoice<T>(ui)` | Post a picker and block until the user chooses | `T` |
| `thread.subscribe()` / `unsubscribe()` / `isSubscribed()` | Persisted per-conversation flag | `boolean` from `isSubscribed()` |
| `thread.getMessages()` | Read the conversation history | messages, or `[]` (capability-gated) |
| `thread.setTitle(title)` | Rename the conversation surface | — |
| `thread.setSuggestedPrompts(...)` | Suggest follow-up prompts | — |
| `thread.react(ref, emoji)` / `unreact(ref, emoji)` | Add or remove an emoji reaction on a message | — |
| `thread.state<T>()` / `setState(v)` | Per-thread state, typed by the `store.state` schema | `T` from `state()` |
| `thread.lookupUser(query)` | Resolve a platform user | user, or `undefined` (capability-gated) |

<Warning>
Handlers must return `void | Promise<void>`, and `post` returns a `MessageRef`. A concise arrow like `({ thread }) => thread.post(...)` fails under `strict`. Use a block body: `async ({ thread }) => { await thread.post(...); }`.
</Warning>

## Posting and rendering

`post`, `update`, `awaitChoice`, and `postEphemeral` take a JSX tree built from the `@copilotkit/channels` components (`Message`, `Header`, `Section`, `Markdown`, `Actions`, `Button`, …). The engine lowers the tree to a platform-neutral IR; each adapter renders what its surface supports and skips nodes it cannot express, so a rich tree degrades gracefully instead of erroring.

```tsx
const ref = await thread.post(
  <Message accent="#27AE60">
    <Header>Deploy status</Header>
    <Section><Markdown>**staging** is green.</Markdown></Section>
  </Message>,
);

await thread.update(ref, <Section>Superseded — see the new report below.</Section>);
await thread.delete(ref);
```

Inline `onClick`/`onSelect` handlers receive `{ action, thread, messageRef, user }`, so a button can rewrite the message it lives in:

```tsx
<Button value="approve" style="primary"
  onClick={async ({ thread, messageRef }) => {
    await thread.update(messageRef, <Section>Approved ✓</Section>);
  }}>
  Approve
</Button>
```

`thread.stream(src)` accepts a plain string or an `AsyncIterable<string>` and progressively edits a live message as tokens arrive. `thread.postFile({ ... })` uploads a file where the adapter implements the `postFile` capability. `thread.postEphemeral(user, ui, { fallbackToDM })` targets a single user; the options argument is required.

## runAgent

`thread.runAgent(input?)` drives the agent's run / tool-call / interrupt loop and renders each step as it streams. Prefer it over hand-managing agent events. Called with no arguments, `prompt` defaults to the inbound `message.contentParts` or `message.text`, so plain `runAgent()` is the correct mention reply:

```ts
channel.onMention(async ({ thread }) => {
  await thread.runAgent();
});
```

Pass `prompt` explicitly only when the input is not in reconstructed history — slash-command arguments, or when combining text and attachments yourself:

```ts
channel.onMessage(async ({ thread, message }) => {
  await thread.runAgent({
    prompt: message.contentParts?.length
      ? [
          ...(message.text ? [{ type: "text" as const, text: message.text }] : []),
          ...message.contentParts,
        ]
      : message.text,
    context: [{ description: "Originating platform", value: message.platform }],
  });
});
```

<ParamField body="prompt" type="string | AgentContentPart[]">
The user turn for this run. Defaults to the inbound message's `contentParts` or `text` when omitted. Slash-command arguments are never posted to the channel, so they are not in reconstructed history — pass them explicitly (`runAgent({ prompt: \`Triage: ${text}\` })`).
</ParamField>

<ParamField body="context" type="ContextEntry[]">
`{ description: string; value: string }` pairs injected into the agent's prompt for this run only. Channel-wide context goes on `createChannel({ context })` instead.
</ParamField>

<ParamField body="tools" type="ChannelTool[]">
Extra tools available for this run, in addition to those registered via `createChannel({ tools })` or `channel.tool()`.
</ParamField>

<ParamField body="transcript" type="boolean">
Auto-bridges cross-platform transcripts: injects history, appends the user turn, runs, appends the reply. The flag owns the whole bridge — do not also append the same turns via `channel.transcripts.append`. It no-ops with a warning when identity or transcripts are not configured.
</ParamField>

<ParamField body="memory" type="{ user?: Grant; project?: Grant }">
Intelligence Memory grant for this run only, where each grant is `"none" | "read" | "read-write"` (e.g. `{ user: "read-write", project: "read" }`). Omitting `memory` disables Memory entirely — there is no implicit access.
</ParamField>

## resume

`thread.resume(value, opts?)` re-enters the run loop after the agent paused itself mid-run (a LangGraph-style interrupt surfaced through `channel.onInterrupt`). It returns the next `MessageRef`, or `undefined`. The options take the same `memory` grant shape as `runAgent`, plus `subject`:

```ts
channel.onInterrupt<{ question: string }>("ask_human", async ({ thread, payload }) => {
  const answer = await thread.awaitChoice<string>(
    /* a <Select> or <Button> group built from payload.question */
  );
  await thread.resume(answer); // agent continues from where it paused
});
```

## awaitChoice

`thread.awaitChoice<T>(ui)` posts the JSX tree and blocks the handler until the user activates a control, resolving to that control's `value` typed as `T`. Because tool handlers receive the live `thread`, calling it inside a `defineChannelTool` handler gates a destructive tool on human approval:

```tsx
const ok = await thread.awaitChoice<boolean>(
  <Message accent="#E01E5A">
    <Section><Markdown>Deploy to **production**? This is irreversible.</Markdown></Section>
    <Actions>
      <Button value={true} style="primary">Ship it</Button>
      <Button value={false} style="danger">Cancel</Button>
    </Actions>
  </Message>,
);
if (!ok) return "User cancelled; nothing was deployed.";
```

Whether the buttons still resolve after a process restart depends on the configured store: the default in-memory `MemoryStore` loses bindings on restart, while a durable `StateStore` adapter plus registered components (`createChannel({ store: { adapter }, components })`) lets clicks survive redeploys.

## Subscriptions

`subscribe()`, `unsubscribe()`, and `isSubscribed()` manage a persisted per-conversation flag. The standard pattern answers every message in a conversation the agent was invited into, rather than only mentions — this is exactly what `examples/minimal-channel/lib/channel.ts` ships:

```ts
channel.onMention(async ({ thread }) => {
  await thread.subscribe();
  await thread.runAgent();
});

channel.onMessage(async ({ thread }) => {
  if (await thread.isSubscribed()) await thread.runAgent();
});
```

## Conversation surface and state

- `thread.getMessages()` reads the conversation history where the adapter implements it.
- `thread.setTitle(title)` renames the conversation surface (for example a Slack assistant pane).
- `thread.setSuggestedPrompts(...)` offers follow-up prompts on surfaces that support them.
- `thread.react(ref, emoji)` / `thread.unreact(ref, emoji)` add or remove an emoji reaction on a posted message. Inbound reactions arrive via `channel.onReaction` or a `<Message onReaction>` prop.
- `thread.state<T>()` / `thread.setState(v)` read and write per-thread state. The type is derived from the `state` schema configured under `createChannel({ store })`.
- `thread.lookupUser(query)` resolves a platform user through the adapter's `lookupUser` implementation.

## Capability gating and degradation

Not every platform can express every operation. Adapters declare a `capabilities` object, and the optional adapter methods behind it include `getMessages`, `postFile`, `setSuggestedPrompts`, and `setThreadTitle`. Capability-gated `Thread` methods degrade rather than throw:

| Method | Where the adapter lacks the capability |
| --- | --- |
| `thread.getMessages()` | returns `[]` |
| `thread.lookupUser(query)` | returns `undefined` |

Rendering follows the same philosophy: an adapter's message renderer is total and skips IR nodes its surface cannot express, so `post`/`update` never throw on an unsupported component. Modals are the exception to skip-and-degrade — an unrenderable `ModalView` throws `ModalRenderError` — but modals open through `ctx.openModal` on `CommandContext`, not through the thread handle.

For direct Slack, `defaultSlackTools` from `@copilotkit/channels/slack` also exposes user resolution to the agent as the `lookup_slack_user` tool.

## Constraints

- Await every thread call. Handlers must resolve to `void`; returning a `MessageRef` from a concise arrow fails under `strict`.
- JSX trees passed to `post`/`update`/`awaitChoice`/`postEphemeral` must come from `@copilotkit/channels` components in a `.tsx` file compiled with `jsxImportSource: "@copilotkit/channels"`. There are no lowercase intrinsic tags — `<b>`, `<div>`, `<span>` are compile errors; emphasis goes inside `<Markdown>`.
- Do not set `runAgent({ transcript: true })` and also append the same turns via `channel.transcripts.append` — the flag owns the bridge.
- Do not expect Intelligence Memory without an explicit `memory` grant on `runAgent` or `resume` — omission disables it.
- Prefer `thread.runAgent()` over manually looping over agent events.

## Related pages

<CardGroup cols={2}>
  <Card title="Handle mentions, messages, and subscriptions" href="/handle-conversations">
    The ten channel handlers that hand you a thread, and the subscribe pattern in context.
  </Card>
  <Card title="Human-in-the-loop approvals" href="/human-in-the-loop">
    awaitChoice, onInterrupt + resume, and making approval buttons survive restarts.
  </Card>
  <Card title="Render interactive UI" href="/render-interactive-ui">
    The JSX trees you pass to post, update, and awaitChoice, and how they degrade per surface.
  </Card>
  <Card title="createChannel reference" href="/createchannel-reference">
    Channel-level options that shape thread behavior: store, state schema, tools, context, components.
  </Card>
  <Card title="Add tools" href="/add-tools">
    ChannelToolContext and how tool handlers use the live thread mid-run.
  </Card>
  <Card title="UI components reference" href="/ui-components-reference">
    Full component vocabulary and the handler context shapes for onClick, onSelect, and onSubmit.
  </Card>
</CardGroup>
