{"slug": "channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams", "title": "Channels SDK: How to bring Your Agent to Any Channel (Slack, Microsoft Teams)", "summary": "CopilotKit has released the Channels SDK, an open-source TypeScript library that brings any AG-UI agent to messaging platforms like Slack, Microsoft Teams, Discord, and Telegram from a single codebase. The SDK includes a persistence layer for memory and context, and supports managed production deployments via CopilotKit Intelligence, eliminating the need to expose public URLs. The project demonstrates integrating an agent into Slack with native rendering through JSX components and MCP tool connections.", "body_md": "Getting any agent into messaging platforms like Slack is complex. You create a Slack app, handle its events, format messages to the platform's format, handle deliveries with retries, and so on.\n\nTaking that to production is much harder. Then, if you want to bring it into another messaging platform, you do most of the work again, and again for the next platform.\n\nTo remove that manual work, we're excited to release the [Channels SDK](https://github.com/copilotKit/channels-sdk), which brings any [AG-UI](https://github.com/ag-ui-protocol/ag-ui) agent to any channel from one codebase, with a persistence layer so it keeps its memory and context.\n\nToday, we will bring an agent into Slack, learn how everything works along with the architecture, the core patterns and connecting it to real tools like Notion using MCP.\n\n```\nnpx copilotkit@latest channels setup\n```\n\nTo try the agent live, we have created public channels on Slack and MS Teams. Head to\n\n[copilotkit.ai/try-channels]and we'll get you in.\n\nA Slack bot running on your own agent, that can:\n\nPrefer a complete working example? [Clone OpenTag on GitHub](https://github.com/CopilotKit/OpenTag).\n\nIf you want to explore on your own, [read the Channels docs](https://docs.copilotkit.ai/channels), or copy a ready-made prompt from the [docs homepage](https://docs.copilotkit.ai) and your coding agent builds your first channel with you.\n\nThe Channels SDK is an open source TypeScript library that takes any [AG-UI](https://github.com/ag-ui-protocol/ag-ui) agent into Slack, Microsoft Teams, Discord, Telegram, WhatsApp and other channels, from one codebase.\n\n```\nnpm i @copilotkit/channels\n```\n\nIt's built on the [AG-UI protocol](https://github.com/ag-ui-protocol/ag-ui), so the app and the agent stay decoupled.\n\nYou can bring any AG-UI compatible agent framework (LangChain, Google ADK, Mastra, Pydantic AI, Claude Agent SDK, etc.) and swap it later without touching the channel code.\n\nAnd if you don't want to bring your own framework, we ship a BuiltInAgent that runs on any model, hosted or local (via Ollama, LM Studio, or vLLM). We will be using this for the setup.\n\nYou write replies as `<Message>`\n\nJSX, and each platform renders them natively: Block Kit in Slack, Adaptive Cards in Teams.\n\nGetting an agent to your channels usually involves:\n\nThat's the direct path, and we do ship adapters for it. But to run in production, we give you managed Slack and Microsoft Teams today with [CopilotKit Intelligence](https://intelligence.copilotkit.ai/).\n\nIt holds the connection and delivers each turn to your app, so your process just runs the agent, with no public URL to expose, and adding a platform is a click in the dashboard.\n\nThis guide uses the managed path, since it's the one you run in production. The integration code is mostly identical either way.\n\nThe diagram below shows the boundary between your app and CopilotKit Intelligence, and the path each message takes.\n\nThere's a [component library](https://docs.copilotkit.ai/reference/channels) with the building blocks: Message, Section, Header, Table, Image, Chart, Actions, Button, and more to help you compose UI blocks easily.\n\nPut a few together and you get a rich reply, rendered natively for you.\n\n``` js\nimport { Message, Header, Section, Context } from \"@copilotkit/channels/ui\";\n\n<Message>\n  <Header>Deploy complete</Header>\n  <Section>v1.4.2 is live in production.</Section>\n  <Context>Shipped by the deploy bot · 12:04 PM</Context>\n</Message>\n```\n\nYou'll need Node.js 22+, a Slack workspace where you can install apps, a free [CopilotKit Intelligence](https://intelligence.copilotkit.ai) key, and a model API key like OpenAI.\n\nHere is the project structure. As we add capabilities below (tools, cards, MCP), we will do it in `components.tsx`\n\nfor the cards and buttons, and `mcp.ts`\n\nfor the Notion client. Everything else stays as is.\n\n```\nslack-bot/\n├── agent.ts        # BuiltInAgent + your model\n├── channel.tsx     # the app\n├── tsconfig.json   # JSX transform pointed at @copilotkit/channels\n├── package.json  \n└── .env            # INTELLIGENCE_API_KEY, ...\n```\n\nIn the Intelligence dashboard, create a channel, choose the platform and set its name like `support-slack`\n\n. Your app declares this exact name later.\n\nIntelligence generates a Slack app manifest. Create the Slack app from it at [api.slack.com/apps](https://api.slack.com/apps), install it, and paste two values back into Intelligence:\n\n`xoxb-…`\n\n), from Both are required. The bot token lets Intelligence post as your app and the signing secret lets it verify Slack's events. Without the secret, events are never delivered.\n\nOnce you create the channel, the dashboard will look like this.\n\nThe manifest you installed already turns these on, so there's nothing to add. It's worth knowing what they do, since the features later in this guide depend on them.\n\nIf you ever build the Slack app manually instead of from the manifest, this is where you'd enable those, then reinstall the Slack app in your workspace.\n\nThe built-in agent runs inside your app, so there's no second server to run. Here's the code.\n\n``` js\n// agent.ts\nimport { BuiltInAgent } from \"@copilotkit/runtime/v2\";\n\nexport function makeAgent(threadId: string) {\n  const agent = new BuiltInAgent({ model: \"openai/gpt-5.5\" });\n  agent.threadId = threadId;\n  return agent;\n}\n```\n\nThe `model`\n\nstring is `provider/model`\n\n. It works with OpenAI, Anthropic, and Google out of the box, and with any OpenAI-compatible endpoint, so you can point it at a local model (Ollama, a self-hosted gateway) instead of a hosted one. See [Model Selection](https://docs.copilotkit.ai/model-selection) for the local and custom-provider setup.\n\nInitialize the project and install the SDK, plus the dev tools to run TypeScript.\n\n```\nnpm init -y && npm pkg set type=module\nnpm install @copilotkit/channels @copilotkit/runtime\nnpm install -D tsx typescript @types/node dotenv\n```\n\nChannels JSX does not use React. Point the JSX transform at `@copilotkit/channels`\n\nso `<Message>`\n\nand `<Button>`\n\nbecome type-checked Slack UI instead of React elements. Put JSX in `.tsx`\n\nfiles and include them.\n\n```\n// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@copilotkit/channels\",\n    \"strict\": true,\n    \"noEmit\": true\n  },\n  \"include\": [\"*.ts\", \"*.tsx\"]\n}\n```\n\nNote: If a component throws \"React is not defined,\" it's because the JSX fell back to React. The `jsxImportSource`\n\nline above is what prevents it.\n\nCreate a `.env`\n\nin the root and add your API keys. You can create a [free CopilotKit Intelligence key](https://intelligence.copilotkit.ai/) from the dashboard.\n\n```\nINTELLIGENCE_API_KEY=cpk-…\nINTELLIGENCE_CHANNEL_NAME=toothless\nOPENAI_API_KEY=sk-…\nPORT=3000\n```\n\nThe app ties the two sides together: `createChannel`\n\nsets it up, one `onMessage`\n\nhandler runs the agent, and a listener keeps the process alive.\n\nHere's what the code does:\n\n`createChannel`\n\ndeclares the bot, which channel, which agent, and `store`\n\n.\n\n`onMessage`\n\nruns the agent on each message and streams the reply back to the thread.\n\nThe runtime and listener connect to Intelligence with your API key and keep the process alive.\n\n``` js\n// channel.tsx\nimport \"dotenv/config\";\nimport { createServer } from \"node:http\";\nimport { createChannel } from \"@copilotkit/channels\";\nimport { CopilotRuntime, CopilotKitIntelligence } from \"@copilotkit/runtime/v2\";\nimport { createCopilotNodeListener } from \"@copilotkit/runtime/v2/node\";\nimport { makeAgent } from \"./agent\";\n\nconst channel = createChannel({\n  name: process.env.INTELLIGENCE_CHANNEL_NAME!, // the channel Code from step 1\n  identifyUser: \"platform\",\n  agent: makeAgent,\n  // Drop duplicate deliveries so a Slack retry can't double-run the agent.\n  store: { concurrency: \"drop\", lockTtl: 60_000, dedupTtl: 300_000 },\n});\n\nchannel.onMessage(async ({ thread, message }) => {\n  await thread.runAgent({ prompt: message.text });\n});\n\nconst intelligence = new CopilotKitIntelligence({ apiKey: process.env.INTELLIGENCE_API_KEY! });\nconst runtime = new CopilotRuntime({ agents: {}, intelligence, channels: [channel] });\nconst listener = createCopilotNodeListener({ runtime, basePath: \"/api/copilotkit\" });\n\nawait listener.channels?.ready({ timeoutMs: 30_000 });\ncreateServer(listener).listen(Number(process.env.PORT ?? 3000));\nconsole.log(\"Channel online.\");\n```\n\nSlack retries any message it doesn't get a quick reply for, and without a `store`\n\nthose retries turn into duplicate runs. The store keeps one run per conversation and drops the repeats.\n\nThe agent runs inside the app, so one command starts everything.\n\n``` python\nnode --env-file=.env --import tsx channel.tsx\n```\n\nIf you have followed the steps, the channel status should change from \"Waiting for runtime\" to \"Online\".\n\nInvite the app to a channel and mention it. It should reply in threads accordingly.\n\n```\n/invite @your-app\n@your-app what can you help with?\n```\n\nThat's the whole setup. Slack application code on one side, your agent on the other.\n\nNow let's make the agent actually useful by implementing Generative UI, Human-in-the-loop, MCP Tools, Threads and more.\n\nGenerative UI lets your Slack agent create interfaces on the fly. Instead of answering in text, it builds what the moment needs and posts it into the thread.\n\nNative cards make it possible. You write them as JSX from `@copilotkit/channels/ui`\n\n, and they render as real, interactive Slack UI: headers, sections, buttons, and more.\n\n``` js\n/** @jsxImportSource @copilotkit/channels */\nimport { Message, Header, Section, Context } from \"@copilotkit/channels/ui\";\n\nexport function TaskCard({ task, owner, priority, status }: TaskRow) {\n  return (\n    <Message>\n      <Header>{`🔴 ${task}`}</Header>\n      <Section>{`*${priority}*   ·   ${status}`}</Section>\n      <Context>{`👤 ${owner}`}</Context>\n    </Message>\n  );\n}\n```\n\nNative components cover more than text. Alongside cards, you get charts, tables, and images, all rendered natively in Slack. Here's a chart:\n\n``` js\n/** @jsxImportSource @copilotkit/channels */\nimport { Chart } from \"@copilotkit/channels/ui\";\n\nexport function CloseTimeChart({ rows }: { rows: { area: string; avgDays: number }[] }) {\n  return (\n    <Chart\n      type=\"verticalBar\"\n      title=\"Avg days to close, by area\"\n      xAxisTitle=\"Area\"\n      yAxisTitle=\"Days\"\n      data={rows.map((r) => ({ label: r.area, value: r.avgDays }))}\n    />\n  );\n}\n```\n\nThe agent fills the data from the conversation, so \"chart close times by area\" becomes real bars in the thread.\n\n**An image** is for what native components can't draw, a flowchart of what the team just brainstormed, or a fully styled widget. You render it to a PNG and post it as a file, and Intelligence hosts it.\n\n``` js\nconst png = await renderFlowchart(triage); // mermaid or HTML → PNG\nawait thread.postFile({\n  bytes: png,\n  filename: \"flow.png\",\n  title: \"Docs issue backlog triage flow\",\n  altText: \"Triage path from raw issue state to the main backlog risk\",\n});\n```\n\nRead more on the [Rich messages docs](https://docs.copilotkit.ai/slack/rich-messages).\n\nAn agent with only a model can answer from what it knows. A tool lets it call your code, hit an API, run a search, read from your database, and use the result in its reply.\n\nYou define one with `defineChannelTool`\n\n: the `parameters`\n\nschema becomes the tool's input, and your `handler`\n\nruns when the agent calls it.\n\nHere's the trimmed snippet showing the flow - you can use Tavily or another suitable provider for web search.\n\n``` js\n// tools.ts\nimport { defineChannelTool } from \"@copilotkit/channels\";\nimport { z } from \"zod\";\n\nexport const webSearch = defineChannelTool({\n  name: \"web_search\",\n  description: \"Search the web and return the top results.\",\n  parameters: z.object({\n    query: z.string().describe(\"What to search for\"),\n  }),\n  async handler({ query }) {\n    const results = await search(query); // your search call, e.g. Tavily\n    return results; // objects are serialized for the model\n  },\n});\n```\n\nRegister it on the channel:\n\n``` js\nconst channel = createChannel({\n  name: process.env.INTELLIGENCE_CHANNEL_NAME!,\n  identifyUser: \"platform\",\n  agent: makeAgent,\n  tools: [webSearch],\n});\n```\n\nNow \"search for the latest Anthropic Claude news and summarize it\" runs your `web_search`\n\n, reads the results, and answers in the thread with that context.\n\nA few rules of thumb from the API: return raw objects or arrays for data tools, return a short confirmation when the handler already posted UI, and throw the real error so the agent can recover, avoid `JSON.stringify`\n\non your own success data.\n\nRead [Tools and context docs](https://docs.copilotkit.ai/slack/tools).\n\n`defineChannelTool`\n\nis for actions you write yourself. To give the agent a whole set of tools something else already exposes, connect an MCP server, and it can search a knowledge base, create a ticket, or update a page.\n\nLet's connect Notion MCP for instance.\n\n`ntn_…`\n\n).Add the token to `.env`\n\n.\n\n```\nNOTION_TOKEN=ntn_...\n```\n\nThen wire the Notion MCP server as one of the agent's MCP clients. It runs as a local process (`npx @notionhq/notion-mcp-server`\n\n) and hands the agent Notion's tools: search, fetch, create, update.\n\n``` js\nimport { BuiltInAgent } from \"@copilotkit/runtime/v2\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  StdioClientTransport,\n  getDefaultEnvironment,\n} from \"@modelcontextprotocol/sdk/client/stdio.js\";\n\nfunction notionMcp() {\n  const client = new Client({ name: \"my-bot\", version: \"0.1.0\" }, { capabilities: {} });\n  return {\n    async tools() {\n      await client.connect(\n        new StdioClientTransport({\n          command: \"npx\",\n          args: [\"-y\", \"@notionhq/notion-mcp-server\"],\n          // Forward env, or NOTION_TOKEN never reaches the spawned server.\n          env: { ...getDefaultEnvironment(), ...process.env } as Record<string, string>,\n        }),\n      );\n      const { tools } = await client.listTools();\n      // Expose each MCP tool to the agent (full mapping in the repo's mcp.ts).\n      return toToolSet(client, tools);\n    },\n  };\n}\n\nexport function makeAgent(threadId: string) {\n  const agent = new BuiltInAgent({\n    model: \"openai/gpt-5.5\",\n    maxSteps: 10,          // let it call a tool, read the result, then answer\n    mcpClients: [notionMcp()],\n  });\n  agent.threadId = threadId;\n  return agent;\n}\n```\n\nNow \"what's in my Q3 roadmap?\" searches Notion and answers from the real page. If the page isn't connected to the integration, the agent says it can't find it instead of guessing. You can enable generative UI as well if you want to reply in a structured way.\n\nHere's an example of fetching Linear tickets, with a similar pattern.\n\nSome actions shouldn't happen without a person approving them. For those critical actions, the agent can do it via human-in-the-loop.\n\nYou author the prompt as a card with two buttons, and the click carries your own value back to the handler.\n\n``` js\n/** @jsxImportSource @copilotkit/channels */\nimport { Message, Header, Actions, Button, type InteractionContext } from \"@copilotkit/channels/ui\";\n\n// The click runs in your runtime; the platform only sends back the button's value.\nasync function onDecision({ action, thread }: InteractionContext<{ ok: boolean; what: string }>) {\n  const v = action.value;\n  if (!v) return; // the click value can be undefined, so guard it first\n\n  await thread.post(v.ok ? `Approved: ${v.what}` : \"Cancelled.\");\n  if (v.ok) {\n    await thread.runAgent({ prompt: `The user approved \"${v.what}\". Do it now, then confirm.` });\n  }\n}\n\nexport function ConfirmAction({ what }: { what: string }) {\n  return (\n    <Message>\n      <Header>{`Approve: ${what}`}</Header>\n      <Actions>\n        <Button value={{ ok: true, what }} style=\"primary\" onClick={onDecision}>Approve</Button>\n        <Button value={{ ok: false, what }} style=\"danger\" onClick={onDecision}>Deny</Button>\n      </Actions>\n    </Message>\n  );\n}\n```\n\nTell the agent to ask for approval before any write. It posts the prompt, pauses, and continues only once approved. Read more on the [Interactive messages docs](https://docs.copilotkit.ai/slack/interactive).\n\nYou can also add more context using the `what`\n\nfield.\n\nPeople drop files into Slack, screenshots, PDFs, logs. The agent forwards what the message carries into the run and the attachment goes with the text.\n\n``` js\nchannel.onMessage(async ({ thread, message }) => {\n  await thread.runAgent({\n    prompt: message.contentParts?.length\n      ? [{ type: \"text\" as const, text: message.text }, ...(message.contentParts ?? [])]\n      : message.text,\n  });\n});\n```\n\nHere's an example of an image attached and the agent responds accordingly.\n\nThe agent remembers in two ways.\n\nTranscripts are keyed to a user, so you give the channel a stable identity, then turn transcripts on in the store.\n\n``` js\nconst channel = createChannel({\n  name: process.env.INTELLIGENCE_CHANNEL_NAME!,\n  identifyUser: ({ actor }) =>\n    actor.email ? { id: actor.email, name: actor.name ?? actor.email } : null, // needs users:read.email\n  agent: makeAgent,\n  store: {\n    concurrency: \"drop\", lockTtl: 60_000, dedupTtl: 300_000,\n    adapter: new MyStore(), // Redis, Postgres\n    transcripts: { retention: \"30d\", maxPerUser: 200 },\n  },\n});\n```\n\nThen call `thread.runAgent({ prompt, transcript: true })`\n\n, and prior history is injected when the thread has a resolved user.\n\nTranscripts and pending approvals live in this store, separate from the thread Intelligence keeps. In development, it's in memory and resets on restart, so for production use a [durable store like Redis or Postgres](https://docs.copilotkit.ai/slack/persistence-and-scaling).\n\nYay! Now your agent is very powerful and can do a lot of tasks, with the context of your channel.\n\nFrom the Intelligence dashboard, you can see all the threads, usage history, runs and complete agent lifecycle per thread via AG-UI events.\n\nThe built-in agent is one option. You can swap it for a major framework, LangGraph, Mastra, the Claude Agent SDK, anything that speaks AG-UI over HTTP.\n\nYou serve that agent at a URL, and the app points at it instead. Nothing else in your bot changes.\n\nHere's what changes if you use LangGraph: only the agent file. Instead of constructing a `BuiltInAgent`\n\n, you point an `HttpAgent`\n\nat your running LangGraph server:\n\n``` js\n// agent.ts\nimport { HttpAgent } from \"@ag-ui/client\";\n\n// Before: the built-in agent\n// export function makeAgent(threadId: string) {\n//   const agent = new BuiltInAgent({ model: \"openai/gpt-5.5\" });\n//   agent.threadId = threadId;\n//   return agent;\n// }\n\n// After: your LangGraph graph, served over AG-UI at AGENT_URL\nexport function makeAgent(threadId: string) {\n  const agent = new HttpAgent({ url: process.env.AGENT_URL! });\n  agent.threadId = threadId;\n  return agent;\n}\n```\n\nThen add `AGENT_URL=http://localhost:8000/agent`\n\nto `.env`\n\n. Your LangGraph graph runs as its own process, with its own model and tools, and the app talks to it over AG-UI.\n\nEverything else stays the same, the tools, cards, approvals, and memory are all defined on `createChannel`\n\n, not the agent, so they carry over untouched.\n\nYour channel tools and approval cards get forwarded to the agent over AG-UI, and transcripts still follow the user. You wrote it once against the built-in agent, and it keeps working when you bring any other agent framework.\n\nEverything above runs from your machine.\n\nTo ship it, deploy the same app as a long-running worker (a container or VM, not a serverless function), since it holds an outbound real-time connection to Intelligence.\n\nIn production, you'll also want a health check that reports ready only when the connection is truly live, and a clean shutdown that releases the session. The [deploy and operate guide](https://docs.copilotkit.ai/slack/deploy-and-operate) covers both.\n\nGo bring your agent into Slack and Microsoft Teams.\n\nTo try the agents live in Slack and Microsoft Teams, we've set up public channels, head to [copilotkit.ai/try-channels](https://www.copilotkit.ai/try-channels) and we'll get you in.\n\nConnect with me on [GitHub](https://github.com/Anmol-Baranwal), [Twitter](https://x.com/Anmol_Codes), [LinkedIn](https://www.linkedin.com/in/Anmol-Baranwal/). thanks for reading!\n\nFollow CopilotKit on [Twitter](https://go.copilotkit.ai/socials-twitter). If you get stuck, reach out to the team on the [CopilotKit](https://go.copilotkit.ai/discord) and [AG-UI](https://go.copilotkit.ai/AG-UI-Discord) communities.", "url": "https://wpnews.pro/news/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams", "canonical_source": "https://dev.to/anmolbaranwal/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams-1bof", "published_at": "2026-08-04 17:18:52+00:00", "updated_at": "2026-08-04 17:49:01.932007+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["CopilotKit", "Channels SDK", "AG-UI", "Slack", "Microsoft Teams", "CopilotKit Intelligence", "OpenTag", "Notion"], "alternates": {"html": "https://wpnews.pro/news/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams", "markdown": "https://wpnews.pro/news/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams.md", "text": "https://wpnews.pro/news/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams.txt", "jsonld": "https://wpnews.pro/news/channels-sdk-how-to-bring-your-agent-to-any-channel-slack-microsoft-teams.jsonld"}}