# Channel lifecycle and status

> The runtime owns the lifecycle — there is no channel.start(). Listener creation starts the Channel; channels.ready() settles activation but also resolves on setup_required; the six SDK status values (connecting, online, setup_required, reconnecting, error, stopped) versus the Intelligence dashboard states; teardown ordering for SIGINT/SIGTERM.

- 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`
- `examples/minimal-channel/server.ts`
- `README.md`
- `examples/minimal-channel/lib/runtime.ts`

---

---
title: "Channel lifecycle and status"
description: "The runtime owns the lifecycle — there is no channel.start(). Listener creation starts the Channel; channels.ready() settles activation but also resolves on setup_required; the six SDK status values (connecting, online, setup_required, reconnecting, error, stopped) versus the Intelligence dashboard states; teardown ordering for SIGINT/SIGTERM."
---

`@copilotkit/runtime` owns the Channel lifecycle end to end. A `Channel` returned by `createChannel` has no public `start()` or `stop()` method — you attach it to a `CopilotRuntime` via `channels: [channel]`, and creating the listener with `createCopilotNodeListener` is what activates it. From that point the lifecycle is observed and controlled through `listener.channels`: `ready()` to await activation settling, `status()` to read the current state, and `stop()` to tear the Channel down.

<Warning>
There is no `channel.start()` / `channel.stop()`. Calling either fails to compile — the runtime starts the Channel when the listener is created, and teardown goes through `listener.channels.stop()`.
</Warning>

## How a Channel starts

Attach the Channel to a `CopilotRuntime` and create a listener. Listener creation triggers activation: the runtime opens the outbound gateway socket to CopilotKit Intelligence and registers the Channel.

```ts title="lib/runtime.ts"
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { channel } from "@/lib/channel";

const intelligence = new CopilotKitIntelligence({
  apiKey: required("INTELLIGENCE_API_KEY"),
  apiUrl: process.env.INTELLIGENCE_API_URL,        // paired override, optional
  wsUrl: process.env.INTELLIGENCE_GATEWAY_WS_URL,  // never derive one from the other
});

const runtime = new CopilotRuntime({
  agents: {}, // required, even when the Channel supplies the agent
  intelligence,
  channels: [channel],
});

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

`listener.channels` is non-optional when you pass a literal non-empty `channels: [channel]` array — that shape is branded, so no `!` or `?.` is needed under `strict`. A dynamically assembled `Channel[]` falls to the optional overload and does need `?.`.

Managed delivery arrives over the Channel's own gateway socket, not the HTTP port — but keep the HTTP server: it serves web requests through the runtime, and most hosts require a listening port for health checks. Because startup uses top-level `await`, the project must be ESM (`npm pkg set type=module`), and Node.js 22+ is required (the launcher needs global `WebSocket`).

## Lifecycle state machine

```mermaid
stateDiagram-v2
    [*] --> connecting: createCopilotNodeListener()
    connecting --> online: activation succeeds
    connecting --> setup_required: declared but unprovisioned
    connecting --> error: activation fails
    online --> reconnecting: gateway socket drops
    reconnecting --> online: connection layer retries
    online --> stopped: channels.stop()
    setup_required --> stopped: channels.stop()
    reconnecting --> stopped: channels.stop()
    stopped --> [*]
```

Activation errors are recorded as status and surfaced through `ready()` / `status()` — they are not thrown from the listener call. Only an up-front misconfiguration, such as a duplicate or missing Channel name, throws synchronously.

## channels.ready()

`ready()` blocks until activation settles. It accepts a timeout:

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

<Warning>
`ready()` is **not** proof of life. It resolves on `setup_required` too, because a declared-but-unprovisioned Channel is a valid degraded state. Without a follow-up status check you get a process that starts cleanly, serves HTTP 200, and answers nothing.
</Warning>

Gate the deploy on the actual status:

