# bot-example — on-call triage assistant (Slack, Discord, Telegram &/or WhatsApp) A runnable demo for [`@copilotkit/channels-slack`](../../packages/channels-slack), [`@copilotkit/channels-discord`](../../packages/channels-discord), [`@copilotkit/channels-telegram`](../../packages/channels-telegram), **and** [`@copilotkit/channels-whatsapp`](../../packages/channels-whatsapp): an on-call triage bot that turns incident chatter into tracked work. It's built with [`@copilotkit/channels`](../../packages/channels) (the platform-agnostic bot core), one or more platform adapters, and [`@copilotkit/channels-ui`](../../packages/channels-ui) (a cross-platform JSX vocabulary for rich messages). **One app, any platform — or all at once.** `createBot` takes an array of adapters; `app/index.ts` includes the Slack adapter when `SLACK_*` secrets are present, the Discord adapter when `DISCORD_*` are present, the Telegram adapter when `TELEGRAM_BOT_TOKEN` is present, and the WhatsApp adapter when `WHATSAPP_*` are present. Everything else in `app/` (tools, components, the `confirm_write` HITL gate, chart/diagram/table rendering) is platform-agnostic and shared verbatim — set the secrets for whichever platform(s) you want and run the same process. It connects to **Linear** and **Notion** over MCP and can: - **Query Linear** — _"what's open in CPK this cycle?"_ → renders issues as a rich card (Block Kit on Slack, Components V2 on Discord, HTML on Telegram). - **File a Linear issue** — _"file this thread as a bug"_ → drafts the issue, asks you to **confirm**, then creates it. - **Find Notion pages** — _"find the runbook for the auth outage"_ → renders matching pages with links. - **Write a postmortem** — _"write this thread up as a Notion doc"_ → reads the thread, summarizes, **confirms**, then creates the page. Every write goes through a human-in-the-loop **`confirm_write`** gate: the agent must call that tool and wait for a Create/Cancel click before it performs any Linear/Notion write. ## How it fits together ``` Slack / Discord / Telegram ──@mention──▶ bot (app/) ──AG-UI──▶ runtime (runtime.ts) │ BuiltInAgent (LLM) ├── Linear MCP (hosted) └── Notion MCP (sidecar) ``` - **`app/`** — the platform-agnostic bot: `createBot` + whichever of the `slack()` / `discord()` / `telegram()` adapters have secrets, the `read_thread` / `render_chart` / `render_diagram` / `render_table` tools, the `issue_card` / `issue_list` / `page_list` render-tools, the `confirm_write` HITL gate, and the bot's context. The components emit a cross-platform JSX IR that each adapter renders natively. This is the directory you'd copy to start your own bot. - **`runtime.ts`** — the agent backend: a single CopilotKit `BuiltInAgent` (LLM + Linear/Notion MCP), served over AG-UI. No Python, no LangGraph. - **`e2e/`** — live test harnesses. The Slack harness (`run.ts` / `restart-recovery.ts`, `pnpm e2e`) is _legacy/WIP — see [Tests](#tests)_; the Telegram harness (`telegram-run.ts`, `pnpm e2e:telegram`) is a manual-trigger smoke test — see [`e2e/TELEGRAM-README.md`](e2e/TELEGRAM-README.md). ### The bot (`app/index.ts`) The core shape is `createBot` + one or more adapters, an `onMention` handler, and `start()`. The snippet below is an **abridged, single-platform sketch** — the real `app/index.ts` builds the adapter list from whichever secrets are present (Slack, Discord, and/or Telegram) and adds graceful shutdown; read the file for the full multi-platform wiring: ```ts import { createBot } from "@copilotkit/channels"; import { slack, defaultSlackTools, defaultSlackContext, SanitizingHttpAgent, } from "@copilotkit/channels-slack"; import { appTools } from "./tools/index.js"; import { appContext } from "./context/app-context.js"; const bot = createBot({ adapters: [ slack({ botToken: process.env.SLACK_BOT_TOKEN!, appToken: process.env.SLACK_APP_TOKEN!, respondTo: { directMessages: true, appMentions: { reply: "thread" }, threadReplies: "mentionsOnly", }, }), ], // One AG-UI agent per conversation, pointed at the runtime. agent: (threadId) => { const a = new SanitizingHttpAgent({ url: process.env.AGENT_URL! }); a.threadId = threadId; return a; }, // defaultSlackTools ships universal-Slack tools (e.g. lookup_slack_user // for @-mentions); appTools adds this bot's tools. defaultSlackContext // ships tagging/mrkdwn/thread-model guidance; appContext adds identity + // triage policy. tools: [...defaultSlackTools, ...appTools], context: [...defaultSlackContext, ...appContext], }); // One handler covers explicit @-mentions and normal DMs. // senderContext names the requesting user so the agent acts "as" them. bot.onMention(async ({ thread, message }) => { await thread.runAgent({ context: senderContext(message.user) }); }); await bot.start(); ``` The runnable Slack example keeps DMs and the assistant pane conversational, but channel/private-channel threads require `@Kite` on each follow-up by default. Set `respondTo.threadReplies: "afterBotReply"` to restore legacy behavior where plain replies in a thread can continue after the bot has posted there. ### Tools (`app/tools/index.ts`) The bot's tools are plain `BotTool`s, collected into `appTools` and spread into `createBot({ tools })`. Each handler receives the generic `BotToolContext` (`{ thread, message?, user?, signal?, platform }`) the adapter supplies at call time; tools reach platform power (post, postFile, `thread.getMessages()`, …) via the `thread` methods: - **`read_thread`** — fetches the messages in the current conversation thread so the agent can summarize/act on a real conversation (e.g. "write this thread up as a postmortem") instead of inventing content. - **`render_chart`** — the agent emits a Chart.js config; rendered to a PNG **locally** in a headless browser (reusing the Playwright dep) and posted inline. - **`render_diagram`** — the agent emits Mermaid; rendered to a PNG the same way. - **`render_table`** — the agent emits columns + rows; rendered natively per platform (a Slack Table block, otherwise a monospace fallback). ### UI as JSX components Rich messages are authored as JSX components over the `@copilotkit/channels-ui` vocabulary (``, `
`, `
`, ``, ``, `