# createChannel reference

> Every createChannel option with constraints: required identifyUser ("platform" or a callback), Channel Code naming rules for name, the agent factory contract and per-turn cloning, adapters, tools, context, components, commands, store (adapter, state schema, actionRetentionMs, concurrency), showToolStatus, replyContinuation, and sanitizeAgentEvents.

- 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`
- `.agents/skills/build-channels-agent/evals/evals.json`

---

---
title: "createChannel reference"
description: "Every createChannel option with constraints: required identifyUser (\"platform\" or a callback), Channel Code naming rules for name, the agent factory contract and per-turn cloning, adapters, tools, context, components, commands, store (adapter, state schema, actionRetentionMs, concurrency), showToolStatus, replyContinuation, and sanitizeAgentEvents."
---

`createChannel(options)` is the entry point of `@copilotkit/channels`. It returns a `Channel` object that you attach handlers to (`onMention`, `onMessage`, `onCommand`, …) and then hand to `CopilotRuntime({ channels: [channel] })`. The function itself does not start anything — there is no `channel.start()`; creating the runtime listener with `createCopilotNodeListener` is what starts the Channel. The factory is named `createChannel`; `createBot` and other `Bot`-prefixed names come from a pre-release and exist nowhere in the shipped packages.

```ts
import { createChannel } from "@copilotkit/channels";
import { makeAgent } from "./agent.js";

const channel = createChannel({
  name: process.env.CHANNEL_CODE!, // must equal the Channel Code in Intelligence
  identifyUser: "platform",        // required
  agent: makeAgent,                // factory: (threadId) => agent
});
```

<Note>
Every API on this page exists in both `@copilotkit/channels@0.6.1` + `@copilotkit/runtime@1.65.0` and the `0.7.1` + `1.66.1` pair, except `defineChannelComponent`, which is 0.7+ only. Channels and Runtime ship as a version-locked pair — upgrade them together.
</Note>

## Options summary

| Option | Type | Required | Purpose |
| --- | --- | --- | --- |
| `identifyUser` | `"platform"` \| callback | Yes | Resolve the canonical user per event |
| `name` | `string` | Managed Channels | Must equal the Intelligence Channel Code |
| `agent` | factory or agent instance | Yes (to run an agent) | AG-UI agent supplied per thread |
| `adapters` | `PlatformAdapter[]` | No | Direct platform connections you own |
| `tools` | `ChannelTool[]` | No | Typed tools the agent can call |
| `context` | `ContextEntry[]` | No | Prompt context injected per run |
| `components` | registered components | No | Re-bind agent-rendered UI handlers after restart |
| `commands` | `ChannelCommand[]` | No | Slash-command metadata (`defineChannelCommand`) |
| `store` | `{ adapter, state, actionRetentionMs, concurrency, … }` | No | Persistence, per-thread state schema, turn concurrency |
| `showToolStatus` | `boolean` | No | Live tool-call progress on managed Slack |
| `replyContinuation` | `{ messageByteLimit, maxMessages, truncationMarker }` | No | Long-reply splitting and truncation |
| `sanitizeAgentEvents` | option | No | Adjust the agent event stream before rendering |

## identifyUser

<ParamField body="identifyUser" type='"platform" | (ctx) => ApplicationUser | null' required>
Required on every `createChannel` call. `"platform"` derives the canonical user from provider + workspace + platform user id and is the right default. Pass a callback to map platform identities onto your own user table; it returns `ApplicationUser | null`.
</ParamField>

<Warning>
Do not confuse this with `CopilotRuntime({ identifyUser })`. The runtime option resolves users for *web* requests and must be absent on a Channels-only runtime. Omitting `identifyUser` on `createChannel` is a documented common mistake.
</Warning>

Per-user Intelligence Memory (`thread.runAgent({ memory })`) only means anything when `identifyUser` resolves a user.

## name — Channel Code rules

<ParamField body="name" type="string">
For a managed Channel, `name` must be the exact Channel Code from Intelligence, typically read from `process.env.CHANNEL_CODE`.
</ParamField>

Channel Code constraints:

- 3–64 characters.
- Starts with a lowercase letter.
- Lowercase letters and digits, separated by single hyphens.
- Unique within the Intelligence project.
- Never the literal string `channels`.

Validation happens in the **runtime at startup**, not inside `createChannel` — a typo fails when the listener starts, not at the call site, and a mismatch leaves the Channel stuck at **Waiting for runtime** in the dashboard. `name` is optional in the types only because purely local / custom-adapter Channels omit it.

## agent — factory contract and per-turn cloning

<ParamField body="agent" type="(threadId: string) => AbstractAgent | AbstractAgent">
Accepts a factory `(threadId) => agent` or a single agent instance. Prefer the factory and return a fresh agent per `threadId`; never share one stateful instance across conversations.
</ParamField>

```ts
import { BuiltInAgent } from "@copilotkit/runtime/v2";

