Microsoft Teams Bot Setup, Capabilities, and Configuration
Learn how to configure Microsoft Teams bots in OpenClaw, including supported features like file uploads, Adaptive Cards, and bundled plugin installation. Essential for developers setting up Teams integration.
Read this when
- Working on Microsoft Teams channel features
Status: text and DM attachments are supported; sending files to channels or groups requires sharePointSiteId plus Graph permissions (see Sending files in group chats). Polls and approval prompts go out as Adaptive Cards. Message actions surface explicit upload-file for file-first sends.
Bundled plugin
Microsoft Teams comes bundled as a plugin in current OpenClaw releases; the standard packaged build needs no separate installation.
On an older build or a custom install without bundled Teams, install the npm package directly:
openclaw plugins install @openclaw/msteams
Use the bare package to track the current official release tag. Pin an exact version only when you need a reproducible install.
Local checkout (running from a git repo):
openclaw plugins install ./path/to/local/msteams-plugin
Details: Plugins
Quick setup
@microsoft/teams.cli handles bot registration, manifest creation, and credential generation in one command.
1. Install and log in
npm install -g @microsoft/teams.cli@preview
teams login
teams status # verify you're logged in and see your tenant info
Note
The Teams CLI is currently in preview. Commands and flags may change between releases.
2. Start a tunnel (Teams cannot reach localhost)
Install and authenticate the devtunnel CLI if needed (getting started guide).
# One-time setup (persistent URL across sessions):
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
# Each dev session:
devtunnel host my-openclaw-bot
# Your endpoint: https://<tunnel-id>.devtunnels.ms/api/messages
Note
--allow-anonymousis required because Teams cannot authenticate with devtunnels. Each incoming bot request is still validated by the Teams SDK.
Alternatives: ngrok http 3978 or tailscale funnel 3978 (URLs may change each session).
3. Create the app
teams app create \
--name "OpenClaw" \
--endpoint "https://<your-tunnel-url>/api/messages"
This creates an Entra ID (Azure AD) application, generates a client secret, builds and uploads a Teams app manifest (with icons), and registers a Teams-managed bot (no Azure subscription needed). The output includes CLIENT_ID, CLIENT_SECRET, TENANT_ID, and a Teams App ID; it also offers to install the app in Teams directly.
4. Configure OpenClaw using the credentials from the output:
{
channels: {
msteams: {
enabled: true,
appId: "<CLIENT_ID>",
appPassword: "<CLIENT_SECRET>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Or use environment variables directly: MSTEAMS_APP_ID, MSTEAMS_APP_PASSWORD, MSTEAMS_TENANT_ID.
5. Install the app in Teams
teams app create prompts you to install the app; select "Install in Teams". To get the install link later:
teams app get <teamsAppId> --install-link
6. Verify everything works
teams app doctor <teamsAppId>
Runs diagnostics across bot registration, AAD app config, manifest validity, and SSO setup.
For production, consider federated authentication (certificate or managed identity) instead of client secrets.
Note
Group chats are blocked by default (
channels.msteams.groupPolicy: "allowlist"). To allow group replies, setchannels.msteams.groupAllowFrom, or usegroupPolicy: "open"to allow any member (mention-gated).
Goals
- Talk to OpenClaw via Teams DMs, group chats, or channels.
- Keep routing deterministic: replies always go back to the channel they arrived on.
- Default to safe channel behavior (mentions required unless configured otherwise).
Config writes
By default, Microsoft Teams can write config updates triggered by /config set|unset (requires commands.config: true).
Disable with:
{
channels: { msteams: { configWrites: false } },
}
Access control (DMs + groups)
Microsoft Teams has one account per channel configuration. Set policies directly under channels.msteams; an accounts map is not supported.
DM access
- Default:
channels.msteams.dmPolicy = "pairing". Unknown senders are ignored until approved. channels.msteams.allowFromshould use stable AAD object IDs or static sender access groups such asaccessGroup:core-team.- Do not rely on UPN/display-name matching for allowlists; they can change. OpenClaw disables direct name matching by default; opt in with
channels.msteams.dangerouslyAllowNameMatching: true. - The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
Group access
- Default:
channels.msteams.groupPolicy = "allowlist"(blocked unless you addgroupAllowFrom). Setchannels.msteams.groupPolicyexplicitly to choose another policy; the root schema default takes precedence overchannels.defaults.groupPolicy. channels.msteams.groupAllowFromcontrols which senders, static sender access groups, or group/channel conversation IDs can trigger in group chats/channels (falls back tochannels.msteams.allowFrom). Conversation IDs can use19:...@thread.tacv2,19:...@thread.v2, or19:...@thread.skype; preserve the exact ID casing. OpenClaw ignores;messageid=...suffixes. Conversation IDs never grant personal-DM access.- Set
groupPolicy: "open"to allow any member (still mention-gated by default). - To block all channels, set
channels.msteams.groupPolicy: "disabled".
Example:
{
channels: {
msteams: {
groupPolicy: "allowlist",
groupAllowFrom: ["00000000-0000-0000-0000-000000000000", "accessGroup:core-team"],
},
},
}
Team + channel allowlist
- To limit replies to specific groups and channels, enumerate the teams and channels beneath
channels.msteams.teams. - Prefer stable Teams conversation IDs obtained from Teams links as lookup keys over changeable display names (refer to Team and Channel IDs).
- If
groupPolicy="allowlist"is set and a teams allowlist exists, only the teams and channels on that list are permitted (mention-gated). groupAllowFromgrants permission to group senders, not to delegated Graph access for other channels. When an existing configuration only specifiesgroupAllowFrom, retain the defaultgroupPolicy: "allowlist"and set the target underchannels.msteams.teams.<team>.channels.- As an alternative, explicitly choose
groupPolicy: "open"to allow broader delegated reads. This also lets any group sender through (still mention-gated by default), making it less restrictive than a scoped team or channel route. - Reads performed directly by the operator or within the active conversation do not require an extra team or channel route.
- The configuration wizard accepts
Team/Channelentries and saves them on your behalf. - At startup, OpenClaw converts team, channel, and user allowlist names into IDs (provided Graph permissions allow it) and records the translation. Names that cannot be resolved stay as entered but are skipped for routing unless
channels.msteams.dangerouslyAllowNameMatching: trueis configured.
Example:
{
channels: {
msteams: {
groupPolicy: "allowlist",
groupAllowFrom: ["00000000-0000-0000-0000-000000000000"],
teams: {
"19:team-id@thread.tacv2": {
channels: {
"19:channel-id@thread.tacv2": { requireMention: true },
},
},
},
},
},
}
Manual configuration (bypassing the Teams CLI)
How it works
- Confirm the Microsoft Teams plugin is present (included with current releases).
- Set up an Azure Bot (App ID, secret, and tenant ID).
- Assemble a Teams app package that references the bot and includes the RSC permissions listed below.
- Deploy the Teams app into a team (or personal scope for direct messages).
- Define
msteamsin~/.openclaw/openclaw.json(or via environment variables) and launch the gateway. - By default, the gateway listens for Bot Framework webhook traffic on
/api/messages.
Step 1: Create Azure Bot
-
Navigate to Create Azure Bot
-
Complete the Basics tab:
Field Value Bot handle Your bot name, e.g., openclaw-msteams(must be unique)Subscription Select your Azure subscription Resource group Create new or use existing Pricing tier Free for dev/testing Type of App Single Tenant (recommended; see note below) Creation type Create new Microsoft App ID
Warning
After 2025-07-31, creating new multi-tenant bots was deprecated. For new bots, choose Single Tenant.
- Press Review + create, then Create (takes about 1-2 minutes).
Step 2: Get credentials
- In the Azure Bot resource, open Configuration and copy the Microsoft App ID (your
appId). - Under Manage Password, go to App Registration, select Certificates & secrets, create a New client secret, and copy the Value (your
appPassword). - From Overview, copy the Directory (tenant) ID (your
tenantId).
Step 3: Configure messaging endpoint
- Open the Azure Bot and go to Configuration.
- Specify the Messaging endpoint:
- Production:
https://your-domain.com/api/messages - Local development: rely on a tunnel (see Local development)
- Production:
Step 4: Enable Teams channel
- In the Azure Bot, select Channels.
- Choose Microsoft Teams, click Configure, then Save.
- Accept the Terms of Service.
Step 5: Build Teams app manifest
- Add a
botentry that includesbotId = <App ID>. - Scopes:
personal,team,groupChat. supportsFiles: true(needed for handling files in personal scope).- Include RSC permissions (see RSC permissions).
- Prepare icons:
outline.png(32x32) andcolor.png(192x192). - Combine
manifest.json,outline.png, andcolor.pnginto a zip file.
Step 6: Configure OpenClaw
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Environment variables: MSTEAMS_APP_ID, MSTEAMS_APP_PASSWORD, MSTEAMS_TENANT_ID.
Step 7: Run the gateway
The Teams channel launches automatically once the plugin is available and msteams configuration contains credentials.
Federated authentication (certificate plus managed identity)
In production, OpenClaw offers federated authentication as a substitute for client secrets, using channels.msteams.authType: "federated". There are two approaches:
Option A: Certificate-based authentication
Utilize a PEM certificate that is registered with your Entra ID app registration.
Setup steps:
- Create or acquire a certificate (PEM format with a private key).
- In Entra ID, go to App Registration, then Certificates & secrets, open Certificates, and upload the public certificate.
Configuration:
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
tenantId: "<TENANT_ID>",
authType: "federated",
certificatePath: "/path/to/cert.pem",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Environment variables:
MSTEAMS_AUTH_TYPE=federatedMSTEAMS_CERTIFICATE_PATH=/path/to/cert.pem
Option B: Azure Managed Identity
Leverage Azure Managed Identity for passwordless authentication on Azure infrastructure (AKS, App Service, Azure VMs).
Operation:
- The bot pod or VM is assigned a managed identity (system or user assigned).
- A federated identity credential connects that managed identity to the Entra ID app registration.
- At runtime, OpenClaw obtains tokens from the Azure IMDS endpoint via
@azure/identity. - Those tokens are handed to the Teams SDK for bot authentication.
Requirements:
- Azure infrastructure with managed identity enabled (AKS workload identity, App Service, VM).
- A federated identity credential established on the Entra ID app registration.
- Network access from the pod or VM to IMDS (
169.254.169.254:80).
Configuration (system-assigned managed identity):
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
tenantId: "<TENANT_ID>",
authType: "federated",
useManagedIdentity: true,
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Configuration (user-assigned managed identity): append managedIdentityClientId: "<MI_CLIENT_ID>" to the block above.
Environment variables:
MSTEAMS_AUTH_TYPE=federatedMSTEAMS_USE_MANAGED_IDENTITY=trueMSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id>(user-assigned only)
AKS Workload Identity setup
For AKS deployments that rely on workload identity:
-
Turn on workload identity for the AKS cluster.
-
Set up a federated identity credential on the Entra ID app registration:
az ad app federated-credential create --id <APP_OBJECT_ID> --parameters '{ "name": "my-bot-workload-identity", "issuer": "<AKS_OIDC_ISSUER_URL>", "subject": "system:serviceaccount:<NAMESPACE>:<SERVICE_ACCOUNT>", "audiences": ["api://AzureADTokenExchange"] }' -
Add an annotation to the Kubernetes service account carrying the app client ID:
apiVersion: v1 kind: ServiceAccount metadata: name: my-bot-sa annotations: azure.workload.identity/client-id: "<APP_CLIENT_ID>" -
Label the pod so workload identity gets injected:
metadata: labels: azure.workload.identity/use: "true" -
Open network access to IMDS (
169.254.169.254): when NetworkPolicy is in use, include an egress rule targeting169.254.169.254/32over port 80.
Auth type comparison
| Method | Config | Pros | Cons |
|---|---|---|---|
| Client secret | appPassword | Straightforward configuration | Rotation of secret needed, weaker security |
| Certificate | authType: "federated" + certificatePath | No secret transmitted over the network | Extra work managing certificates |
| Managed Identity | authType: "federated" + useManagedIdentity | No password, nothing to manage | Depends on Azure infrastructure |
certificateThumbprint may be configured next to certificatePath, yet the auth path does not consult it at present; it exists purely for future compatibility.
Default behavior: when authType is not defined, OpenClaw authenticates with a client secret (appPassword). Configurations already in place continue to function without changes.
Local development (tunneling)
Teams has no way to reach localhost. Keep a persistent dev tunnel active so the URL does not change between sessions:
# One-time setup:
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
# Each dev session:
devtunnel host my-openclaw-bot
Other options: ngrok http 3978 or tailscale funnel 3978 (these may produce a different URL on every session).
When the tunnel URL changes, point the endpoint at the new value:
teams app update <teamsAppId> --endpoint "https://<new-url>/api/messages"
Testing the bot
Run diagnostics:
teams app doctor <teamsAppId>
This single pass verifies the bot registration, AAD app, manifest, and SSO configuration.
Send a test message:
- Install the Teams app via the install link from
teams app get <id> --install-link. - Locate the bot inside Teams and send it a direct message.
- Watch the gateway logs for incoming activity.
Environment variables
The auth-related config keys below can be supplied through environment variables rather than openclaw.json (other keys, for instance groupPolicy or historyLimit, only work in config files):
| Env var | Config key | Notes |
|---|---|---|
MSTEAMS_APP_ID | appId | |
MSTEAMS_APP_PASSWORD | appPassword | |
MSTEAMS_TENANT_ID | tenantId | |
MSTEAMS_AUTH_TYPE | authType | "secret" or "federated" |
MSTEAMS_CERTIFICATE_PATH | certificatePath | federated plus certificate |
MSTEAMS_CERTIFICATE_THUMBPRINT | certificateThumbprint | accepted, not mandatory for auth |
MSTEAMS_USE_MANAGED_IDENTITY | useManagedIdentity | federated plus managed identity |
MSTEAMS_MANAGED_IDENTITY_CLIENT_ID | managedIdentityClientId | only for user-assigned managed identity |
Member info action
OpenClaw provides a Graph-backed member-info action for Microsoft Teams, letting agents and automations fetch verified roster details for a specified conversation.
Prerequisites:
ChannelSettings.Read.GroupandTeamMember.Read.GroupRSC permissions (these are already part of the recommended manifest).
The action is present whenever Graph credentials are configured; no separate channels.msteams.actions.memberInfo switch exists.
Queries on standard channels return the matching team-roster identity, display name, email, and roles.
Inside the current DM or group chat, the action can return the trusted sender's stable user ID.
Lookups for private/shared-channel members or non-current chat participants demand extra roster permissions
and get refused by the default permission baseline.
History context
channels.msteams.historyLimitdetermines how many recent channel/group messages get wrapped into the prompt. It falls back tomessages.groupChat.historyLimit, then to 50 by default. Set0to switch it off.- Thread history that gets fetched is filtered by sender allowlists (
allowFrom/groupAllowFrom), so thread context seeding only includes messages coming from approved senders. - Quoted attachment context (extracted from the Skype Reply-schema HTML inside a reply's own attachments) goes through without filtering; only thread-history seeding applies the sender-allowlist filter at this time.
- DM history can be capped with
channels.msteams.dmHistoryLimit(user turns). Per-user overrides:channels.msteams.dms["<user_id>"].historyLimit.
Current Teams RSC permissions (manifest)
The resourceSpecific permissions listed below are the ones already defined in our Teams app manifest. Their effect is limited to the team or chat where the app has been installed.
For channels (team scope):
ChannelMessage.Read.Group(Application) - get every channel message, no @mention neededChannelMessage.Send.Group(Application)Member.Read.Group(Application)Owner.Read.Group(Application)ChannelSettings.Read.Group(Application)TeamMember.Read.Group(Application)TeamSettings.Read.Group(Application)
For group chats:
ChatMessage.Read.Chat(Application) - get every group chat message, no @mention needed
The Teams CLI is the tool to use for adding RSC permissions:
teams app rsc add <teamsAppId> ChannelMessage.Read.Group --type Application
Example Teams manifest (redacted)
A minimal, valid example containing all required fields. Swap in your own IDs and URLs.
{
$schema: "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
manifestVersion: "1.23",
version: "1.0.0",
id: "00000000-0000-0000-0000-000000000000",
name: { short: "OpenClaw" },
developer: {
name: "Your Org",
websiteUrl: "https://example.com",
privacyUrl: "https://example.com/privacy",
termsOfUseUrl: "https://example.com/terms",
},
description: { short: "OpenClaw in Teams", full: "OpenClaw in Teams" },
icons: { outline: "outline.png", color: "color.png" },
accentColor: "#5B6DEF",
bots: [
{
botId: "11111111-1111-1111-1111-111111111111",
scopes: ["personal", "team", "groupChat"],
isNotificationOnly: false,
supportsCalling: false,
supportsVideo: false,
supportsFiles: true,
},
],
webApplicationInfo: {
id: "11111111-1111-1111-1111-111111111111",
},
authorization: {
permissions: {
resourceSpecific: [
{ name: "ChannelMessage.Read.Group", type: "Application" },
{ name: "ChannelMessage.Send.Group", type: "Application" },
{ name: "Member.Read.Group", type: "Application" },
{ name: "Owner.Read.Group", type: "Application" },
{ name: "ChannelSettings.Read.Group", type: "Application" },
{ name: "TeamMember.Read.Group", type: "Application" },
{ name: "TeamSettings.Read.Group", type: "Application" },
{ name: "ChatMessage.Read.Chat", type: "Application" },
],
},
},
}
Manifest caveats (must-have fields)
- The value of
bots[].botIdmust equal the Azure Bot App ID. - The value of
webApplicationInfo.idmust equal the Azure Bot App ID. bots[].scopeshas to cover the surfaces you intend to work with (personal,team,groupChat).- File handling in personal scope depends on
bots[].supportsFiles: truebeing set. - Channel traffic needs
authorization.permissions.resourceSpecificto include both read and send permissions for channels.
Updating an existing app
# Download, edit, and re-upload the manifest
teams app manifest download <teamsAppId> manifest.json
# Edit manifest.json locally...
teams app manifest upload manifest.json <teamsAppId>
# Version is auto-bumped if content changed
After the update, the app needs to be reinstalled in every team. Also, completely exit and restart Teams (closing the window alone is not enough) so that cached app metadata gets cleared.
Manual manifest update (without CLI)
- Apply the new settings to
manifest.json. - Raise the
versionvalue (for instance,1.0.0→1.1.0). - Recreate the zip for the manifest along with its icons (
manifest.json,outline.png,color.png). - Upload the fresh zip:
- Teams Admin Center: Teams apps → Manage apps → locate your app → Upload new version.
- Sideload: Teams → Apps → Manage your apps → Upload a custom app.
Capabilities: RSC only vs Graph
With Teams RSC only (app installed, no Graph API permissions)
What functions:
- Reading the text portion of channel messages.
- Sending text content to channels.
- Getting personal (DM) file attachments.
What does not function:
- Channel/group images or file contents (the payload carries only an HTML stub).
- Fetching attachments that live in SharePoint/OneDrive.
- Accessing message history beyond the live webhook event.
With Teams RSC + Microsoft Graph Application permissions
What it adds:
- Fetching hosted content (for example, images pasted into messages).
- Fetching file attachments that live in SharePoint/OneDrive.
- Using Graph to read channel/chat message history.
RSC vs Graph API
| Capability | RSC permissions | Graph API |
|---|---|---|
| Real-time messages | Yes (via webhook) | No (polling only) |
| Historical messages | No | Yes (can query history) |
| Setup complexity | App manifest only | Requires admin consent + token flow |
| Works offline | No (must be running) | Yes (query anytime) |
Bottom line: RSC handles real-time listening; Graph API handles historical access. If you need to retrieve missed messages while offline, Graph API with ChannelMessage.Read.All is the way to go (admin consent is required).
Graph-enabled media + history
Turn on only the Microsoft Graph application permissions that match the Teams scopes and data you actually use:
- In Entra ID (Azure AD) App Registration, add Graph Application permissions:
ChannelMessage.Read.Allcovers channel attachments and channel history.Chat.Read.Allcovers group-chat attachments and group-chat history.Files.Read.Allis needed only when attachment bytes must be pulled from SharePoint/OneDrive storage; history-only setups can skip it.
- Grant admin consent at the tenant level.
- Increase the Teams app manifest version, re-upload it, and reinstall the app in Teams.
- Completely exit and restart Teams so cached app metadata is cleared.
Channel/group file recovery (graphMediaFallback)
Teams has the ability to strip file markers from the HTML activity sent to a bot. When that happens, the Bot Framework activity looks exactly like a normal HTML message; the full attachment reference exists only on the Graph copy of the message.
After granting the permissions above, enable the fallback:
{
channels: {
msteams: {
graphMediaFallback: true,
},
},
}
This applies only to channels and group chats. It triggers one Graph message lookup whenever an HTML activity came through without any directly downloadable media, including ordinary or mention-only messages. The default is false, so existing installations will not automatically see extra Graph traffic or permission errors.
User mentions: @mentions function immediately for users who are already part of the conversation. To search for and mention users who are not in the current conversation, add the User.Read.All (Application) permission and get admin consent.
Known limitations
Webhook timeouts
Messages arrive through an HTTP webhook from Teams. OpenClaw sets fixed HTTP server timeouts on that webhook listener: 30s of inactivity, 30s for the total request, and 15s to receive headers. Optional inbound media and context enrichment share a 10-second budget. The SDK returns once the raw activity is durably appended; the agent turn drains independently and replies proactively. If request handling or durable admission misses the transport window, Teams may retry the activity, and the ingress tombstone rejects a repeated event ID.
Teams cloud and service URL support
This SDK-backed Teams path is live-validated for Microsoft Teams public cloud.
Inbound replies use the incoming Teams SDK turn context. Out-of-context proactive operations - sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies - use the stored conversation reference serviceUrl. Public cloud defaults to the Teams SDK public cloud environment and allows stored references on the public Teams Connector host: https://smba.trafficmanager.net/.
Public cloud is the default. You do not need to set channels.msteams.cloud or channels.msteams.serviceUrl for normal public-cloud bots.
For non-public Teams clouds, set cloud and the matching proactive boundary when Microsoft publishes one:
channels.msteams.cloudselects the Teams SDK cloud preset for authentication, JWT validation, token services, and Graph scope.channels.msteams.serviceUrlselects the Bot Connector endpoint boundary used to validate stored conversation references before proactive sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies. It is required for USGov and DoD SDK clouds. For China/21Vianet, OpenClaw uses the SDKChinapreset and accepts stored/configured service URLs only on Azure China Bot Framework channel hosts.
Microsoft publishes the global proactive Bot Connector endpoints in the Create the conversation section of the Teams proactive messaging docs. Use the incoming activity's serviceUrl when available; otherwise use Microsoft's table below.
| Teams environment | OpenClaw config | Proactive serviceUrl |
|---|---|---|
| Public | no cloud/serviceUrl config needed | https://smba.trafficmanager.net/teams |
| GCC | set serviceUrl; no separate Teams SDK cloud preset exists | https://smba.infra.gcc.teams.microsoft.com/teams |
| GCC High | cloud: "USGov" + serviceUrl | https://smba.infra.gov.teams.microsoft.us/teams |
| DoD | cloud: "USGovDoD" + serviceUrl | https://smba.infra.dod.teams.microsoft.us/teams |
| China/21Vianet | cloud: "China" | use the incoming activity's serviceUrl |
For GCC, Microsoft documents a distinct proactive service URL, yet the Teams SDK lacks a dedicated GCC cloud preset. Here is an example:
{
"channels": {
"msteams": {
"serviceUrl": "https://smba.infra.gcc.teams.microsoft.com/teams"
}
}
}
And here is one for GCC High:
{
"channels": {
"msteams": {
"cloud": "USGov",
"serviceUrl": "https://smba.infra.gov.teams.microsoft.us/teams"
}
}
}
Only supported Microsoft Teams Bot Connector hosts can use channels.msteams.serviceUrl. When a service URL is set, OpenClaw verifies that the stored conversation serviceUrl matches that host before executing proactive sends, edits, deletes, cards, polls, or queued long-running replies. Under the default public-cloud setup, OpenClaw fails closed if a stored conversation points to a host outside the public Teams Connector. After altering cloud or service URL settings, receive a fresh message from the conversation to ensure the stored reference is up to date.
Microsoft's Teams proactive endpoint table lists no separate global smba URL for China/21Vianet. Set cloud: "China" so the Teams SDK uses Azure China auth, token, and JWT endpoints. Proactive sends then need either a stored conversation reference from an incoming China Teams activity or an explicitly configured service URL on the Azure China Bot Framework channel boundary (*.botframework.azure.cn). Graph-backed Teams helpers stay disabled for cloud: "China" until OpenClaw directs Graph requests through the Azure China Graph endpoint.
Formatting
Teams markdown is more limited than Slack or Discord:
- Basic formatting works: bold, italic,
code, links. - Complex markdown (tables, nested lists) may not render correctly.
- Adaptive Cards are supported for approval prompts, polls, and semantic presentation sends (see below).
Configuration
Key settings (see /gateway/configuration for shared channel patterns):
channels.msteams.enabled: turns the channel on or off.channels.msteams.appId,channels.msteams.appPassword,channels.msteams.tenantId: credentials for the bot.channels.msteams.cloud: which Teams SDK cloud environment applies (Public,USGov,USGovDoD, orChina;Publicis the default). For USGov or DoD SDK clouds, set this viaserviceUrl; China relies on the SDK preset and stored Azure China Bot Framework conversation references, with Graph-backed helpers staying disabled until Azure China Graph routing becomes available.channels.msteams.serviceUrl: the Bot Connector service URL boundary for SDK proactive operations. The SDK default is used for the public cloud; for GCC (https://smba.infra.gcc.teams.microsoft.com/teams), GCC High, or DoD, you must set it. China accepts Azure China Bot Framework channel hosts when the stored conversation reference originates from Teams operated by 21Vianet.channels.msteams.webhook.port(defaults to3978).channels.msteams.webhook.path(defaults to/api/messages).channels.msteams.dmPolicy:pairing | allowlist | open | disabled(defaults topairing).channels.msteams.allowFrom: DM allowlist, with AAD object IDs being the recommended entries. Stable AAD object IDs also grant permission for approval actions. When Graph access is present, the wizard resolves names to IDs during setup.channels.msteams.defaultTo: the default outbound target; a stable AAD object ID can also grant permission for approval actions.channels.msteams.dangerouslyAllowNameMatching: a break-glass toggle that re-enables mutable UPN/display-name matching and direct team/channel name routing.channels.msteams.textChunkLimit: outbound text chunk size in characters (default4000, hard-capped at4000even if a higher value is configured).channels.msteams.streaming.chunkMode: eitherlength(the default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.msteams.mediaAllowHosts: allowlist for inbound attachment hosts, defaulting to Microsoft/Teams domains: Graph, SharePoint/OneDrive, Teams CDN, Bot Framework, Azure Media Services.channels.msteams.mediaAuthAllowHosts: allowlist for attaching Authorization headers on media retries, defaulting to Graph plus Bot Framework hosts.channels.msteams.graphMediaFallback: opt into Graph message lookups when channel/group HTML omits file markers (defaultfalse; see Channel/group file recovery).channels.msteams.mediaMaxMb: per-channel media size limit override in MB. When unset, it falls back toagents.defaults.mediaMaxMb.channels.msteams.requireMention: require @mention in channels/groups (defaulttrue).channels.msteams.replyStyle:thread | top-level(see Reply style).channels.msteams.teams.<teamId>.replyStyle: per-team override.channels.msteams.teams.<teamId>.requireMention: per-team override.channels.msteams.teams.<teamId>.tools: default per-team tool policy overrides (allow/deny/alsoAllow) applied when a channel override is absent.channels.msteams.teams.<teamId>.toolsBySender: default per-team per-sender tool policy overrides ("*"wildcard supported).channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle: per-channel override.channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention: per-channel override.channels.msteams.teams.<teamId>.channels.<conversationId>.tools: per-channel tool policy overrides (allow/deny/alsoAllow).channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender: per-channel per-sender tool policy overrides ("*"wildcard supported).toolsBySenderkeys must carry explicit prefixes:channel:,id:,e164:,username:,name:(older keys without prefixes still resolve toid:only).channels.msteams.authType: authentication type, either"secret"(the default) or"federated".channels.msteams.certificatePath: file path for the PEM certificate (used with federated and certificate authentication).channels.msteams.certificateThumbprint: certificate thumbprint, optional for authentication but accepted.channels.msteams.useManagedIdentity: turns on managed identity authentication (federated mode).channels.msteams.managedIdentityClientId: client ID for a user-assigned managed identity.channels.msteams.sharePointSiteId: SharePoint site ID for uploading files in group chats or channels (details in Sending files in group chats).channels.msteams.welcomeCard,channels.msteams.groupWelcomeCard,channels.msteams.promptStarters: the welcome Adaptive Card shown on first DM or group contact, along with its suggested prompt buttons.channels.msteams.responsePrefix: text added to the start of outbound replies.channels.msteams.feedbackEnabled(defaults totrue),channels.msteams.feedbackReflection(defaults totrue),channels.msteams.feedbackReflectionCooldownMs: thumbs-up/down feedback on replies and the follow-up reflection for negative feedback.channels.msteams.sso,channels.msteams.delegatedAuth: Bot Framework OAuth connection and delegated Graph scopes for SSO-backed flows;sso.enabled: truerequiressso.connectionName.
Routing and sessions
- Session keys follow the standard agent format (see /concepts/session):
- Direct messages share the main session (
agent:<agentId>:<mainKey>). - Channel/group messages use the conversation id:
agent:<agentId>:msteams:channel:<conversationId>agent:<agentId>:msteams:group:<conversationId>
- Direct messages share the main session (
Reply style: threads vs posts
Teams offers two channel UI styles on the same underlying data model:
| Style | Description | Recommended replyStyle |
|---|---|---|
| Posts (classic) | Messages appear as cards with threaded replies underneath | thread (default) |
| Threads (Slack-like) | Messages flow linearly, more like Slack | top-level |
The problem: the Teams API does not expose which UI style a channel uses. If you use the wrong replyStyle:
threadin a Threads-style channel → replies appear nested awkwardly.top-levelin a Posts-style channel → replies appear as separate top-level posts instead of in-thread.
Solution: configure replyStyle per-channel based on how the channel is set up:
{
channels: {
msteams: {
replyStyle: "thread",
teams: {
"19:abc...@thread.tacv2": {
channels: {
"19:xyz...@thread.tacv2": {
replyStyle: "top-level",
},
},
},
},
},
},
}
Resolution precedence
When the bot sends a reply into a channel, replyStyle is resolved from the most specific override down to the default. The first non-undefined value wins:
- Per-channel -
channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle - Per-team -
channels.msteams.teams.<teamId>.replyStyle - Global -
channels.msteams.replyStyle - Implicit default - derived from
requireMention:requireMention: true→threadrequireMention: false→top-level
If you set requireMention: false globally without an explicit replyStyle, mentions in Posts-style channels surface as top-level posts even when the inbound was a thread reply. Pin replyStyle: "thread" at the global, team, or channel level to avoid surprises.
For proactive sends into a stored channel conversation (queued tool-call replies, long-running agents), the same team/channel resolution applies; group chats and personal (DM) conversations always resolve to top-level for proactive sends regardless of replyStyle.
Thread context preservation
When replyStyle: "thread" is active and the bot receives an @mention inside a channel thread, OpenClaw reconnects the original thread root to the outbound conversation reference (19:...@thread.tacv2;messageid=<root>), ensuring the response appears in that same thread. This behavior applies both to immediate in-turn sends and to proactive sends that occur after the Bot Framework turn context has lapsed (for example, long-running agents or queued tool-call replies via mcp__openclaw__message).
The thread root comes from the stored threadId on the conversation reference. References saved before threadId revert to activityId (whatever inbound activity last populated the conversation), so current deployments continue functioning without needing a re-seed.
When replyStyle: "top-level" is active, inbound channel-thread messages are deliberately answered as new top-level posts, with no thread suffix added. This is the right behavior for Threads-style channels; if you see top-level posts where threaded replies were expected, replyStyle is misconfigured for that channel.
Attachments and images
Current limitations:
- DMs: images and file attachments work through Teams bot file APIs.
- Channels/groups: attachments reside in M365 storage (SharePoint/OneDrive). The webhook payload delivers only an HTML stub, not the actual file bytes. Graph API permissions are required to download channel attachments.
- For explicit file-first sends, use
action=upload-filewithmedia/filePath/path; optionalmessagebecomes the accompanying text/comment, andfilename(ortitle) overrides the uploaded name.
Without Graph permissions, channel messages containing images arrive as text-only (the image content is inaccessible to the bot).
By default, OpenClaw downloads media only from Microsoft/Teams hostnames. Override this with channels.msteams.mediaAllowHosts (use ["*"] to permit any host).
Authorization headers attach only for hosts listed in channels.msteams.mediaAuthAllowHosts (defaulting to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes).
Sending files in group chats
Bots can send files in DMs using the built-in FileConsentCard flow. Sending files in group chats/channels requires additional setup:
| Context | How files are sent | Setup needed |
|---|---|---|
| DMs | FileConsentCard → user accepts → bot uploads | Works out of the box |
| Group chats/channels | Upload to SharePoint → native file card | Requires sharePointSiteId + Graph permissions |
| Images (any context) | Base64-encoded inline | Works out of the box |
Why group chats need SharePoint
Bots operate under an application identity, while Microsoft Graph's /me resource requires a signed-in user. To send files in group chats/channels, the bot uploads to a SharePoint site and generates a sharing link.
Setup
-
Add Graph API permissions in Entra ID (Azure AD) → App Registration:
Sites.ReadWrite.All(Application) - upload files to SharePoint.ChatMember.Read.All(Application) - least-privileged tenant-wide permission for group-chat file sends.Chat.Read.Allalso works and already covers this when group-chat history is enabled. As a per-chat alternative, use theChatMember.Read.Chatresource-specific consent permission.
-
Grant admin consent for the tenant.
-
Get your SharePoint site ID:
# Via Graph Explorer or curl with a valid token: curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}" # Example: for a site at "contoso.sharepoint.com/sites/BotFiles" curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles" # Response includes: "id": "contoso.sharepoint.com,guid1,guid2" -
Configure OpenClaw:
{ channels: { msteams: { // ... other config ... sharePointSiteId: "contoso.sharepoint.com,guid1,guid2", }, }, }
Sharing behavior
| Context and permission | Sharing behavior |
|---|---|
Channel + Sites.ReadWrite.All | Organization-wide sharing link (anyone in org can access) |
Group chat + Sites.ReadWrite.All + a supported chat-member read grant | Per-user sharing link (only chat members can access) |
| Group chat without a supported chat-member read grant | Send fails closed |
Per-user sharing is more secure since only chat participants can access the file. OpenClaw requires a successful member lookup for group chats; timeouts, transport failures, empty results, and Graph API denials fail the send instead of widening access to the organization.
Fallback behavior
| Scenario | Result |
|---|---|
| Group chat + file + SharePoint and member permissions configured | Upload to SharePoint, send a native file card |
| Group chat + file + missing SharePoint or member permissions | Fail with an actionable configuration error |
Channel + file + sharePointSiteId configured | Upload to SharePoint, send a native file card |
| Personal chat + file | FileConsentCard flow (works without SharePoint) |
| Any context + image | Base64-encoded inline (works without SharePoint) |
Files stored location
Uploaded files are stored in a /OpenClawShared/ folder in the configured SharePoint site's default document library.
Native approval cards
Microsoft Teams can deliver exec and plugin approval requests as Adaptive Cards in the originating conversation. Each card describes the requested command or plugin action and provides only the decisions allowed for that request, such as Approve once, Always allow, and Deny. After a decision or expiration, OpenClaw updates the original card with its final status.
Enable the existing top-level approval forwarding settings for each approval type you want to receive:
{
approvals: {
exec: { enabled: true, mode: "session" },
plugin: { enabled: true, mode: "session" },
},
channels: {
msteams: {
allowFrom: ["00000000-0000-0000-0000-000000000000"],
},
},
}
approvals.exec and approvals.plugin are independent; enabling one does not enable the other. Native card delivery also requires a configured Teams bot and at least one approver resolved from channels.msteams.allowFrom or channels.msteams.defaultTo. Approvers must be stable AAD object IDs; display names, email addresses, group entries, and conversation IDs do not grant approval access. OpenClaw checks the clicking user's AAD object ID before resolving the request.
No Teams-specific approval configuration is required. The existing /approve <id> <decision> command remains available as a text fallback when native delivery is unavailable. For forwarding modes and supported decisions, see Approval forwarding to chat channels.
Polls (Adaptive Cards)
OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).
- CLI:
openclaw message poll --channel msteams --target conversation:<id> --poll-question "..." --poll-option "..." --poll-option "...". - Votes are recorded by the gateway in OpenClaw plugin-state SQLite under
state/openclaw.sqlite. - Existing
msteams-polls.jsonfiles are imported byopenclaw doctor --fix, not by the running plugin. - The gateway must stay online to record votes.
- Polls do not auto-post result summaries, and there is no poll-results CLI yet.
Presentation cards
Send semantic presentation payloads to Teams users or conversations using the message tool, CLI, or normal reply delivery. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.
The presentation parameter accepts semantic blocks. When presentation is provided, the message text is optional. Buttons render as Adaptive Card submit or URL actions. Select menus are not native in the Teams renderer, so OpenClaw downgrades them to readable text before delivery.
Agent tool:
{
action: "send",
channel: "msteams",
target: "user:<id>",
presentation: {
title: "Hello",
blocks: [{ type: "text", text: "Hello!" }],
},
}
CLI:
openclaw message send --channel msteams \
--target "conversation:19:abc...@thread.tacv2" \
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello!"}]}'
For target format details, see Target formats below.
Target formats
MSTeams targets rely on prefixes to tell users apart from conversations:
| Target type | Format | Example |
|---|---|---|
| User (by ID) | user:<aad-object-id> | user:40a1a0ed-4ff2-4164-a219-55518990c197 |
| User (by name) | user:<display-name> | user:John Smith (requires Graph API) |
| Group/channel | conversation:<conversation-id> | conversation:19:abc123...@thread.tacv2 |
| Group/channel (raw) | <conversation-id> | 19:abc123...@thread.tacv2, 19:...@unq.gbl.spaces, or a bare a:/8:orgid:/29: Bot Framework id |
CLI examples:
# Send to a user by ID
openclaw message send --channel msteams --target "user:40a1a0ed-..." --message "Hello"
# Send to a user by display name (triggers Graph API lookup)
openclaw message send --channel msteams --target "user:John Smith" --message "Hello"
# Send to a group chat or channel
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"
# Send a presentation card to a conversation
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello"}]}'
Agent tool examples:
{
action: "send",
channel: "msteams",
target: "user:John Smith",
message: "Hello!",
}
{
action: "send",
channel: "msteams",
target: "conversation:19:abc...@thread.tacv2",
presentation: {
title: "Hello",
blocks: [{ type: "text", text: "Hello" }],
},
}
Note
When the
user:prefix is absent, names are resolved as groups or teams. For targeting individuals by display name,user:must always be used.
Proactive messaging
- Since OpenClaw only records conversation references once a user has engaged, proactive messages are possible solely after that interaction.
- Gating via
dmPolicyand allowlists is covered in /gateway/configuration.
Team and Channel IDs (Common Gotcha)
The groupId query parameter found in Teams URLs is not the team ID for configuration. Pull IDs from the URL path instead:
Team URL:
https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
└────────────────────────────┘
Team conversation ID (URL-decode this)
Channel URL:
https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=...
└─────────────────────────┘
Channel ID (URL-decode this)
For config:
- Team key = the path segment following
/team/(URL-decoded, for instance19:Bk4j...@thread.tacv2; older tenants might display@thread.skype, which also works). - Channel key = the path segment following
/channel/(URL-decoded). - For OpenClaw routing, ignore the
groupIdquery parameter. It represents the Microsoft Entra group ID, not the Bot Framework conversation ID present in incoming Teams activities.
Private channels
Private channels offer only limited bot support:
| Feature | Standard channels | Private channels |
|---|---|---|
| Bot installation | Yes | Limited |
| Real-time messages (webhook) | Yes | May not work |
| RSC permissions | Yes | May behave differently |
| @mentions | Yes | If bot is accessible |
| Graph API history | Yes | Yes (with permissions) |
Workarounds if private channels do not work:
- For bot interactions, stick with standard channels.
- Use DMs; users can always message the bot directly.
- For historical access, employ Graph API (needs
ChannelMessage.Read.All).
Troubleshooting
Common issues
- Images not showing in channels: Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
- No responses in channel: mentions are required by default; set
channels.msteams.requireMention=falseor configure per team/channel. - Version mismatch (Teams still shows old manifest): remove + re-add the app and fully quit Teams to refresh.
- 401 Unauthorized from webhook: expected when testing manually without an Azure JWT; means the endpoint is reachable but auth failed. Use Azure Web Chat to test properly.
Manifest upload errors
- "Icon file cannot be empty": the manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for
outline.png, 192x192 forcolor.png). - "webApplicationInfo.Id already in use": the app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation.
- "Something went wrong" on upload: upload via https://admin.teams.microsoft.com instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error.
- Sideload failing: try "Upload an app to your org's app catalog" instead of "Upload a custom app"; this often bypasses sideload restrictions.
RSC permissions not working
- Verify
webApplicationInfo.idmatches your bot's App ID exactly. - Re-upload the app and reinstall in the team/chat.
- Check if your org admin has blocked RSC permissions.
- Confirm you are using the right scope:
ChannelMessage.Read.Groupfor teams,ChatMessage.Read.Chatfor group chats.
References
- Create Azure Bot - Azure Bot setup guide
- Teams Developer Portal - create/manage Teams apps
- Teams app manifest schema
- Receive channel messages with RSC
- RSC permissions reference
- Teams bot file handling (channel/group requires Graph)
- Proactive messaging
- @microsoft/teams.cli - Teams CLI for bot management
Related
- Channels Overview - all supported channels
- Pairing - DM authentication and pairing flow
- Groups - group chat behavior and mention gating
- Channel Routing - session routing for messages
- Security - access model and hardening