An open-source, self-hosted on-call triage assistant for Slack and Microsoft Teams — clone it, customize it, ship your own.
best-channels-demo.mp4
A spreadsheet goes in. A native Slack chart, a Linear issue behind an approval gate, and a cited research brief come back — in the thread, where the work already is.
Channels SDK brings any AG-UI agent into Slack and Microsoft Teams. Its README shows you the pieces. OpenTag is those pieces assembled into something you would actually deploy — and it is built to be taken, not just read.
| Clone it | Customize it | Ship it |
|---|---|---|
| A working triage bot in one quick start: managed Channel, Node runtime, Python LangGraph agent, and native Slack UI. | The agent, the persona, the tools, and the UI are each one file or one directory. Point it at your own agent without touching the Channel lifecycle. | Two Railway services, pinned SDK versions, graceful shutdown, and a live Slack harness — production shape, not demo shape. |
Slack and Microsoft Teams are supported today. Discord, Telegram, and WhatsApp are coming soon.
Every frame below is this repository running in a real Slack workspace.
Prerequisites: Node.js 22+, pnpm, Python 3.12,
uv, a CopilotKit account, an OpenAI API key,
and a Slack workspace you can install an app into.
pnpm installDo this before touching Slack. The Channel is what generates a Slack app manifest already pointed at the right Request URL, so creating the Slack app first means creating the wrong one.
Two ways to do it. Both end in the same place.
npx --yes copilotkit@latest channels setupThat installs a channels-setup skill and copies a one-line prompt to your
clipboard (--no-clipboard prints it instead). Paste it into your agent:
Use the
channels-setupskill to set up a Channel for this project.
The skill is deliberately a pointer rather than a copy of the steps: it fetches its workflow from https://copilotkit.ai/channels-guide.md at run time, so it cannot go stale against the CLI. It covers the whole path — selecting the project, creating and reconciling the Channel, walking you through the Slack console handoff, and proving a real mention gets a reply. It hands every secret back to you; it never asks you to paste one into chat.
It writes .agents/skills/channels-setup and a skills-lock.json, both already
gitignored here. Note that it installs to every coding agent it detects with no
way to narrow the list. To install for one agent only, use the equivalent
command, which takes the same prompt:
npx --yes copilotkit@latest skills onboard --channels --agent claude-codeThe skill will also carry you through the rest of this quick start. One thing to
watch: its workflow is written for a project starting from nothing, so it has
phases for building the agent and writing the Channel runtime. OpenTag already
has both — agent/ and server.ts. Point your agent
at the existing code to verify and run, not to rewrite.
npx --yes copilotkit@latest project selectnpx --yes copilotkit@latest channels add --name open-tag --display-name "OpenTag" --adapter slack --jsonchannels add declares the Channel in .copilotkit/channels.json, creates it
on the server, and returns a JSON envelope with one of three states:
completed— the adapter is attached. Continue to step 3.blocked— a normal pause waiting on you in the Slack console. ReadnextAction: it carries the prefilled manifest link, the exact environment variable names to set, and theresumeCommandto run afterward. This exits 0.failed— stop and read the error code. Do not continue.
--name is a slug and must match INTELLIGENCE_CHANNEL_NAME character for
character. If you are running a fork against your own project, pick your own
name — see Channel names claim deliveries.
For Microsoft Teams, use --adapter teams. Two Teams steps stay yours because
nothing can work around them: granting tenant admin consent, and uploading the
app package through Apps → Manage your apps → Upload an app.
These apply on either path. Follow the CLI's emitted nextAction rather than
remembered Slack steps, and watch for:
- After creating the app from the link, open OAuth & Permissions and choose Reinstall to Workspace. Slack applies the manifest's real scopes only on reinstall.
- Take the Bot User OAuth Token (
xoxb-) from OAuth & Permissions, not the token shown in the app-creation modal, and the Signing Secret from Basic Information → App Credentials. Those two values are all the Slack adapter wants — there is no app-levelxapp-token anywhere on this path. - The signing secret is reissued on reinstall. If auth fails right after a reinstall, suspect a stale stored secret before a missing scope.
cp .env.example .envSet:
OPENAI_API_KEY=sk-...
AGENT_URL=http://localhost:8123/
INTELLIGENCE_API_KEY=cpk-...
INTELLIGENCE_CHANNEL_NAME=open-tagBoth the Node runtime and the Python agent load this one root .env; Railway
supplies the same values as service variables. Tavily, GitHub, PostHog, Linear,
and Notion are optional — see Optional research
sources.
INTELLIGENCE_API_URL and INTELLIGENCE_GATEWAY_WS_URL already default to the
production Intelligence endpoints in
app/env.ts, so leaving them unset is correct.
pnpm devThe predev hook syncs the locked Python environment and installs Playwright's
Chromium. pnpm dev then runs the Python agent with reload enabled and the Node
runtime in watch mode. The runtime waits for its Intelligence connection to
become ready before its HTTP listener accepts traffic.
/invite @OpenTag
Installed in the workspace is not the same as present in a conversation. Slack
emits no app_mention at all for a channel the app is not a member of, so
without this OpenTag looks broken while behaving correctly.
A Channel that installs cleanly and answers nothing is the most expensive failure available here, because it looks finished. Three checks separate the two. Send them from a real human account:
- Mention it.
@OpenTag what changed in the last deploy?— expect a useful, model-backed reply. - Follow up without mentioning it, in that same thread — expect a reply. A mention subscribes the thread; unmentioned messages run the agent only in already-subscribed threads.
- Send an unmentioned message in a fresh conversation — expect silence. A reply here means the trigger rules are wrong.
If any of those fail, in this order:
npx --yes copilotkit@latest channels status --jsonIt reports declaration, source, server, adapter, environment, and lifecycle
diagnostics. Resolve every one. Two of its warnings are expected for OpenTag
and are not faults: it flags INTELLIGENCE_API_URL and
INTELLIGENCE_GATEWAY_WS_URL as unset because
app/env.ts defaults them in code rather than in .env.
LOG_LEVEL=debug pnpm runtimeThe runtime logger defaults to error, while every Channel lifecycle
breadcrumb is emitted at warn — including channel "<name>" requires setup,
the single highest-value diagnostic here. At the default level it is written and
discarded.
Note that the runtime does not hot-reload Channel wiring. After editing a
handler, the agent, or the Channel, restart the process and confirm online
again before retesting. A stale process answering with the old behavior is
indistinguishable from a change that did not work.
Managed delivery is claim-based: two runtimes declaring the same Channel name in the same Intelligence project race per delivery, and the loser silently receives nothing. The tell is a Slack reply your terminal knows nothing about.
INTELLIGENCE_CHANNEL_NAME defaults to open-tag, which is the name the
production deployment uses. Give a local or forked runtime its own Intelligence
project, its own API key, and its own Channel name.
OpenTag is meant to be forked. Each thing you would want to change is one file or one directory, and none of them require touching the Channel lifecycle.
| To change… | Edit | Notes |
|---|---|---|
| The persona and behavior | agent/prompts/ |
system.py holds the base system prompt |
| The agent itself | agent/agent.py |
A LangGraph deep agent; model and reasoning effort come from the environment |
| The agent framework | AGENT_URL |
Point it at any AG-UI-compatible agent. The runtime speaks AG-UI over HTTP and does not care what is on the other end |
| Which tools the agent has | agent/tools.py, agent/internal_sources.py |
Sources register only when their credentials are present |
| What gets rendered in chat | app/components/, app/tools/ |
Issue cards, tables, charts, diagrams |
| Mentions, commands, triggers | app/channel.tsx |
The whole Channel surface in one file |
| Which writes need approval | agent/write_confirmation.py |
The interceptor that emits confirm_write |
| The deployment topology | .railway/railway.ts |
Two services, declared as code |
If you are customizing with a coding agent, read AGENTS.md
first — it names the API authority for Channels code and points at the setup
workflow instead of letting an agent improvise one.
- Mentions and app-owned commands.
- Sender-aware context and Slack tools scoped to Slack turns.
- Rich issue, page, status, incident, link, table, native Slack chart, and diagram output.
- File-aware prompts.
- A LangGraph interrupt and resumable confirmation card before Linear or Notion writes.
- Graceful, idempotent shutdown for Channels, HTTP, and the rendering browser.
- Nullable parent-message ID normalization through
SanitizingHttpAgent.
The Python agent is the only supported backend. Its identity and behavior live
in agent/agent.py; the Channel UI and behavior live under
app/.
Slack / Microsoft Teams
│ HTTPS to an Intelligence-hosted Request URL
▼
CopilotKit Intelligence
│ outbound websocket from your runtime
▼
runtime (Node + CopilotRuntime with embedded Channels)
│ AG-UI
▼
agent (Python + LangGraph deepagents)
├── OpenAI
├── Tavily (optional)
├── GitHub MCP (optional, read-only)
├── PostHog MCP (optional, read-only)
├── Linear MCP (optional)
└── Notion MCP (optional remote server)
| You run | CopilotKit Intelligence manages |
|---|---|
| The Python agent, its model credentials, and its tools | Slack and Microsoft Teams platform credentials |
| The long-running Node Channels runtime | Platform ingress and credentialed delivery |
| Deployment, state, and logs | Runtime registration, health, and reconnects |
Neither leg is Socket Mode, and neither needs a tunnel or a public URL of your
own. Slack reaches Intelligence over HTTPS, authenticated by the signing secret
Intelligence holds. Intelligence reaches your runtime over a websocket your
process opens outbound, authenticated by INTELLIGENCE_API_KEY.
There is one canonical runtime host: server.ts.
app/index.ts composes one CopilotKitIntelligence, one
CopilotRuntime, and one adapter-free managed Channel. Intelligence owns the
Slack and Microsoft Teams adapters, their credentials, and attachments — no
platform credential belongs in this repository's environment.
@copilotkit/channels and @copilotkit/runtime are pinned for reproducible
deploys. package.json is the source of truth for both
versions.
One naming collision is worth internalizing: the CopilotKit CLI's channels
commands configure managed Intelligence Channels. They do not configure the
open-source @copilotkit/channels adapter packages, which are a separate
product that shares the words "channels" and "Slack". OpenTag uses the package
to define its Channel and Intelligence to deliver to it.
Every one of these is optional. Without them OpenTag still chats, triages requests, and renders UI from model knowledge.
| Variable | Enables |
|---|---|
TAVILY_API_KEY |
Live web research |
GITHUB_PERSONAL_ACCESS_TOKEN |
Read-only repository, code, issue, and PR search |
POSTHOG_PERSONAL_API_KEY |
PostHog analytics, read-only (use the MCP Server key preset) |
LINEAR_API_KEY |
Hosted Linear MCP |
NOTION_MCP_URL + NOTION_MCP_AUTH_TOKEN |
Remote Notion MCP; setting only one disables it |
Every Linear and Notion mutation is intercepted in code before the MCP request
runs. The interceptor emits confirm_write and proceeds only after approval;
reads and rendering do not pause.
setup.md documents each source, its overrides, and the full
environment contract.
.railway/railway.ts defines exactly two services,
both sourced from CopilotKit/OpenTag on main:
| Service | Root | Start | Health |
|---|---|---|---|
agent |
agent |
uvicorn main:app --host "" --port ${PORT:-8123} |
/health |
runtime |
repository root | pnpm runtime |
/api/copilotkit/info |
The runtime reaches the agent over Railway private networking and embeds the
managed Channel. Connecting both services to main enables GitHub-triggered
deployments after merges. See setup.md for the variable
contract.
pnpm install --frozen-lockfile
pnpm setup:dev
pnpm check-types
pnpm test
(cd agent && uv run pytest)
node node_modules/railway/dist/iac/bin.jsThe Slack live harness is documented in e2e/README.md.
| I want to… | Start here |
|---|---|
| Get OpenTag answering in Slack | Quick start |
| Fork it and change the agent | Make it yours |
| Read the full environment contract | setup.md |
| Customize it with a coding agent | AGENTS.md |
| Understand the SDK underneath | Channels SDK |
| Build a Channel from scratch | Channels documentation |
| Try Channels with no setup at all | Try Channels |
| Connect an agent in another framework | AG-UI integrations |
Cut production @kite over to OpenTag |
docs/migration-kite.md |
MIT © CopilotKit


