# Managed Channels vs direct adapters

> The managed default (no adapter, Channel Code from Intelligence, no platform tokens in your process) versus the direct-adapter path (adapters: [slack({ botToken, appToken })], Socket Mode, defaultSlackTools/defaultSlackContext). When each applies, and why switching to a direct adapter to escape setup_required is a known failure mode.

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

---

---
title: "Managed Channels vs direct adapters"
description: "The managed default (no adapter, Channel Code from Intelligence, no platform tokens in your process) versus the direct-adapter path (adapters: [slack({ botToken, appToken })], Socket Mode, defaultSlackTools/defaultSlackContext). When each applies, and why switching to a direct adapter to escape setup_required is a known failure mode."
---

`createChannel` supports two ways to reach a platform. The default is a **managed Channel**: no `adapters` option at all, `name` set to the Channel Code from CopilotKit Intelligence, and no Slack or Teams tokens anywhere in your process — Intelligence holds the platform credentials and you add platforms in the dashboard rather than in code. The secondary path is a **direct adapter**: `adapters: [slack({ botToken, appToken })]`, where you hold the platform tokens yourself and Slack delivery runs over Socket Mode. Both paths attach the Channel to a `CopilotRuntime` with a `CopilotKitIntelligence` connection — a direct adapter changes who owns the platform credentials, not who owns the lifecycle.

## The two paths at a glance

| Aspect | Managed (default) | Direct adapter |
| --- | --- | --- |
| `adapters` option | Absent | `[slack({ botToken, appToken })]` |
| `name` | Must equal the exact Channel Code from Intelligence | Free-form (Channel Code rules do not gate it) |
| Platform tokens in your process | None | `SLACK_BOT_TOKEN` (`xoxb-…`) and `SLACK_APP_TOKEN` (`xapp-…`) |
| Slack delivery | Signed HTTPS ingress into Intelligence + outbound gateway socket | Socket Mode via the `xapp-` app token |
| Adding a platform | In the Intelligence dashboard | In code, per adapter |
| Intelligence API key | Required | Still required — the runtime owns the lifecycle |
| Slash commands | Not part of the managed product surface | Available through the adapter |
| `showToolStatus` | `createChannel({ showToolStatus: true })` | Ignored on `createChannel` — pass `slack({ showToolStatus: true })` |

```mermaid
flowchart LR
  subgraph slackPlatform["Slack"]
    events["Platform events"]
  end
  subgraph intelligence["CopilotKit Intelligence"]
    ingress["Signed HTTPS ingress"]
    lifecycle["Runtime registration,\nhealth, reconnects"]
    creds["Slack / Teams credentials\n(managed path only)"]
  end
  subgraph yourProcess["Your long-running Node process"]
    channel["createChannel(...)"]
    runtime["CopilotRuntime + listener"]
    adapter["slack({ botToken, appToken })\n(direct path only)"]
  end
  events -- "managed" --> ingress
  ingress -- "gateway socket" --> channel
  events <-- "direct: Socket Mode" --> adapter
  adapter --> channel
  runtime --- lifecycle
```

## The managed default

A managed Channel has no adapter. `name` binds the process to a Channel declared in Intelligence, and Intelligence delivers each turn over the Channel's gateway socket:

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

Constraints that only apply on this path:

- **`name` must be the exact Channel Code**: 3–64 characters, starting with a lowercase letter, lowercase letters and digits separated by single hyphens, project-unique, and never the literal `channels`. The runtime validates it, not `createChannel` — a typo fails at startup and leaves the Channel at **Waiting for runtime** in the dashboard.
- **No `xapp-` token, ever.** Socket Mode belongs only to the direct-adapter path. Managed delivery uses signed HTTPS ingress into Intelligence plus an outbound gateway socket, so a managed setup needs no app token.
- **Managed Slack hides tool-call progress by default.** Opt in with `createChannel({ showToolStatus: true })`; the lifecycle events still land in Intelligence history either way.

