# Quickstart

> Build the first managed Channel: create the Channel in CopilotKit Intelligence, write the createChannel + CopilotRuntime + createCopilotNodeListener listener, set the four environment variables, start with node --env-file, and verify status().overall === "online" before trusting the deploy.

- 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

- `README.md`
- `.agents/skills/build-channels-agent/SKILL.md`
- `examples/minimal-channel/server.ts`
- `examples/minimal-channel/lib/runtime.ts`
- `examples/minimal-channel/.env.example`

---

---
title: "Quickstart"
description: "Build the first managed Channel: create the Channel in CopilotKit Intelligence, write the createChannel + CopilotRuntime + createCopilotNodeListener listener, set the four environment variables, start with node --env-file, and verify status().overall === \"online\" before trusting the deploy."
---

A managed Channel is a long-running Node.js process that registers itself with CopilotKit Intelligence and receives every platform turn over a persistent gateway connection. The process you write does three things: declare the Channel with `createChannel`, attach it to a `CopilotRuntime` connected to `CopilotKitIntelligence`, and start it by creating a listener with `createCopilotNodeListener` — there is no `channel.start()`. Intelligence holds the Slack or Microsoft Teams credentials; your process carries only an Intelligence API key and the Channel Code.

<Note>
Prefer an agent-driven setup? `npx copilotkit@latest channels setup` walks a coding agent through this same path plus the provider-console steps. See <a href="/coding-agent-setup">Set up with a coding agent</a>. The steps below are the manual equivalent.
</Note>

## Prerequisites

- Node.js 22 or later (the launcher requires global `WebSocket`).
- A long-running Node process or container. A serverless request handler cannot host a Channel — it must own a persistent gateway connection.
- A CopilotKit Intelligence account. The managed path requires an Intelligence API key; there is no standalone way to run a Channel.
- An ESM project (`"type": "module"` in `package.json`) — the listener code uses top-level `await`.

## Build the Channel

<Steps>
<Step title="Create the Channel in CopilotKit Intelligence">

