Back to Copilotkit

Connect and run your agent in Microsoft Teams

showcase/shell-docs/src/content/docs/frontends/teams.mdx

1.68.211.6 KB
Original Source

In this guide, you will connect an AG-UI agent to a managed Microsoft Teams 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.

The bot is created in your own Microsoft tenant and belongs to you. Setup registers it in Teams Developer Portal — no Azure subscription and no Azure Bot resource are involved.

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.

What kind of bot this is

Microsoft supports more than one kind of bot identity. Setup creates a Teams-managed bot: Teams Developer Portal owns the registration, and its app ID and client secrets live there under Tools → Bot management, in your directory, rotatable by your own admins. It is registered single-tenant, so it is scoped to your organization. Nothing is created in your Azure account and nothing is billable. If you stop using CopilotKit the registration stays in your Developer Portal; removing it is yours to do.

The alternative Microsoft offers is an Azure Bot — a billable Azure Bot Service resource in your own subscription, needing a subscription, resource group, and region. Microsoft's Teams CLI recognizes only these two kinds; a self-managed Microsoft Entra app registration that you create and manage yourself is not something it can produce or consume.

Treat the choice as permanent. Microsoft provides a one-way teams app bot migrate to Azure that replaces the existing registration, with nothing to migrate back. CopilotKit itself is indifferent to which kind you point a Channel at — Intelligence performs an ordinary client-credentials grant with the app ID, tenant ID, and secret — but the app ID is embedded in the app package as the bot ID, so changing identity means generating a fresh package, uploading it, and re-installing.

Before you start

  • Node.js 22 or later; the managed launcher requires the global WebSocket available in Node.js 22+
  • A long-running Node host or container; serverless request handlers cannot own the persistent gateway connection

Build and run your Channel

<Steps> <Step> ### Create the runner
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"]
}
```
</Step> <Step> ### Connect your agent backend
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" />
</Step> <Step> ### Declare the managed Teams Channel
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"),
  identifyUser: "platform",
  agent: makeAgent,
});

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,
  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. Teams delivers to the
Intelligence messaging endpoint, which setup registered on the bot for
you.
</Step> <Step> ### Configure secrets and start
```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. |
</Step> <Step> ### Verify a real Teams message
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. A message that does not mention it is
ambient, and an ambient message only reaches your agent once a thread has
been subscribed to.

A real response validates the bot registration, Intelligence messaging
endpoint, managed credentials, gateway listener, agent, and Adaptive Card
reply path.
</Step> </Steps>

Troubleshooting

<Accordions> <Accordion title="The Channel stays at Waiting for runtime"> Confirm the Code matches `CHANNEL_CODE` and the API key belongs to the same Intelligence project. Provider routing comes from the Teams connection attached in Intelligence, not from a `createChannel` option. </Accordion> <Accordion title="Teams cannot reach the bot"> In Teams Developer Portal, open **Tools → Bot management**, select the bot, and compare its endpoint address with the messaging endpoint shown in Intelligence. They must match exactly, including no leading or trailing space — Developer Portal rejects a padded value and reports it as a save failure, which reads like a portal fault rather than a bad value. A bot with the wrong endpoint completes setup and then never receives anything.
This is not your runner's `PORT`. The runner takes no inbound provider
traffic at all.
</Accordion> <Accordion title="The app package is rejected"> Upload the complete zip that setup produced; do not unzip it and upload only `manifest.json`. If the app is already in the team and you are re-adding it to change permissions, remove it first — Teams settles app permissions only while the app is being added. </Accordion> <Accordion title="Startup settles but does not become Online"> Inspect `channels.status()` after the guard in the runner. Finish any **Setup incomplete** state in Intelligence. If Intelligence reports **Conflict**, compare the complete Channel declaration set on every replica; identical replicas should elect an active owner and standbys. </Accordion> </Accordions>

Next, map application users and choose Memory grants, add tools and context, or build interactive approvals.