showcase/shell-docs/src/content/docs/channels/index.mdx
Channels let you take a CopilotKit agent and run it as a bot inside a chat app. The user talks to your agent in Slack, Teams, WhatsApp, Telegram, or Discord, and your agent answers right there in the thread.
You write the bot once. The same agent, tools, commands, and interactive cards run on every platform you attach.
Three pieces: a TanStack AI agent, JSX turned on, and a channel that plugs the agent into a platform and replies with clickable UI.
npm install @copilotkit/channels @copilotkit/runtime @tanstack/ai @tanstack/ai-openai
A Built-in Agent in TanStack factory mode. You own the model call with TanStack AI; the agent turns its stream into the events the channel renders.
import { BuiltInAgent, convertInputToTanStackAI } from "@copilotkit/runtime/v2";
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export const agent = new BuiltInAgent({
type: "tanstack",
factory: (ctx) => {
const { messages, systemPrompts, tools } = convertInputToTanStackAI(ctx.input);
return chat({
adapter: openaiText("gpt-5.5"),
messages,
systemPrompts: ["You are a helpful support assistant.", ...systemPrompts],
tools,
abortController: ctx.abortController,
});
},
});
Replies render as native UI. Point the JSX runtime at the channels package.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@copilotkit/channels"
}
}
Declare a channel, then answer messages. A reply can be the agent's own words, or a card whose button you handle right where you wrote it.
import { createChannel } from "@copilotkit/channels";
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { Message, Section, Actions, Button } from "@copilotkit/channels/ui";
import { agent } from "./agent";
// A managed channel: Intelligence hosts the transport, so no adapter.
const channel = createChannel({
name: "support-bot",
agent,
});
channel.onMessage(async ({ thread, message }) => {
// let the agent answer
await thread.runAgent({ prompt: message.text });
// then post a card, and handle the click right here
await thread.post(
<Message>
<Section>{"Did that answer your question?"}</Section>
<Actions>
<Button
onClick={async ({ thread }) => {
await thread.post("Glad I could help!");
}}
>
Yes, thanks
</Button>
</Actions>
</Message>,
);
});
// The runtime owns the channel's lifecycle. A CopilotKit Intelligence key runs
// any channel (free tier available); `ready()` activates it.
const runtime = new CopilotRuntime({
agents: {},
intelligence: new CopilotKitIntelligence({
// apiUrl and wsUrl default to the managed Intelligence platform. Override
// both together only for a self-hosted deployment.
apiKey: process.env.COPILOTKIT_API_KEY!,
}),
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
channels: [channel],
});
const listener = createCopilotNodeListener({ runtime });
await listener.channels.ready();
// Mount the listener on an HTTP server to keep the process running (see the Quickstart).
That is a working bot: a TanStack AI agent, answering across platforms, replying with clickable UI. The Quickstart adds the dashboard setup and environment, and the UI library shows how to register that card so its button still works after a restart.
Every channel runs through the Intelligence runtime, so both paths need a CopilotKit Intelligence key (free tier available). The difference is who holds the platform bot credentials — either way, the runtime drives the channel.
| Managed | Direct (platform adapter) | |
|---|---|---|
| Who holds the platform tokens | CopilotKit Intelligence | You supply them to the adapter |
| Add a platform | Attach it in the Intelligence dashboard | Add another adapter in code |
| Best for | Shipping fast, many platforms, no platform secrets in your app | Owning one platform's connection end to end |
With the managed path your process holds no Slack or Teams tokens: Intelligence receives the platform event, delivers the turn to your bot, runs the same handlers, and does the credentialed send back. With a direct adapter you supply the platform credentials, but the runtime still owns the channel's lifecycle. Start with managed. See Platforms for what Intelligence handles for you and the per-platform setup.