Create a Channel in the [CopilotKit Intelligence dashboard](https://docs.copilotkit.ai/channels) and connect Slack. Keep two values for later steps:

- The **Channel Code** — the identifier you pass as `createChannel({ name })`. It must match exactly: 3–64 characters, starting with a lowercase letter, lowercase letters and digits separated by single hyphens, unique within the project, and never the literal `channels`. A mismatch leaves the Channel at **Waiting for runtime** in the dashboard.
- The **project-scoped Intelligence API key**, issued from **API Keys** in the Intelligence project sidebar.

</Step>
<Step title="Install the SDK">

```sh
npm install @copilotkit/channels @copilotkit/runtime
npm install --save-dev tsx typescript @types/node
npm pkg set type=module
```

`@copilotkit/channels` and `@copilotkit/runtime` ship as a tested pair — always upgrade them together. If the project later fails to compile with an error about separate declarations of a private `_debug` property, pin `@ag-ui/client` with an `overrides` entry; see <a href="/installation">Installation</a>.

</Step>
<Step title="Write the listener">

The listener below uses CopilotKit's built-in agent. Replace `makeAgent` with any AG-UI-compatible agent factory (LangGraph, CrewAI, Mastra, a remote `HttpAgent`, …) without changing the Channel lifecycle.

```ts channel.ts
import { createServer } from "node:http";
import { createChannel } from "@copilotkit/channels";
import {
  BuiltInAgent,
  CopilotKitIntelligence,
  CopilotRuntime,
} from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";

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

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

const channel = createChannel({
  name: required("CHANNEL_CODE"),
  identifyUser: "platform",
  agent: makeAgent,
});

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 }],
  });
});

const intelligence = new CopilotKitIntelligence({
  apiKey: required("INTELLIGENCE_API_KEY"),
});

const runtime = new CopilotRuntime({
  agents: {},
  intelligence,
  identifyUser: () => ({
    id: "channels-runtime",
    name: "Channels Runtime",
  }),
  channels: [channel],
});

const listener = createCopilotNodeListener({
  runtime,
  basePath: "/api/copilotkit",
});

const channels = listener.channels;
if (!channels) throw new Error("Channels control surface was not created.");

const server = createServer(listener);
const shutdown = async () => {
  await channels.stop();
  if (server.listening) server.close();
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);

await channels.ready({ timeoutMs: 30_000 });

const status = channels.status();
if (status.overall !== "online") {
  throw new Error(`Channel is not online: ${JSON.stringify(status)}`);
}

const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => {
  console.log(`Channel online; lifecycle server listening on :${port}`);
});
```

Key points in this file:

- `identifyUser: "platform"` on `createChannel` is required. It derives the canonical user from provider + workspace + platform user id.
- `agent` is a factory `(threadId) => agent` — return a fresh agent per thread rather than sharing one stateful instance.
- Creating the listener is what starts the Channel, so `SIGINT`/`SIGTERM` teardown is wired around it and `channels.stop()` runs before the HTTP server closes.
- Managed delivery arrives over the Channel's own gateway socket, not the HTTP port — but keep the server: the runtime serves web requests through it, and most hosts require a listening port for health checks.

</Step>
<Step title="Set the four environment variables">

```dotenv .env
OPENAI_API_KEY=<openai-api-key>
INTELLIGENCE_API_KEY=<project-api-key>
CHANNEL_CODE=<channel-code-from-intelligence>
PORT=3000
```

<ParamField body="OPENAI_API_KEY" type="string" required>
Model credential consumed by `BuiltInAgent`. Not needed when you swap in a remote AG-UI agent instead.
</ParamField>

<ParamField body="INTELLIGENCE_API_KEY" type="string" required>
The project-scoped key from the Intelligence **API Keys** sidebar. Keep `.env` out of source control and never expose this key in browser code.
</ParamField>

<ParamField body="CHANNEL_CODE" type="string" required>
The exact Channel Code from Intelligence. Validated by the runtime at startup, not by `createChannel` — a typo fails at start, and the dashboard shows **Waiting for runtime**.
</ParamField>

<ParamField body="PORT" type="number" default="3000">
Port for the lifecycle HTTP server.
</ParamField>

Hosted Intelligence supplies the API and gateway endpoints automatically. Self-hosted deployments override `INTELLIGENCE_API_URL` and `INTELLIGENCE_GATEWAY_WS_URL` as a pair of bare base URLs — never derive one from the other. See <a href="/configuration-reference">Configuration reference</a>.

</Step>
<Step title="Start it">

```sh
node --env-file=.env --import tsx channel.ts
```

Expected output once activation completes:

```
Channel online; lifecycle server listening on :3000
```

When the Intelligence dashboard reports **Online**, invite the app to a Slack channel and mention it. The agent receives the conversation and replies in the thread.

</Step>
</Steps>

## Verify before trusting the deploy

`await channels.ready()` is not proof of life. It settles when activation resolves — including into `setup_required`, because a declared-but-unprovisioned Channel is a valid degraded state. Without the status gate you get a process that starts cleanly, serves HTTP 200 on its port, and answers nothing in Slack.

```ts
await channels.ready({ timeoutMs: 30_000 });

const status = channels.status();
if (status.overall !== "online") {
  throw new Error(`Channel is not online: ${JSON.stringify(status)}`);
}
```

`channels.status()` returns `overall` plus a per-Channel map, using the SDK's six lifecycle values:

| `status().overall` | Meaning |
| --- | --- |
| `connecting` | Activation in flight; `ready()` has not settled. |
| `online` | Connected. Send a real provider message to verify the full path. |
| `setup_required` | Declared but unprovisioned — finish the provider setup in Intelligence. `ready()` resolves here. |
| `reconnecting` | The gateway socket dropped; the connection layer is retrying. |
| `error` | Activation failed. `ready()` rejects with the cause. |
| `stopped` | `channels.stop()` was called. |

Activation errors are recorded as status and surfaced through `ready()` / `status()` — they are not thrown mid-run. Only an up-front misconfiguration, such as a duplicate or missing Channel name, throws synchronously. These SDK values are a different vocabulary from the Intelligence dashboard states (Waiting for runtime, Setup incomplete, Online, …); see <a href="/channel-lifecycle">Channel lifecycle and status</a> for the mapping.

## Troubleshooting

- **Dashboard stuck at "Waiting for runtime"** — the `name` passed to `createChannel` does not exactly match the Channel Code in Intelligence, or the process never started. Fix the code, not the platform connection.
- **`status().overall === "setup_required"`** — the Channel is declared but the provider setup in Intelligence is unfinished. Complete it in the dashboard. Do not switch to a direct adapter to escape this state — that is a known failure mode, not a fix. See <a href="/managed-vs-direct">Managed Channels vs direct adapters</a>.
- **`TS1309: The current file is a CommonJS module`** — the project is missing `"type": "module"`; run `npm pkg set type=module`. The listener uses top-level `await` and requires ESM.
- **`createChannel({ agent })` fails on a private `_debug` property** — two copies of `@ag-ui/client` are installed. Pin one with an `overrides` entry per <a href="/installation">Installation</a>.

More failure modes with fixes are collected in <a href="/troubleshooting">Troubleshooting</a>.

## Next

<CardGroup cols={2}>
  <Card title="Minimal Channel example" href="/minimal-channel-example">
    The same listener split into files — channel, runtime, env, server — with a no-model-call verification trick.
  </Card>
  <Card title="Handle mentions, messages, and subscriptions" href="/handle-conversations">
    Wire the full set of channel handlers and the subscribe pattern for answering every message in an invited conversation.
  </Card>
  <Card title="Channel lifecycle and status" href="/channel-lifecycle">
    How listener creation starts the Channel, what ready() actually settles on, and teardown ordering for SIGINT/SIGTERM.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Every environment variable, the paired Intelligence URL overrides, and the required tsconfig and package.json shape.
  </Card>
</CardGroup>