export function makeAgent(threadId: string) {
  const agent = new BuiltInAgent({ model: "openai:gpt-5.4-mini" });
  agent.threadId = threadId;
  return agent;
}
```

The built-in agent runs in the same Node process — no `AGENT_URL`, no second server. For a remote AG-UI agent, use `HttpAgent` from `@ag-ui/client` (also re-exported from `@copilotkit/channels`) pointed at the agent's URL, as `examples/minimal-channel/lib/channel.ts` does.

Cloning and concurrency behavior:

- Turn concurrency defaults to `"parallel"`.
- You do not hand-manage isolation: Channels **clones the agent per turn** for every configured shape — a singleton instance, a fresh-per-call factory, and a factory that returns the same object.
- What cloning cannot fix is a broken `clone()`. A custom `AbstractAgent` subclass with no `clone()`, or one that drops subclass state, fails loudly at turn start.

<Warning>
If passing *any* agent fails to compile with "Types have separate declarations of a private property `_debug`", two copies of `@ag-ui/client` are installed. Pin one with `{ "overrides": { "@ag-ui/client": "0.0.57" } }` (use the version `@copilotkit/runtime` declares — `npm ls @ag-ui/client` shows both) and reinstall. See the installation page.
</Warning>

## adapters — direct platform connections

<ParamField body="adapters" type="PlatformAdapter[]">
Pass adapters only when *you* hold the platform tokens. One Channel can run several platforms at once. The managed default has no adapter at all — Intelligence holds the credentials and you add platforms in the dashboard.
</ParamField>

```ts
import { slack, defaultSlackTools, defaultSlackContext } from "@copilotkit/channels/slack";

