--- title: Microsoft Teams description: Bring custom agents into Microsoft Teams with Adaptive Cards, streaming updates, approvals, and durable conversation context. hideTOC: true --- Microsoft Teams can be the frontend for agents built on any harness or framework, so your organization can get work done where conversations, decisions, and approvals already happen. The open source Bot SDK gives you the adapter path for TypeScript handlers and JSX-authored Adaptive Cards. CopilotKit Enterprise Intelligence adds the self-hosted or cloud-hosted production layer for conversation mapping, durable agent state, approvals, observability, and release operations when Teams becomes a critical workflow. ## Prerequisites - Node.js 20+ - An OpenAI API key (or Anthropic/Google, or any model the [built-in agent](/build-with-agents) supports) - For local testing, nothing else. The M365 Agents Playground runs over `npx`, with no account needed. - For real Teams (the last step): a Microsoft 365 tenant that allows [custom app upload](https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload), plus access to create an [Entra app registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) and an [Azure Bot resource](https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration) ## Getting started ### Scaffold the project ```bash mkdir my-teams-bot && cd my-teams-bot npm init -y && npm pkg set type=module ``` Install the bot packages, plus `@copilotkit/runtime` for the in-process agent and `tsx` to run TypeScript directly: ```bash npm install @copilotkit/channels @copilotkit/channels-ui @copilotkit/channels-teams @copilotkit/runtime npm install -D tsx typescript @types/node ``` ```bash pnpm add @copilotkit/channels @copilotkit/channels-ui @copilotkit/channels-teams @copilotkit/runtime pnpm add -D tsx typescript @types/node ``` ```bash yarn add @copilotkit/channels @copilotkit/channels-ui @copilotkit/channels-teams @copilotkit/runtime yarn add -D tsx typescript @types/node ``` Then create a `tsconfig.json` that points the JSX factory at `@copilotkit/channels-ui`. That is what makes `` / ` , ); return; } await thread.runAgent(); }); ``` Send a message containing "deploy" and click the buttons. Your handler code never leaves your process. Teams only sees an opaque action id carried in the card's `Action.Submit` data. The default action store is **in-memory**: after a process restart, clicks on old buttons are acknowledged but ignored. For buttons that survive restarts, plug a durable store into `createBot({ actionStore })` (see the [ActionStore contract](/reference/channels/types/ActionStore)). ### Gate an action on human approval The same buttons can **block the agent** until a human decides. A tool calls `await thread.awaitChoice()`, which posts the card and suspends the run until a click resolves it. It's the Teams equivalent of React's `useHumanInTheLoop`. Because the bot acks the Teams turn immediately and runs the agent out-of-turn, the approval can land minutes later and still resume the agent: ```tsx title="bot.tsx" import { defineBotTool } from "@copilotkit/channels"; import { Message, Header, Actions, Button } from "@copilotkit/channels-ui"; import { z } from "zod"; const confirmSend = defineBotTool({ name: "confirm_send", description: "Ask the user to approve before sending. BLOCKS until they click; returns {confirmed}.", parameters: z.object({ summary: z.string() }), async handler({ summary }, { thread }) { const choice = await thread.awaitChoice<{ confirmed?: boolean }>( // [!code highlight]
{`📣 ${summary}?`}
, ); return choice?.confirmed ? "Approved, proceed." : "Declined, stop."; }, }); const bot = createBot({ adapters: [teams({ port: 3978 })], agent: makeAgent, // as above tools: [confirmSend], // [!code highlight] }); ``` The agent calls `confirm_send` before the consequential action, the card posts, and the run waits for Approve/Reject. The [full example](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) wires this into a `send_announcement` flow and updates the card in place (✅/🚫) the moment it's clicked. Pending `awaitChoice` waiters live in memory, so they don't survive a process restart. Durable waiters are a planned follow-up.
## Split the bot and the agent The bot and its agent talk over [AG-UI](https://docs.ag-ui.com), an open protocol for communication between agents and frontends, so they don't have to share a process. The production shape is two services joined by a URL: move the `CopilotSseRuntime` block into its own process (or point at any existing AG-UI endpoint, such as a deployed CopilotKit runtime or a LangGraph server) and point the bot at it via env: ```tsx title="bot.tsx" agent: (threadId) => { const agent = new SanitizingHttpAgent({ url: process.env.AGENT_URL! }); // [!code highlight] agent.threadId = threadId; return agent; }, ``` [`SanitizingHttpAgent`](https://github.com/CopilotKit/CopilotKit/tree/main/packages/channels-teams) is an `HttpAgent` that tolerates the event streams real agent backends emit. Use it instead of the stock `HttpAgent` when connecting to a remote runtime. ## Sideload into Microsoft Teams The Playground needs no credentials. The real Teams client does. Keep the same `bot.tsx`, then add a public HTTPS endpoint, a Microsoft app registration, an Azure Bot resource, and a Teams app manifest. ### Give the bot a public HTTPS endpoint Real Teams reaches your bot over the internet, so it needs a public HTTPS messaging endpoint. The bot is a plain Node HTTP server with one route (`POST /api/messages`) and a `/healthz` probe, so you have two ways to get one. **Deploy it** (recommended for anything past a quick test). It runs on any host that gives you a public HTTPS URL, whether that's a container platform, a PaaS, or a VM behind a reverse proxy. Bind to the port the host provides (the adapter reads `PORT`), and keep the agent runtime reachable from the bot, either in-process as written here or as a second service (see [Split the bot and the agent](#split-the-bot-and-the-agent)). The [`examples/teams`](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) project ships a Dockerfile so the whole thing is a one-step container build. **Tunnel to localhost** for quick testing against real Teams without deploying. Any tunneler works; Microsoft's [`devtunnel` CLI](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started) is one option: ```bash devtunnel create copilotkit-teams -a devtunnel port create copilotkit-teams -p 3978 devtunnel host copilotkit-teams ``` Either way you end up with a public host. Your bot's messaging endpoint is `https:///api/messages`, which you'll plug into the Azure Bot resource and the manifest below. ### Create Microsoft credentials In **Microsoft Entra ID**, create an app registration and copy its **Application (client) ID** (this is your Teams bot id), its **Directory (tenant) ID**, and a new **client secret** value. In **Azure Bot Service**, create a bot resource that uses that app registration, set its messaging endpoint to `https:///api/messages`, and enable the **Microsoft Teams** channel. Then run the bot with those credentials. The Teams adapter reads them from the `clientId` / `clientSecret` / `tenantId` environment variables (the names the M365 Agents SDK uses), or you can pass them to `teams({ clientId, clientSecret, tenantId, port })`: ```bash export OPENAI_API_KEY=sk-... export clientId= export clientSecret= export tenantId= npx tsx bot.tsx ``` With credentials set, the bot acks each Teams turn immediately and runs the agent on a detached context, so a HITL approval can resume the agent minutes later. In the anonymous Playground there's no app id, so the run uses the inbound turn instead, which localhost holds open across the wait. ### Build the Teams app package A Teams app package is a zip of a manifest plus two icons. Create `appPackage/manifest.json`: ```json title="appPackage/manifest.json" { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", "manifestVersion": "1.19", "version": "1.0.0", "id": "", "developer": { "name": "Your company", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, "name": { "short": "CopilotKit Bot", "full": "CopilotKit Teams Bot" }, "description": { "short": "A CopilotKit assistant for Microsoft Teams.", "full": "A Microsoft Teams bot powered by CopilotKit." }, "icons": { "color": "color.png", "outline": "outline.png" }, "accentColor": "#5B5FC7", "bots": [ { "botId": "", "scopes": ["personal", "team", "groupChat"], "supportsFiles": false, "isNotificationOnly": false } ], "validDomains": [""] } ``` Replace `` with a fresh UUID, `` with the Entra client id, and `` with the host only (no `https://`). Add the two required icons, `color.png` (192×192) and `outline.png` (32×32, transparent), then zip them together: ```bash cd appPackage zip -r appPackage.zip manifest.json color.png outline.png ``` The [`examples/teams`](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) project ships a `pnpm package` script that validates the manifest, injects your app id from env, generates placeholder icons, and builds the zip. ### Upload and test in Teams In Microsoft Teams: open **Apps → Manage your apps → Upload a custom app**, choose `appPackage/appPackage.zip`, then open a personal chat with the bot and send `Hello`. You should get the same reply you saw in the Playground. - **Teams says the bot can't be reached.** Confirm the Azure Bot messaging endpoint is exactly `https:///api/messages`, and that your deployment (or tunnel) is up and reachable over HTTPS. - **Auth errors.** Confirm `clientId` / `clientSecret` / `tenantId` match the Entra app registration used by the Azure Bot resource. - **"Upload a custom app" is missing.** Your tenant doesn't allow sideloading for your account; ask a tenant admin to enable custom app upload. ## Known limitations (v1) - **In-memory state:** conversation history and pending HITL approvals are in-memory, so they don't survive a process restart. Swap in a durable `ConversationStore` / `ActionStore` for production. - **Streamed by message edit:** replies post once and edit in place as tokens arrive, rather than native Teams token streaming (a planned enhancement). - **No slash commands, file upload, or directory lookup yet:** drive everything through `onMessage` plus tools. - **Replies only:** the bot answers turns it's part of (DMs and `@`-mentions); it doesn't post proactively on its own. ## Next steps - **API reference:** the [Channels reference](/reference/channels), covering [createBot](/reference/channels/functions/createBot), the [Thread API](/reference/channels/classes/Thread), [tools](/reference/channels/functions/defineBotTool), and the [component vocabulary](/reference/channels/components/Message) - **Full example:** the [Teams demo bot](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams), a `BuiltInAgent` with agent-rendered Adaptive Cards, a human-in-the-loop approval gate, and a deployable app package