showcase/shell-docs/src/content/docs/frontends/teams.mdx
In this guide, you will connect an AG-UI agent to a managed Azure Bot, start a long-running Channels SDK listener, and verify a real Microsoft Teams message. CopilotKit Intelligence owns the public messaging endpoint and Teams credentials; your process owns the agent and application logic.
New to the product? Start with the Channels overview to understand how the SDK, Runtime, Intelligence, and provider connection fit together.
Before you continue, configure the Channel in Intelligence.
You should have CHANNEL_CODE and INTELLIGENCE_API_KEY.
WebSocket
available in Node.js 22+The install command below uses an exact, tested SDK pair; upgrade
`@copilotkit/channels` and `@copilotkit/runtime` together.
```bash title="Terminal"
mkdir my-teams-channel
cd my-teams-channel
npm init -y
npm pkg set type=module
npm install --save-exact @copilotkit/[email protected] @copilotkit/[email protected]
npm install -D tsx typescript @types/node
```
```json title="tsconfig.json"
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["*.ts", "*.tsx"]
}
```
The selector's **Agent backend** controls the setup below. It must export a
fresh `makeAgent(threadId)` result for each Teams conversation.
<FrameworkSetup concept="channels-agent-setup" />
Create the listener below. Replace `support-teams` with the exact Code shown
in Intelligence.
```ts title="channel.ts"
import { createServer } from "node:http";
import { createChannel } from "@copilotkit/channels";
import {
CopilotKitIntelligence,
CopilotRuntime,
} from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { makeAgent } from "./agent.js";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
const channel = createChannel({
name: required("CHANNEL_CODE"),
provider: "teams",
agent: makeAgent,
context: [
{
description: "Channel behavior",
value: "You are working in Microsoft Teams. Keep responses concise.",
},
],
});
channel.onMessage(async ({ thread, message }) => {
await thread.runAgent({
prompt: message.contentParts?.length
? [
...(message.text
? [{ type: "text" as const, text: message.text }]
: []),
...message.contentParts,
]
: message.text,
context: [
{ description: "Originating platform", value: message.platform },
],
});
});
const intelligence = new CopilotKitIntelligence({
apiKey: required("INTELLIGENCE_API_KEY"),
// Managed deployments use the hosted defaults. Override both URLs
// together only for self-hosted or non-production Intelligence.
apiUrl: process.env.INTELLIGENCE_API_URL,
wsUrl: process.env.INTELLIGENCE_GATEWAY_WS_URL,
});
const runtime = new CopilotRuntime({
agents: {},
intelligence,
identifyUser: () => ({ id: "channels-runtime", name: "Channels Runtime" }),
channels: [channel],
});
// Wire teardown before the listener exists, because creating it is what
// starts the Channel. A Ctrl-C during the connect window then still tears
// the Channel down instead of hitting Node's default handler.
let teardown: (() => Promise<void>) | undefined;
const shutdown = async () => {
await teardown?.();
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
const listener = createCopilotNodeListener({
runtime,
basePath: "/api/copilotkit",
});
const channels = listener.channels;
const server = createServer(listener);
teardown = async () => {
await channels.stop();
if (server.listening) server.close();
};
// Optional: block startup until the activation above settles, so a broken
// deploy fails loudly instead of serving as a bot that never answers.
await channels.ready({ timeoutMs: 30_000 });
const status = channels.status();
if (status.overall !== "online") {
throw new Error(`Teams Channel is not online: ${JSON.stringify(status)}`);
}
const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => {
console.log(`Teams Channel online; lifecycle server listening on :${port}`);
});
```
Creating the Node listener starts the Channel: it owns its own process
lifetime, so a declared Channel connects because it was declared. `ready()`
is therefore optional and purely await-and-observe — it resolves once
activation settles and rejects with the activation failure. Because it can
settle with setup still required, inspect `status()` before reporting the
Channel online. Skip `ready()` and activation failures land in your logs
instead.
The runner does not expose a Teams webhook. The public messaging endpoint
remains the Intelligence endpoint you copied into Azure Bot.
```dotenv title=".env"
INTELLIGENCE_API_KEY=<project-api-key>
CHANNEL_CODE=support-teams
# Add the agent variables shown for your selected backend.
PORT=3000
# Optional paired overrides for self-hosted or non-production Intelligence:
# INTELLIGENCE_API_URL=https://intelligence.example.com
# INTELLIGENCE_GATEWAY_WS_URL=wss://realtime.intelligence.example.com
```
Hosted Intelligence supplies both managed base URLs by default. For a
self-hosted or non-production deployment, override both together. The REST
and realtime planes use separate hosts, so do not derive the WebSocket URL
from the API URL. Pass each as a bare base URL without `/api`, `/socket`,
`/runner`, or `/client`. Create the project-scoped runtime key from **API
Keys** in the Intelligence project sidebar.
Start the selected agent backend, then run:
```bash title="Terminal"
node --env-file=.env --import tsx channel.ts
```
Intelligence should report **Online**.
#### Know the healthy state
Starting the process is what connects the Channel, so it should become
**Online** on its own; the `await channels.ready(...)` in this guide only
waits for that to settle.
| Status | What to check |
| --- | --- |
| **Disabled** | Enable the Channel before expecting delivery. |
| **Setup incomplete** | Finish the selected provider's required setup fields before starting the runtime. |
| **Setup failed** | Reopen platform setup and correct the rejected credentials or configuration. |
| **Waiting for runtime** | Start the process — creating the listener connects the Channel — and match its Code and provider to this Channel. |
| **Conflict** | Compare every replica's complete Channel declaration set. Identical replicas should elect one active owner and connected standbys; different but overlapping sets are unsafe. |
| **Offline** | Check the listener process, network, and Intelligence gateway connection. |
| **Delivery failing** | Check platform credentials, app permissions, and the provider response. |
| **Online** | The runtime is connected; send a real provider message to verify the full path. |
Open the app in Teams, add it to a chat or team, then send:
```text title="Microsoft Teams"
Summarize the decisions in this conversation.
```
In a team channel, mention the bot if the Teams surface requires it. A real
response validates the Azure Bot registration, Intelligence messaging
endpoint, managed credentials, gateway listener, agent, and Adaptive Card
reply path.
Next, add tools and context or interactive approvals.