```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)}`);
}
```

How `ready()` settles per outcome:

| Activation outcome | `ready()` behavior |
| --- | --- |
| `online` | Resolves |
| `setup_required` | Resolves (degraded but valid) |
| `error` | Rejects with the activation cause |
| Timeout elapsed | Rejects |

## channels.status()

`channels.status()` returns SDK lifecycle values as `overall` plus a per-Channel map.

| `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. |

### SDK status versus Intelligence dashboard states

The six SDK values are **not** the same vocabulary as the Intelligence dashboard's Channel states: **Disabled, Setup incomplete, Setup failed, Waiting for runtime, Conflict, Offline, Delivery failing, Online**. The SDK reports what your process observes about its own activation and gateway connection; the dashboard reports what Intelligence observes about provisioning and delivery. A process can sit at `setup_required` locally while the dashboard shows **Setup incomplete**, or report `online` while the dashboard shows **Delivery failing**.

<Note>
Do not switch to a direct adapter to escape `setup_required` — it is a provisioning state, cleared by finishing the provider setup in Intelligence, not a reason to change the delivery path.
</Note>

## Teardown ordering for SIGINT/SIGTERM

Wire the signal handlers **before** the listener exists, because creating the listener starts the Channel. A Ctrl-C during the connect window then still tears the Channel down instead of hitting Node's default handler. Stop the Channel first, then close the HTTP server:

```ts title="server.ts (recommended ordering)"
let teardown: (() => Promise<void>) | undefined;
const shutdown = async () => { await teardown?.(); };
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);

const listener = createCopilotNodeListener({ runtime, basePath: "/api/copilotkit" });
const channels = listener.channels;
const server = createServer(listener);
teardown = async () => {
  await channels.stop();
  if (server.listening) server.close();
};

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

server.listen(Number(process.env.PORT ?? 3000));
```

The `examples/minimal-channel` server shows the compact form of the same shape — `ready()` gate, HTTP server, `SIGINT` teardown:

```ts title="examples/minimal-channel/server.ts"
import { createServer } from "node:http";
import { listener } from "@/lib/runtime";

await listener.channels?.ready({ timeoutMs: 15_000 });

const server = createServer(listener);

server.listen(3000, () => {
  console.log("\nServer listening on port 3000...");
});

process.on("SIGINT", async () => {
  await listener.channels?.stop();
  server.close();
});
```

## Verifying a live Channel

`status().overall === "online"` confirms activation, not the full delivery path. To verify end to end without spending a model call, use the minimal-channel pattern: an `onMention` handler that posts a single `🪁` instead of running the agent. When Intelligence shows **Online**, mention the app on the provider — a kite in the thread confirms ingress, your process, and delivery all work. Then swap the handler body for `thread.runAgent()`.

## Lifecycle rules summary

- Do not call `channel.start()` / `channel.stop()` — no public lifecycle method exists on `Channel`. Attach it to `CopilotRuntime` and create a listener.
- Wire `SIGINT`/`SIGTERM` handlers before creating the listener.
- Do not treat `await ready()` as proof of life — gate on `status().overall === "online"`.
- Activation errors surface through `ready()` rejection and `status()`, not as thrown exceptions from listener creation.
- Attach handlers (`onMention`, `onMessage`, …) to the Channel before the runtime starts it.

## Related pages

<CardGroup>
  <Card title="Minimal Channel example" href="/minimal-channel-example">
    The smallest complete listener with the ready() gate, SIGINT teardown, and the 🪁 no-model-call verification trick.
  </Card>
  <Card title="Architecture and the runtime boundary" href="/architecture">
    Why a Channel needs a persistent gateway connection, and what you host versus what Intelligence manages.
  </Card>
  <Card title="Managed Channels vs direct adapters" href="/managed-vs-direct">
    Why switching to a direct adapter to escape setup_required is a known failure mode.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    setup_required, Waiting for runtime, and ready() resolving on a degraded Channel — with fixes.
  </Card>
</CardGroup>
