Connecting OpenClaw to Microsoft Teams, step by step
Teams ships bundled with OpenClaw, but the platform side runs through Microsoft's preview CLI, an Azure bot, and a tunnel. The working path, the single-tenant rule, and the file-sending catch.
The Teams plugin ships bundled with OpenClaw: nothing to install on the agent side. The work is all on Microsoft’s side: an Azure bot registration created through Microsoft’s preview Teams CLI, a tunnel so Teams can reach your machine (“Teams cannot reach localhost”), and a single-tenant app (Microsoft deprecated new multi-tenant bots in mid-2025). Once wired, DMs work well including attachments; group channels come with real caveats around files and threading. Budget an evening, not five minutes.
The working path
Microsoft’s own CLI is the documented golden path (it’s in preview; flags can drift):
npm install -g @microsoft/teams.cli@preview teams login teams app create --name "OpenClaw" \ --endpoint "https://<your-tunnel-url>/api/messages"
That one create command does the Azure heavy lifting (bot registration plus app registration) and hands back the three values OpenClaw needs: CLIENT_ID, CLIENT_SECRET, and TENANT_ID, plus a Teams App ID. Note the tenant rule: new bots are single-tenant (Microsoft closed multi-tenant bot creation after July 2025), which is fine for the run-it-for-your-own-org case this serves.
Teams delivers messages by calling your endpoint, so the gateway’s webhook (port 3978, /api/messages) needs a public HTTPS front: the docs offer devtunnel (persistent), ngrok, or Tailscale Funnel. Then the OpenClaw side is three keys:
channels: {
msteams: {
enabled: true,
appId: "<CLIENT_ID>",
appPassword: "<CLIENT_SECRET>",
tenantId: "<TENANT_ID>",
},
}Install the app into Teams via teams app get <teamsAppId> --install-link, sanity-check with teams app doctor, restart the gateway, and message the bot. One ritual the docs are emphatic about: after any manifest change, reinstall the app and fully quit and relaunch Teams. The client caches aggressively.
Access control
Standard OpenClaw posture with Teams-flavored identifiers: DMs are pairing-gated by default; allowlists should use stable AAD object IDs rather than display names; and group/channel access is blocked by default (groupPolicy: "allowlist"), mention-gated even when opened up. Per-team and per-channel overrides exist, keyed by those long 19:…@thread.tacv2 IDs, which you pull from the Teams URL path, not the groupId query parameter.
The caveats that actually bite
- Files in group chats are a project. DM attachments work both ways, but in channels the payload carries an HTML stub, not bytes. Reading group files needs Graph permissions, and sending them needs a SharePoint site ID plus Graph permissions. Plan on text-first in channels.
- Threading is configured blind. Teams has two channel UI styles (Posts vs Threads) and the API doesn’t expose which one a channel uses. The
replyStylesetting is per-channel and you have to set it to match what you see. - Messages chunk at 4,000 characters; Teams markdown is more limited than you’d hope (tables and nested lists may not render); private channels have limited bot support.
- The webhook has real timing budgets (roughly 30-second request windows), relevant if your tunnel adds latency.
Worth it?
Same verdict shape as Google Chat: if Teams is where your organization lives, this is the supported route and it works. For a personal agent’s first channel, the consumer apps are radically less ceremony. And the agent answering in Teams is only as available as the machine and tunnel underneath it, which is the part that has to stay up around the clock.