Connect and run your agent in Microsoft Teams

Run any supported agent framework in Teams through a managed CopilotKit Intelligence connection.


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.

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#

Create the runner#

The install command below uses an exact, tested SDK pair; upgrade @copilotkit/channels and @copilotkit/runtime together.

Terminal
mkdir my-teams-channel
cd my-teams-channel
npm init -y
npm pkg set type=module
npm install --save-exact @copilotkit/channels@0.6.1 @copilotkit/runtime@1.65.0
npm install -D tsx typescript @types/node
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["*.ts", "*.tsx"]
}

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.

Follow the LangGraph FastAPI quickstart to configure the sample_agent graph and AG-UI endpoint, then start the same FastAPI application:

Agent application
uv run main.py

Point the LangGraph HTTP adapter at the AG-UI endpoint:

.env
AGENT_URL=http://localhost:8123/

The Runtime package installed by the Channels quickstart includes the native LangGraph adapter.

This selector variant follows the quickstart's FastAPI deployment, so use LangGraphHttpAgent. Return a fresh adapter for every channel thread:

agent.ts
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";

export function makeAgent(threadId: string) {
  const agent = new LangGraphHttpAgent({
    url: process.env.AGENT_URL!,
  });
  agent.threadId = threadId;
  return agent;
}

Declare the managed Teams Channel#

Create the listener below. Replace support-teams with the exact Code shown in Intelligence.

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. The public messaging endpoint remains the Intelligence endpoint you copied into Azure Bot.

Configure secrets and start#

.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:

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.

StatusWhat to check
DisabledEnable the Channel before expecting delivery.
Setup incompleteFinish the selected provider's required setup fields before starting the runtime.
Setup failedReopen platform setup and correct the rejected credentials or configuration.
Waiting for runtimeStart the process — creating the listener connects the Channel — and match its Code and provider to this Channel.
ConflictCompare every replica's complete Channel declaration set. Identical replicas should elect one active owner and connected standbys; different but overlapping sets are unsafe.
OfflineCheck the listener process, network, and Intelligence gateway connection.
Delivery failingCheck platform credentials, app permissions, and the provider response.
OnlineThe runtime is connected; send a real provider message to verify the full path.

Verify a real Teams message#

Open the app in Teams, add it to a chat or team, then send:

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.

Troubleshooting#

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.

Teams cannot reach the bot

In Azure Bot Configuration, use the exact read-only Messaging endpoint from Intelligence and confirm the Microsoft Teams channel is enabled. Do not use your runner's PORT.

The custom app package is rejected

Download a fresh package after entering the Azure Bot App ID. Upload the complete zip; do not unzip and upload only manifest.json.

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.

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