Quickstart

Build a bot that answers messages in a chat app, end to end.


You have a CopilotKit agent. By the end of this page it answers messages in a real chat thread, run by the CopilotKit runtime.

You need a CopilotKit Intelligence key

Every channel runs through the Intelligence runtime, so a CopilotKit Intelligence key is required to run one — for both managed and direct channels. A free tier is available. Grab a project key (cpk-...) from the Intelligence dashboard before you start.

Install#

npm install @copilotkit/channels @copilotkit/runtime @tanstack/ai @tanstack/ai-openai

Turn on JSX#

Replies render through components, so point the JSX runtime at the UI library. See the UI library for what this unlocks.

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@copilotkit/channels"
  }
}

Create a channel in the dashboard#

In the Intelligence dashboard, create a channel and attach a platform (Slack, Teams, and so on). Note the channel name, you will pass the same name in code. Copy your project runtime API key (cpk-...).

See Intelligence Platform for the dashboard walkthrough and per-platform install links.

Set the environment#

The Intelligence runtime reads the first two. For a managed channel no platform tokens live in your app. OPENAI_API_KEY powers the agent's model.

COPILOTKIT_INTELLIGENCE_URL=https://your-intelligence-url
COPILOTKIT_API_KEY=cpk-...
OPENAI_API_KEY=sk-...

Define the agent#

The agent is the brain. Build it in TanStack factory mode: you own the LLM call with TanStack AI's chat(), and the Built-in Agent turns its stream into the events the channel renders.

agent.ts
import { BuiltInAgent, convertInputToTanStackAI } from "@copilotkit/runtime/v2";
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const SYSTEM_PROMPT = "You are a helpful support assistant. Keep replies short.";

export const agent = new BuiltInAgent({
  type: "tanstack",
  factory: (ctx) => {
    // Turn the run input into TanStack AI messages, system prompts, and the
    // channel tools forwarded on this turn.
    const { messages, systemPrompts, tools } = convertInputToTanStackAI(ctx.input);

    return chat({
      adapter: openaiText("gpt-5.5"),
      messages,
      systemPrompts: [SYSTEM_PROMPT, ...systemPrompts],
      tools,
      abortController: ctx.abortController,
    });
  },
});

Write the bot#

bot.ts
import { createChannel } from "@copilotkit/channels";
import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { agent } from "./agent";

// A managed channel: Intelligence hosts the platform transport, so it takes no
// adapter. The name must match the channel you created in the dashboard.
const channel = createChannel({
  name: "support-bot",
  agent,
});

// Intelligence only delivers turns your bot should answer (a mention or a
// DM), so run the agent on each delivered message.
channel.onMessage(async ({ thread, message }) => {
  await thread.runAgent({ prompt: message.text });
});

// The runtime owns the channel. Declare it here, mount the listener, and
// `ready()` activates it. The Intelligence key (free tier available) is what
// lets the runtime run any channel.
const apiUrl = process.env.COPILOTKIT_INTELLIGENCE_URL!;
const intelligence = new CopilotKitIntelligence({
  apiUrl,
  wsUrl: apiUrl.replace(/^http/, "ws"), // ws base derives from the api base
  apiKey: process.env.COPILOTKIT_API_KEY!,
});

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

const listener = createCopilotNodeListener({ runtime });
await listener.channels?.ready();
console.log("bot listening");

Run it#

npx tsx bot.ts

Message the bot in your chat app. It replies in the thread.

Keep the process alive and shut down cleanly

The listener is a request handler, so to hold the lifecycle-owning process open mount it on a server, and stop the channels on shutdown:

import { createServer } from "node:http";

createServer(listener).listen(3000);

const shutdown = async () => {
  await listener.channels?.stop();
  process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

What thread gives you#

onMessage hands you a thread and the incoming message.

channel.onMessage(async ({ thread, message }) => {
  message.text;        // the user's text
  message.user;        // { id, name?, handle?, email? }
  thread.platform;     // "slack" | "teams" | ...

  await thread.post("a plain text reply");
  await thread.runAgent({ prompt: message.text });
});

thread.runAgent runs your agent and streams its reply into the thread. thread.post sends a message yourself, without the agent.

Next steps#