Everything beyond the quick start: the full Slack app walkthrough, the complete environment reference, running standalone vs. from the monorepo, the Intelligence Gateway channel mode, wiring up Linear / Notion / inline charts, the other chat platforms, slash commands, tests, and how the pieces fit together.
- How it fits together
- Running it — monorepo or standalone, self-hosted or Intelligence Gateway
- Deep research (LangGraph deep agent)
- Intelligence channel mode
- 1. Create a Slack app
- 2. Environment variables
- 3. Integrations — Linear, Notion, charts
- Other platforms — Discord, Telegram, WhatsApp
- Slash commands
- Files → charts, diagrams & tables
- Tests
Slack / Discord / Telegram / WhatsApp ──@mention──▶ KiteBot (app/) ──AG-UI──▶ runtime (runtime.ts)
│ BuiltInAgent (LLM)
├── Linear MCP (hosted)
└── Notion MCP (sidecar)
Three moving parts: the chat-platform app(s) in app/, the agent (runtime.ts), and —
if you use Notion — a small Notion MCP sidecar. KiteBot speaks to the agent over
AG-UI; the agent is one CopilotKit BuiltInAgent (an LLM plus
optional MCP tools — no Python, no LangGraph).
KiteBot runs in one of two modes: self-hosted (pnpm dev → app/index.ts, holds the Slack
tokens directly) or Intelligence Gateway (pnpm channel → app/managed.ts, over the
CopilotKit Intelligence Realtime Gateway — see Intelligence channel
mode). Both modes talk to the same agent backend
(pnpm runtime → runtime.ts) via AGENT_URL.
| Concept | Where |
|---|---|
createBot({ adapters, agent, tools, context, commands }) |
app/index.ts |
| Multi-adapter wiring (Slack/Discord/Telegram/WhatsApp, secret-gated) | app/index.ts |
read_thread — grounds the agent in the real conversation |
app/tools/read-thread.ts |
| Render-tools + JSX components (issue card/list, Notion pages) | app/tools/render-tools.tsx, app/components/ |
| Chart / diagram rendering (Playwright → PNG) | app/tools/render-chart.tsx, render-diagram.tsx, app/render/ |
Table rendering (native <Table> block, monospace fallback) |
app/tools/render-table.tsx |
| Status / incident / links showcase cards | app/tools/showcase-tools.tsx, app/components/_status.ts |
Blocking human-in-the-loop gate (confirm_write) |
app/human-in-the-loop/confirm-write.tsx |
Slash commands (/agent, /triage, /preview, /file-issue) |
app/commands/index.ts |
A Block Kit modal (/file-issue) |
app/modals/file-issue.tsx |
The agent backend — one BuiltInAgent (LLM + Linear/Notion MCP) |
runtime.ts |
app/is the platform-agnostic KiteBot code. This is the directory you copy to start your own bot.runtime.tsis the agent backend, served over AG-UI.e2e/holds live test harnesses (the Slack harness is being migrated to the newcreateBotAPI; the Telegram harness is a working manual-trigger smoke test — seee2e/TELEGRAM-README.md).
It's built on:
@copilotkit/channels— the platform-agnostic bot engine.@copilotkit/channels-slack/-discord/-telegram/-whatsapp— the platform adapters.@copilotkit/channels-ui— a cross-platform JSX vocabulary for rich messages (Block Kit on Slack, Components V2 on Discord, HTML on Telegram).@copilotkit/channels-intelligence— runs the same KiteBot over the CopilotKit Intelligence Realtime Gateway (Intelligence Gateway mode, no platform tokens in this process).@copilotkit/runtime— the AG-UI agent backend.
If you're working inside the CopilotKit monorepo,
this code runs there as examples/slack, whose package.json name is slack-example (this
standalone repo's package.json name is opentag — the --filter below only resolves inside
the monorepo), building the @copilotkit/channels* adapters from source:
pnpm install # repo root
pnpm --filter slack-example notion-mcp # only if using Notion → http://127.0.0.1:3001/mcp
pnpm --filter slack-example runtime # CopilotKit runtime on :8200, agent "triage"
pnpm --filter slack-example dev # KiteBot (tsx watch app/index.ts)The @copilotkit/channels* packages are published on npm, so a plain npm install here works
as-is — no monorepo required:
npm install
npm run notion-mcp # terminal 1 — only if using Notion
npm run runtime # terminal 2 — the agent backend on :8200
npm run dev # terminal 3 — KiteBot, self-hosted (holds the Slack tokens)The chart/diagram renderers need a Chromium binary: npx playwright install chromium.
agent/ is an alternative agent backend to runtime.ts: a Python
deepagents (LangGraph) planner with a virtual
filesystem and OPTIONAL Tavily web research, served over AG-UI on :8123. Instead of
runtime.ts's single system-prompted BuiltInAgent call, it plans with write_todos,
reads/writes its own virtual files, and — when configured — researches the web before
synthesizing an answer, all while still calling KiteBot's forwarded generative-UI tools the same
way the TS runtime does.
Setup — requires uv and Python 3.12:
cd agent && uv syncCopy agent/.env.example to agent/.env and fill it in:
| Variable | What it's for |
|---|---|
OPENAI_API_KEY |
Required — the model. |
TAVILY_API_KEY |
Optional — turns on live web research. Without it the agent still chats and generates UI components, answering from its own knowledge. |
OPENAI_MODEL |
Defaults to gpt-5.5, matching the rest of OpenTag. |
SERVER_HOST / SERVER_PORT |
Defaults to 0.0.0.0:8123. |
Run it:
pnpm agent # cd agent && uv run python main.py (port from SERVER_PORT/PORT env, default 8123)Then point the bot at it instead of runtime.ts by setting in the root .env:
AGENT_URL=http://localhost:8123/With the deep agent in the mix, a local setup is three processes: pnpm agent (the Python
deep-research brain, :8123), the bot (pnpm channel or pnpm dev), and — if you're using
runtime.ts instead — pnpm runtime (:8200). agent and runtime are two alternative brains
for the same bot; run whichever one AGENT_URL points at.
pnpm channel (app/managed.ts) runs the same KiteBot over the CopilotKit Intelligence
Realtime Gateway instead of a native platform adapter — this process holds no Slack tokens;
Intelligence owns the Slack edge (signed ingress + Connector Outbox egress) and streams render
frames back over @copilotkit/channels-intelligence. It's the Intelligence Gateway counterpart to
the self-hosted pnpm dev mode described above — you still run this process yourself and bring
your own CopilotKit Intelligence project.
npm run runtime # terminal 1 — the agent backend on :8200 (same as self-hosted)
npm run channel # terminal 2 — the Intelligence Gateway KiteBot (tsx app/managed.ts)Configure it with:
| Variable | What it's for |
|---|---|
INTELLIGENCE_GATEWAY_WS_URL |
The Intelligence Realtime Gateway websocket endpoint. |
INTELLIGENCE_API_KEY |
Auth for the gateway connection. |
INTELLIGENCE_ORG_ID / INTELLIGENCE_PROJECT_ID / INTELLIGENCE_CHANNEL_ID |
Scopes the connection to your Intelligence org/project/channel. |
INTELLIGENCE_CHANNEL_NAME |
The registered channel name (lowercase kebab). Defaults to kitebot. |
The agent backend is still required in this mode — pnpm runtime (runtime.ts) — the Intelligence
channel host points its AGENT_URL at it exactly like the self-hosted KiteBot does. AGENT_URL
itself is required in every mode (the process exits at startup if it's unset); .env.example ships
it pre-filled with the local runtime URL as a template, not as a code-level default. See
.env.example for the full annotated list.
- Go to https://api.slack.com/apps?new_app=1 → From a manifest → paste
slack-app-manifest.yaml. The manifest declares all four slash commands, the assistant pane, theusers:read.emailscope, and Socket Mode (so KiteBot connects outbound — no public URL needed). - OAuth & Permissions → Install to Workspace → copy the
xoxb-Bot User OAuth Token → this is yourSLACK_BOT_TOKEN. - Basic Information → App-Level Tokens → generate one with the
connections:writescope → copy thexapp-token → this is yourSLACK_APP_TOKEN.
(Discord, Telegram, and WhatsApp setup is documented inline in .env.example
and summarized under Other platforms.)
Copy the template and fill in the platform(s) and integrations you want — KiteBot starts an
adapter for each platform whose secrets are present, and the agent wires up whichever data
sources have credentials. (Running in Intelligence channel mode
instead uses the INTELLIGENCE_* variables in place of the platform tokens below.)
cp .env.example .env| Variable | What it's for |
|---|---|
SLACK_BOT_TOKEN / SLACK_APP_TOKEN |
Run on Slack (see step 1). |
OPENAI_API_KEY |
The model. Required — the runtime is OpenAI-only (it runs on the OpenAI Responses API, needed for web_search); ANTHROPIC_API_KEY / GOOGLE_API_KEY are not read by this runtime. |
AGENT_MODEL |
OpenAI model id override, optionally prefixed openai/ (stripped). Defaults to openai/gpt-5.5. |
LINEAR_API_KEY / LINEAR_TEAM_KEY |
Wire up Linear (linear.app → Settings → API → Personal API keys). |
NOTION_TOKEN / NOTION_MCP_AUTH_TOKEN |
Wire up Notion (see Notion). |
DISCORD_BOT_TOKEN / DISCORD_APP_ID |
Run on Discord. |
TELEGRAM_BOT_TOKEN |
Run on Telegram. |
WHATSAPP_ACCESS_TOKEN (+ siblings) |
Run on WhatsApp Cloud API. |
INTELLIGENCE_GATEWAY_WS_URL / INTELLIGENCE_API_KEY / INTELLIGENCE_ORG_ID / INTELLIGENCE_PROJECT_ID / INTELLIGENCE_CHANNEL_ID / INTELLIGENCE_CHANNEL_NAME |
Run in Intelligence channel mode instead of holding platform tokens directly. |
AGENT_URL |
Where KiteBot POSTs. Required — the process exits at startup if unset; the template value points at the local runtime (…/agent/triage/run). |
Every integration is independent — set only what you need. The full annotated list, including the
WhatsApp webhook details, is in .env.example.
The hosted Linear MCP accepts a raw API key as a bearer token (no OAuth dance). Create one at
linear.app → Settings → API → Personal API keys, set LINEAR_API_KEY, and optionally
LINEAR_TEAM_KEY (the default team to file/query against). Leave LINEAR_API_KEY blank to run
without Linear. With it set, the agent can:
- Query Linear — "what's open in CPK this cycle?" → renders the issues as a rich card.
- File a Linear issue — "file this thread as a bug" → drafts it, asks you to confirm, then creates it.
Notion runs as a small Streamable-HTTP sidecar wrapping the official
@notionhq/notion-mcp-server. Start
it with pnpm notion-mcp (or npm run notion-mcp).
NOTION_TOKEN— the Notion integration secret the sidecar uses to call the Notion API (notion.so → Settings → Connections → develop integrations).NOTION_MCP_AUTH_TOKEN— a bearer the sidecar requires on its HTTP transport; pick any strong string and set the same value here and when starting the sidecar. Leave it blank to run without Notion.
With it set, the agent can find pages ("find the runbook for the auth outage") and write a postmortem ("write this thread up as a Notion doc" → reads, summarizes, confirms, then creates the page).
Every write — Linear or Notion — goes through a blocking confirm_write gate: the agent must
call that tool and wait for a Create / Cancel click before it performs the write. See
app/human-in-the-loop/confirm-write.tsx.
The chart/diagram libraries load from a CDN into a local headless browser (override
CHART_JS_URL / MERMAID_URL) — your data is rendered locally and never sent to a rendering
service. Requires a Chromium binary: npx playwright install chromium.
The same app/ code runs on every platform — createBot takes an array of adapters, and
app/index.ts starts one for each platform whose secrets are present. Everything else (tools,
components, the HITL gate, rendering) is shared verbatim.
- Discord — set
DISCORD_BOT_TOKEN+DISCORD_APP_ID(and optionallyDISCORD_GUILD_IDfor instant slash-command registration in dev). Enable the Message Content and Server Members privileged intents. - Telegram — message @BotFather →
/newbot→ setTELEGRAM_BOT_TOKEN. Long-polling is the default ingress (no public URL needed). - WhatsApp — set
WHATSAPP_ACCESS_TOKEN+ siblings from your Meta App → WhatsApp → API Setup. The server listens on$PORTfor the webhook.
Per-platform details are documented inline in .env.example.
Four app-owned commands, registered via createBot({ commands })
(app/commands/index.ts):
/agent <text>— a mention-free entry point; runs the agent with the command text./triage [note]— summarizes the conversation and proposes issues to file./preview <title>— privately previews the issue KiteBot would file (only you see it); degrades to a DM where ephemerals aren't supported./file-issue— opens a structured issue modal; degrades to a conversational flow on platforms without modals (e.g. Telegram).
On Slack, all four must be declared under Slash Commands — the manifest already does this.
Upload a file and KiteBot analyzes it: images and PDFs go straight to the model; CSV/JSON/text are decoded and handed over as text. Then ask it to visualize:
chart revenue by month · diagram this incident flow · show it as a table
PDFs and images need a vision/document-capable model. The runtime is OpenAI-only, and the default
openai/gpt-5.5reads both natively. If you overrideAGENT_MODEL, pick another vision/document-capable OpenAI model — non-OpenAI model ids (Claude, Gemini, etc.) are not supported by this runtime.
npm test # unit: read_thread, render tools, components, confirm_write, modals, commands
npm run check-types # tsc --noEmitThe live-Slack e2e harness (npm run e2e) is being migrated to the new createBot API and
doesn't run against this code as-is. The Telegram harness (npm run e2e:telegram) is a working
manual-trigger smoke test — see e2e/TELEGRAM-README.md.