const channel = createChannel({
  name: "support-slack",
  identifyUser: "platform",
  adapters: [
    slack({
      botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-…
      appToken: process.env.SLACK_APP_TOKEN!, // xapp-… (Socket Mode)
    }),
  ],
  agent: makeAgent,
  tools: [...defaultSlackTools],
  context: [...defaultSlackContext],
});
```

Constraints:

- A direct adapter does **not** remove the Intelligence requirement — the runtime still owns the lifecycle.
- Socket Mode and the `xapp-` token belong only to this path; a managed Channel uses signed HTTPS ingress plus an outbound gateway socket and needs no app token.
- Switching to a direct adapter to escape a `setup_required` managed Channel is a known failure mode, not a fallback — fix the managed setup instead.
- There is no `provider: "slack"` option on `createChannel`. The platform comes from the adapter, or from Intelligence for a managed Channel.

Adapters live on subpaths: `@copilotkit/channels/slack`, `/teams`, `/discord`, `/telegram`, `/whatsapp`.

## tools

<ParamField body="tools" type="ChannelTool[]">
Typed tools the agent can call, built with `defineChannelTool`. Parameters accept any Standard Schema validator (Zod, Valibot, ArkType). Also registrable after construction with `channel.tool(t)` before the runtime starts, or per run via `thread.runAgent({ tools })`.
</ParamField>

Tool handlers receive the parsed args plus `ChannelToolContext` = `{ thread, message?, user, actor, signal?, platform }`, where `user` is `ApplicationUser | null`. The return value goes back to the **agent**, not the user: return raw data (it is JSON-stringified for you), return the actual error text on failure, and for a tool that posts a card return a short confirmation like `"Displayed the issue card."`.

## context

<ParamField body="context" type="ContextEntry[]">
`{ description: string; value: string }` pairs injected into the agent's prompt on every run — the channel, the caller's role, anything that grounds the turn. Per-run context goes through `thread.runAgent({ context })` instead. Direct Slack Channels should include `defaultSlackContext`.
</ParamField>

## components

<ParamField body="components" type="ChannelComponent[]">
Registers components created with `defineChannelComponent` (0.7+) so the agent can call them as render tools, and so their keyed interaction handlers can be **re-bound after a restart** when the store is durable.
</ParamField>

Interactive handlers are keyed by content-stable IDs (`"ck:" + sha1(name | path | stableStringify(props)).slice(0, 16)`), so the same rendered control always maps back to the same handler. Durability requires both a durable `store.adapter` *and* registration here — without registration, a click on a message posted before the restart degrades to "action expired".

## commands

<ParamField body="commands" type="ChannelCommand[]">
Slash commands with richer metadata — a description and an `options` schema registered with the platform — built with `defineChannelCommand`. Handlers attach with `channel.onCommand(name, fn)`; arguments arrive on `CommandContext` as raw `text`, with `options` populated only on structured surfaces like Discord.
</ParamField>

## store

<ParamField body="store" type="object">
Persistence and per-thread behavior: the state store adapter, the per-thread state schema, transcripts, action retention, and turn `concurrency` (default `"parallel"`).
</ParamField>

```ts
const channel = createChannel({
  identifyUser: "platform",
  store: {
    adapter: myRedisStore,                      // StateStore implementation
    actionRetentionMs: 7 * 24 * 60 * 60 * 1000, // default 7 days
  },
  components: [IssueCard], // required for handler re-binding
});
```

<ResponseField name="store.adapter" type="StateStore">
Where interaction bindings, subscriptions, and per-thread state live. Default is the in-memory `MemoryStore` — ephemeral, so bindings are lost on restart and a button clicked after a redeploy won't resolve. Implement the `StateStore` interface (Redis, Postgres, …) for buttons that must work hours later or across deploys.
</ResponseField>

<ResponseField name="store.state" type="schema">
The per-thread state schema that types `thread.state<T>()` / `thread.setState(v)`.
</ResponseField>

<ResponseField name="store.actionRetentionMs" type="number" default="604800000">
How long interaction bindings are retained. Default 7 days.
</ResponseField>

<ResponseField name="store.concurrency" type="string" default='"parallel"'>
Turn concurrency. The per-turn agent clone means parallel turns do not share mutable agent state.
</ResponseField>

<Warning>
`createChannel({ actionStore })` still works but is **deprecated** — use `store.adapter`.
</Warning>

## showToolStatus

<ParamField body="showToolStatus" type="boolean">
Managed Slack hides tool-call progress by default so the conversation ends with a clean result; the lifecycle events still land in Intelligence history and are available on replay. Set `showToolStatus: true` to opt into the live timeline per Channel.
</ParamField>

This option is **ignored for direct-adapter Channels** — configure those on the adapter instead: `slack({ showToolStatus: true })`. Other managed providers keep their own default when it is unset.

## replyContinuation

<ParamField body="replyContinuation" type="{ messageByteLimit, maxMessages, truncationMarker }">
Providers cap how much text one message holds. Past the per-message limit the reply is split across continuation messages; past the ceiling it is truncated with a visible marker. Honoured by managed and direct Slack today.
</ParamField>

## sanitizeAgentEvents

<ParamField body="sanitizeAgentEvents" type="option">
Adjusts the agent's event stream before it is surfaced into the conversation. Listed by the SDK alongside `showToolStatus` and `replyContinuation` as an output-shaping option; no further constraints are documented in this repository.
</ParamField>

## What createChannel does not accept

- No `provider` option — the platform comes from adapters or Intelligence.
- No lifecycle methods on the return value — `channel.start()` / `channel.stop()` do not exist; attach the Channel to `CopilotRuntime` and create a listener, and stop via `listener.channels.stop()`.
- No `Bot`-prefixed API — `createBot`, `defineBotTool`, `defineBotCommand`, and `BotToolContext` do not compile.
- No generic event emitter — use the named handlers (`onMention`, `onMessage`, `onCommand`, `onInteraction`, `onInterrupt`, `onReaction`, `onModalSubmit`, `onModalClose`, `onThreadStarted`, `onWelcome`), attached before the runtime starts the Channel.

## Related pages

<CardGroup cols={2}>
  <Card title="Managed Channels vs direct adapters" href="/managed-vs-direct">
    When to pass `adapters` versus letting Intelligence hold the platform credentials.
  </Card>
  <Card title="Add tools" href="/add-tools">
    `defineChannelTool`, Standard Schema validators, and the `ChannelToolContext` shape.
  </Card>
  <Card title="Channel lifecycle and status" href="/channel-lifecycle">
    How the runtime starts the Channel, `ready()` semantics, and the six status values.
  </Card>
  <Card title="Human-in-the-loop approvals" href="/human-in-the-loop">
    Why `store.adapter` plus registered `components` make approval buttons survive restarts.
  </Card>
  <Card title="Handle mentions, messages, and subscriptions" href="/handle-conversations">
    The ten channel handlers attached to the object `createChannel` returns.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    `CHANNEL_CODE`, `INTELLIGENCE_API_KEY`, and the paired URL overrides.
  </Card>
</CardGroup>