The `examples/minimal-channel` example is this shape end to end: `createChannel` with no `adapters`, a `CopilotRuntime({ agents: {}, intelligence, channels: [channel] })`, and a `createCopilotNodeListener` whose creation starts the Channel.

## The direct-adapter path

Pass `adapters` only when *you* own the platform connection — you created the Slack app and you hold the bot and app tokens, and you do not want Intelligence holding the platform credentials. This puts platform secrets in your app and per-platform wiring in code:

```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 /* , ...yourTools */],
  context: [...defaultSlackContext /* , ...yourContext */],
});
```

- `adapters` is an array — one Channel can run several platforms at once. Adapters live on subpaths: `@copilotkit/channels/slack`, `/teams`, `/discord`, `/telegram`, `/whatsapp`.
- `defaultSlackTools` / `defaultSlackContext` add the `lookup_slack_user` tool plus tagging, mrkdwn, and threading guidance. Include them for direct Slack — a managed Channel gets equivalent behavior from Intelligence.
- `showToolStatus` set on `createChannel` is **ignored for direct-adapter Channels**; configure it on the adapter as `slack({ showToolStatus: true })`.
- `replyContinuation` (long-reply splitting and truncation) is honored by both managed and direct Slack.

<Warning>
A direct adapter does **not** remove the Intelligence requirement. There is no `channel.start()` and no standalone mode — you still attach the Channel to a `CopilotRuntime` with a `CopilotKitIntelligence` connection and create the listener, exactly as on the managed path. Claiming the direct adapter runs standalone is one of the failure modes the repository's own eval suite tests for.
</Warning>

## Choosing a path

Default to managed. The direct adapter is justified by exactly one condition: your organization must hold the Slack app and its tokens itself. Signals that point at each path:

- **Managed** — nothing in your requirements says you hold platform tokens; you want to add Teams later without code changes; you want zero platform secrets in your deployment; you are following the quickstart or the `setup-slack-channel` workflow.
- **Direct** — you already have your own Slack app, you hold `xoxb-`/`xapp-` tokens, and a policy or architecture decision requires that CopilotKit never hold the platform credentials.

## `setup_required` is not a reason to switch

A managed Channel that reports `setup_required` is **declared but unprovisioned** — the Channel exists in Intelligence but the provider setup (the Slack connection) is unfinished. Swapping to a direct adapter to "make it work" is a known failure mode, not a fallback: it trades an incomplete dashboard step for a different architecture with token custody you did not plan for, and the underlying setup gap remains for every future managed Channel. Fix the managed setup instead — the `setup-slack-channel` skill covers diagnosing a Channel stuck at `setup_required`, sitting at Waiting for runtime, or Online but silent.

Two related traps compound this one:

- **`channels.ready()` resolves on `setup_required`.** A declared-but-unprovisioned Channel is a valid degraded state, so `await ready()` is not proof of life. Gate startup on `channels.status().overall === "online"`, or the process starts cleanly, serves HTTP 200, and answers nothing.
- **A `name` mismatch looks similar but is a different state.** A Channel Code typo shows **Waiting for runtime** in the dashboard, not `setup_required`. Check the code matches exactly before touching anything else.

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

## Related pages

<CardGroup>
  <Card title="Channel lifecycle and status" href="/channel-lifecycle">
    The six SDK status values, why ready() resolves on setup_required, and how they map to the Intelligence dashboard states.
  </Card>
  <Card title="Architecture and the runtime boundary" href="/architecture">
    What you host versus what Intelligence manages, and why a Channel needs a persistent gateway connection.
  </Card>
  <Card title="createChannel reference" href="/createchannel-reference">
    Every createChannel option, including adapters, Channel Code naming rules, and showToolStatus.
  </Card>
  <Card title="Set up with a coding agent" href="/coding-agent-setup">
    The setup workflow that provisions a managed Channel correctly instead of routing around setup_required.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    setup_required, Waiting for runtime, and the other documented failure modes with fixes.
  </Card>
</CardGroup>
