Microsoft Teams Bot Support, Capabilities, and Configuration
This page covers Microsoft Teams bot support status, capabilities, and configuration. It is intended for developers integrating bots with Teams, including file sharing and poll delivery.
Read this when
- Working on Microsoft Teams channel features
Status: Text and direct messages with attachments are supported. To send files in channels or group chats, you need sharePointSiteId along with Graph permissions (refer to Sending files in group chats). Polls are delivered through Adaptive Cards. Message actions provide explicit upload-file for initiating file sends.
Bundled plugin
Microsoft Teams comes bundled as a plugin in current OpenClaw releases; no separate installation is needed in the standard packaged build.
On older builds or custom installations that exclude the bundled Teams plugin, install the npm package directly:
openclaw plugins install @openclaw/msteams
Use the bare package to track the latest official release tag. Pin a specific version only when you require a reproducible installation.
Local checkout (running from a git repository):
openclaw plugins install ./path/to/local/msteams-plugin
Details: Plugins
Quick setup
@microsoft/teams.cli handles bot registration, manifest creation, and credential generation in a single 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
- Communicate with OpenClaw through 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)
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).channels.defaults.groupPolicycan override the shared default whenchannels.msteams.groupPolicyis unset. channels.msteams.groupAllowFromcontrols which senders or static sender access groups can trigger in group chats/channels (falls back tochannels.msteams.allowFrom).- 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
- Scope group/channel replies by listing teams and channels under
channels.msteams.teams. - Use stable Teams conversation IDs from Teams links as keys, not mutable display names (see Team and Channel IDs).
- When
groupPolicy="allowlist"and a teams allowlist is present, only listed teams/channels are accepted (mention-gated). - The configure wizard accepts
Team/Channelentries and stores them for you. - On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow) and logs the mapping. Unresolved names are kept as typed but ignored for routing unless
channels.msteams.dangerouslyAllowNameMatching: trueis set.
Example:
{
channels: {
msteams: {
groupPolicy: "allowlist",
teams: {
"My Team": {
channels: {
General: { requireMention: true },
},
},
},
},
},
}
Manual setup (without the Teams CLI)
How it works
- Confirm the Microsoft Teams plugin is available (bundled in current releases).
- Create an Azure Bot (App ID + secret + tenant ID).
- Build a Teams app package referencing the bot, including the RSC permissions below.
- Upload/install the Teams app into a team (or personal scope for DMs).
- Configure
msteamsin~/.openclaw/openclaw.json(or env vars) and start the gateway. - The gateway listens for Bot Framework webhook traffic on
/api/messagesby default.
Step 1: Create Azure Bot
-
Navigate to Create an Azure Bot.
-
Configure the Basics tab with these values:
Field Value Bot handle A unique name for your bot, for instance openclaw-msteamsSubscription Choose your Azure subscription Resource group Either create a new one or pick an existing Pricing tier Select Free for development or testing Type of App Single Tenant (recommended; see the warning below) Creation type Create new Microsoft App ID
Warning
After 2025-07-31, creating new multi-tenant bots is no longer supported. For any new bot, choose Single Tenant.
- Select Review + create, then Create (deployment takes about 1, 2 minutes).
Step 2: Get credentials
- From the Azure Bot resource, open Configuration and copy the Microsoft App ID (this is your
appId). - Under Manage Password, go to App Registration, then Certificates & secrets. Click New client secret and copy the Value (this is your
appPassword). - On the Overview page, copy the Directory (tenant) ID (this is your
tenantId).
Step 3: Configure messaging endpoint
- In your Azure Bot, go to Configuration.
- Specify the Messaging endpoint:
- For production:
https://your-domain.com/api/messages - For local development: use a tunnel (refer to Local development)
- For production:
Step 4: Enable Teams channel
- In your Azure Bot, open Channels.
- Select Microsoft Teams, then Configure, and finally Save.
- Accept the Terms of Service when prompted.
Step 5: Build Teams app manifest
- Add a
botentry that containsbotId = <App ID>. - Set scopes to
personal,team, andgroupChat. - Include
supportsFiles: true(mandatory when handling files in the personal scope). - Configure RSC permissions (see RSC permissions).
- Prepare two icon files:
outline.png(32x32) andcolor.png(192x192). - Package
manifest.json,outline.png, andcolor.pnginto a single zip archive.
Step 6: Configure OpenClaw
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
appPassword: "<APP_PASSWORD>",
tenantId: "<TENANT_ID>",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Set these environment variables: MSTEAMS_APP_ID, MSTEAMS_APP_PASSWORD, MSTEAMS_TENANT_ID.
Step 7: Run the gateway
Once the plugin is available and the msteams configuration has credentials, the Teams channel activates on its own.
Federated authentication (certificate plus managed identity)
For production environments, OpenClaw supports federated authentication as an alternative to client secrets, using channels.msteams.authType: "federated". Two approaches are available:
Option A: Certificate-based authentication
Use a PEM certificate registered with your Entra ID app registration.
Setup:
- Obtain or create a certificate in PEM format that includes the private key.
- In Entra ID, open App Registration, go to Certificates & secrets, select Certificates, and upload the public certificate.
Config:
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
tenantId: "<TENANT_ID>",
authType: "federated",
certificatePath: "/path/to/cert.pem",
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Env vars:
MSTEAMS_AUTH_TYPE=federatedMSTEAMS_CERTIFICATE_PATH=/path/to/cert.pem
Option B: Azure Managed Identity
Use Azure Managed Identity for passwordless authentication when running on Azure infrastructure such as AKS, App Service, or Azure VMs.
How it works:
- The bot's pod or VM is assigned a managed identity (either system-assigned or user-assigned).
- A federated identity credential connects that managed identity to the Entra ID app registration.
- At runtime, OpenClaw calls
@azure/identityto request tokens from the Azure IMDS endpoint. - The Teams SDK receives this token for bot authentication.
Prerequisites:
- Azure infrastructure with managed identity enabled (AKS workload identity, App Service, or VM).
- A federated identity credential created on the Entra ID app registration.
- Network connectivity to IMDS (
169.254.169.254:80) from the pod or VM.
Config (system-assigned managed identity):
{
channels: {
msteams: {
enabled: true,
appId: "<APP_ID>",
tenantId: "<TENANT_ID>",
authType: "federated",
useManagedIdentity: true,
webhook: { port: 3978, path: "/api/messages" },
},
},
}
Config (user-assigned managed identity): add managedIdentityClientId: "<MI_CLIENT_ID>" to the block shown above.
Env vars:
MSTEAMS_AUTH_TYPE=federatedMSTEAMS_USE_MANAGED_IDENTITY=trueMSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id>(only for user-assigned)
AKS Workload Identity setup
For AKS deployments using workload identity:
-
Enable workload identity on your AKS cluster.
-
Create 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"] }' -
Annotate the Kubernetes service account with 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 that workload identity is injected:
metadata: labels: azure.workload.identity/use: "true" -
Allow network access to IMDS (
169.254.169.254): if you use NetworkPolicy, add an egress rule for169.254.169.254/32on port 80.
Auth type comparison
| Method | Config | Pros | Cons |
|---|---|---|---|
| Client secret | appPassword | Straightforward to set up | Requires secret rotation, less secure |
| Certificate | authType: "federated" + certificatePath | No shared secret transmitted | Overhead of certificate management |
| Managed Identity | authType: "federated" + useManagedIdentity | No passwords, no secrets to handle | Only works on Azure infrastructure |
You can include certificateThumbprint alongside certificatePath, but the authentication path currently ignores it; it is accepted solely for future compatibility.
Default: when authType is not configured, OpenClaw authenticates using a client secret (appPassword). Existing configurations remain unaffected.
Local development (tunneling)
Microsoft Teams cannot reach localhost. Use a persistent development tunnel so the URL remains the same across 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 (URLs might change with each session).
If the tunnel URL changes, adjust the endpoint:
teams app update <teamsAppId> --endpoint "https://<new-url>/api/messages"
Testing the bot
Execute diagnostics:
teams app doctor <teamsAppId>
Validates bot registration, AAD application, manifest, and SSO configuration in a single run.
Send a test message:
- Deploy the Teams app (use the install link from
teams app get <id> --install-link). - Locate the bot in Teams and send it a direct message.
- Review gateway logs for incoming activity.
Environment variables
These authentication configuration keys can be supplied through environment variables instead of openclaw.json (other keys, like groupPolicy or historyLimit, are only configurable through the configuration file):
| 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 user-assigned managed identity |
Member info action
OpenClaw provides a Graph-backed member-info action for Microsoft Teams, enabling agents and automations to retrieve verified roster details for a specified conversation.
Prerequisites:
ChannelSettings.Read.GroupandTeamMember.Read.GroupRSC permissions (already included in the suggested manifest).
The action becomes available whenever Graph credentials are set up; no separate channels.msteams.actions.memberInfo toggle exists.
Standard channel lookups return the corresponding team-roster identity, display name, email, and roles.
In the current direct message or group chat, the action can provide the trusted sender's stable user ID.
Lookups for private or shared channels and members not in the current chat require additional roster permissions
and are blocked by the default permission baseline.
History context
channels.msteams.historyLimitcontrols how many recent channel or group chat messages are included in the prompt. Falls back tomessages.groupChat.historyLimit, then defaults to 50. Set0to turn off.- Retrieved thread history is filtered by sender allowlists (
allowFrom/groupAllowFrom), so thread context seeding includes only messages from permitted senders. - Quoted attachment context (extracted from the Skype Reply-schema HTML in a reply's own attachments) passes through without filtering; only thread-history seeding applies the sender-allowlist filter at this time.
- DM history can be limited with
channels.msteams.dmHistoryLimit(user turns). Per-user overrides:channels.msteams.dms["<user_id>"].historyLimit.
Current Teams RSC permissions (manifest)
These are the current resourceSpecific permissions in our Teams app manifest. They apply only within the team or chat where the app is installed.
For channels (team scope):
ChannelMessage.Read.Group(Application) - receive all channel messages without needing an @mentionChannelMessage.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) - receive all group chat messages without needing an @mention
Add RSC permissions using the Teams CLI:
teams app rsc add <teamsAppId> ChannelMessage.Read.Group --type Application
Example Teams manifest (redacted)
A minimal, valid example with the required fields. Replace 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)
bots[].botIdmust be identical to the Azure Bot App ID.webApplicationInfo.idis required to be the same as the Azure Bot App ID.bots[].scopeshas to list the surfaces you intend to utilize (personal,team,groupChat).bots[].supportsFiles: trueis mandatory for handling files in a personal scope.authorization.permissions.resourceSpecificneeds to include channel read and send permissions for channel traffic.
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 making changes, reinstall the app in every team, then completely exit and restart Teams (not merely closing the window) to flush cached app metadata.
Updating the manifest manually (without CLI)
- Modify
manifest.jsonwith the updated configuration. - Raise the
versionvalue (for instance,1.0.0→1.1.0). - Recreate the zip archive containing the manifest and icons (
manifest.json,outline.png,color.png). - Upload the new archive:
- Teams Admin Center: Go to Teams apps → Manage apps → locate your app → Upload new version.
- Sideload: Open Teams → Apps → Manage your apps → Upload a custom app.
Capabilities: RSC only vs Graph
With Teams RSC only (app installed, no Graph API permissions)
Functional:
- Extracting text from channel messages.
- Posting text content to channels.
- Accepting file attachments in direct messages (DMs).
Non-functional:
- Image or file content in channels or groups (only an HTML stub is included in the payload).
- Retrieving attachments saved in SharePoint or OneDrive.
- Accessing message history beyond the live webhook event.
With Teams RSC + Microsoft Graph Application permissions
Enhancements:
- Fetching hosted content (such as images pasted into messages).
- Retrieving file attachments stored in SharePoint or OneDrive.
- Accessing channel and chat message history through Graph.
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) |
Key takeaway: Use RSC for live monitoring and the Graph API for accessing past messages. To retrieve missed messages during offline periods, the Graph API with ChannelMessage.Read.All is necessary (admin consent required).
Graph-enabled media + history
Activate only the Microsoft Graph application permissions needed for the Teams scopes and data you work with:
- In Entra ID (Azure AD) App Registration, add Graph Application permissions:
ChannelMessage.Read.Allfor channel attachments and channel history.Chat.Read.Allfor group-chat attachments and group-chat history.Files.Read.Allwhen downloading attachment bytes from SharePoint or OneDrive storage; not needed for history-only setups.
- Grant admin consent for the tenant.
- Increase the Teams app manifest version, re-upload it, and reinstall the app in Teams.
- Completely exit and restart Teams to clear cached app metadata.
Channel/group file recovery (graphMediaFallback)
Teams may strip file markers from the HTML activity sent to a bot. When this happens, the Bot Framework activity looks just like a regular HTML message, and the full attachment reference exists only on the Graph copy of the message.
Enable this fallback after granting the permissions listed above:
{
channels: {
msteams: {
graphMediaFallback: true,
},
},
}
This setting applies only to channels and group chats. It triggers one Graph message lookup whenever an HTML activity lacks directly downloadable media, including ordinary messages or those with only mentions. The default is false, so existing installations avoid extra Graph traffic or permission errors automatically.
User mentions: @mentions work without extra setup for users already in the conversation. To dynamically search for and mention users not in the current conversation, add the User.Read.All (Application) permission and grant admin consent.
Known limitations
Webhook timeouts
Teams sends messages through an HTTP webhook. OpenClaw applies fixed HTTP server timeouts to that webhook listener: 30 seconds for inactivity, 30 seconds for the total request, and 15 seconds to receive headers. Optional inbound media and context enrichment share a 10-second budget. The SDK returns after the raw activity is durably appended; the agent turn runs 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-based Teams path is live-validated for the Microsoft Teams public cloud.
Inbound replies use the incoming Teams SDK turn context. Out-of-context proactive operations, such as sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies, use the stored conversation reference serviceUrl. The public cloud defaults to the Teams SDK public cloud environment and allows stored references on the public Teams Connector host: https://smba.trafficmanager.net/.
The public cloud is the default configuration. 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 or 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 |
Example for GCC, where Microsoft documents a separate proactive service URL but the Teams SDK exposes no separate GCC cloud preset:
{
"channels": {
"msteams": {
"serviceUrl": "https://smba.infra.gcc.teams.microsoft.com/teams"
}
}
}
Example for GCC High:
{
"channels": {
"msteams": {
"cloud": "USGov",
"serviceUrl": "https://smba.infra.gov.teams.microsoft.us/teams"
}
}
}
channels.msteams.serviceUrl is restricted to supported Microsoft Teams Bot Connector hosts. When a service URL is configured, OpenClaw checks that the stored conversation serviceUrl uses the same host before proactive sends, edits, deletes, cards, polls, or queued long-running replies run. With the default public-cloud config, OpenClaw fails closed if a stored conversation points outside the public Teams Connector host. Receive a fresh message from the conversation after changing cloud or service URL settings so the stored conversation reference is current.
The smba endpoint for global proactive messaging is not available separately for China/21Vianet in Microsoft's Teams proactive endpoint table. Set up cloud: "China" so the Teams SDK points to Azure China's authentication, token, and JWT endpoints. For proactive sends, you need either a stored conversation reference from an incoming China Teams activity or an explicitly set 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 traffic through the Azure China Graph endpoint.
Formatting
Teams markdown is more constrained than what Slack or Discord offer:
- Simple formatting like bold, italic,
code, and links works. - Complex markdown such as tables or nested lists may not display correctly.
- Adaptive Cards are available for polls and semantic presentation sends (details below).
Configuration
Essential configuration options (shared channel patterns are covered in /gateway/configuration):
channels.msteams.enabled: turns the channel on or off.channels.msteams.appId,channels.msteams.appPassword,channels.msteams.tenantId: authentication details for the bot.channels.msteams.cloud: the cloud environment for the Teams SDK (options arePublic,USGov,USGovDoD, orChina;Publicis the default). For USGov or DoD SDK clouds, configure it withserviceUrl. China relies on the SDK preset and stored Azure China Bot Framework conversation references, and Graph-backed helpers stay disabled until Azure China Graph routing becomes available.channels.msteams.serviceUrl: the Bot Connector service URL boundary for SDK proactive operations. The public cloud uses the SDK default; specify it for GCC (https://smba.infra.gcc.teams.microsoft.com/teams), GCC High, or DoD. 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: a DM allowlist (AAD object IDs are recommended). When Graph access is available, the wizard resolves names to IDs during setup.channels.msteams.dangerouslyAllowNameMatching: a break-glass toggle that re-enables mutable UPN or display-name matching and direct team or channel name routing.channels.msteams.textChunkLimit: the size of outbound text chunks in characters (defaults to4000, and is hard-capped at4000even if a higher value is configured).channels.msteams.streaming.chunkMode: set tolength(default) ornewlineto split on blank lines (paragraph boundaries) before length chunking.channels.msteams.mediaAllowHosts: an allowlist for inbound attachment hosts (defaults to Microsoft or Teams domains: Graph, SharePoint or OneDrive, Teams CDN, Bot Framework, Azure Media Services).channels.msteams.mediaAuthAllowHosts: an allowlist for attaching Authorization headers on media retries (defaults to Graph and Bot Framework hosts).channels.msteams.graphMediaFallback: opts into Graph message lookups when channel or group HTML omits file markers (defaults tofalse; refer to Channel/group file recovery).channels.msteams.mediaMaxMb: a per-channel media size limit override in MB. Falls back toagents.defaults.mediaMaxMbwhen not set.channels.msteams.requireMention: requires an @mention in channels or groups (defaults totrue).channels.msteams.replyStyle:thread | top-level(see Reply style).channels.msteams.teams.<teamId>.replyStyle: a per-team override.channels.msteams.teams.<teamId>.requireMention: a per-team override.channels.msteams.teams.<teamId>.tools: default per-team tool policy overrides (allow/deny/alsoAllow) used when a channel override is absent.channels.msteams.teams.<teamId>.toolsBySender: default per-team per-sender tool policy overrides (the"*"wildcard is supported).channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle: a per-channel override.channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention: a 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 (the"*"wildcard is supported).- Prefixes for
toolsBySenderkeys should be explicit:channel:,id:,e164:,username:,name:(older keys without prefixes still map only toid:). channels.msteams.authType: determines authentication type, either"secret"(default) or"federated".channels.msteams.certificatePath: file path to a PEM certificate (used for federated and certificate authentication).channels.msteams.certificateThumbprint: certificate thumbprint; accepted but not mandatory for authentication.channels.msteams.useManagedIdentity: enables 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 (refer to 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 prepended to outbound replies.channels.msteams.feedbackEnabled(default istrue),channels.msteams.feedbackReflection(default istrue),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 use the main session (
agent:<agentId>:<mainKey>). - Channel and group messages rely on the conversation id:
agent:<agentId>:msteams:channel:<conversationId>agent:<agentId>:msteams:group:<conversationId>
- Direct messages use the main session (
Reply style: threads vs posts
Teams provides two channel UI styles that share 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 reveal which UI style a channel uses. Using the wrong replyStyle leads to:
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 requireMention: false is set globally without an explicit replyStyle, mentions in Posts-style channels appear as top-level posts even when the inbound message was a thread reply. Pin replyStyle: "thread" at the global, team, or channel level to prevent 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 was @mentioned within a channel thread, OpenClaw reconnects the original thread root to the outbound conversation reference (19:...@thread.tacv2;messageid=<root>), ensuring the reply appears in the same thread. This applies both to live in-turn sends and to proactive sends after the Bot Framework turn context has expired (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. Older stored references that predate threadId fall back to activityId (using whichever inbound activity last seeded the conversation), so existing 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 appended. This behavior is correct for Threads-style channels. If you expected threaded replies but got top-level posts, then replyStyle is set incorrectly 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 contains 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 or comment, andfilename(ortitle) overrides the uploaded filename.
Without Graph permissions, channel messages containing images arrive as text only (the image content remains inaccessible to the bot).
By default, OpenClaw downloads media only from Microsoft/Teams hostnames. Override this with channels.msteams.mediaAllowHosts (use ["*"] to allow any host).
Authorization headers are attached only for hosts listed in channels.msteams.mediaAuthAllowHosts (defaults to Graph and Bot Framework hosts). Keep this list restrictive and 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, whereas Microsoft Graph's /me resource requires a signed-in user. To send files in group chats or 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 because 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 cause the send to fail instead of broadening access to the entire 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 placed in a /OpenClawShared/ folder within the configured SharePoint site's default document library.
Polls (Adaptive Cards)
OpenClaw delivers Teams polls as Adaptive Cards (no native Teams poll API exists).
- CLI:
openclaw message poll --channel msteams --target conversation:<id> --poll-question "..." --poll-option "..." --poll-option "...". - The gateway records votes 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 remain online to capture votes.
- Polls do not automatically post result summaries, and no poll-results CLI is available 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 becomes optional. Buttons render as Adaptive Card submit or URL actions. Select menus are not natively supported 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 use prefixes to differentiate between users and 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 omitted, names are resolved as groups or teams. Always prependuser:when addressing users by their display name.
Proactive messaging
- Proactive messages can only be sent after a user has engaged, since OpenClaw saves conversation references at that moment.
- Refer to /gateway/configuration for
dmPolicyand allowlist gating.
Team and Channel IDs (Common Gotcha)
The groupId query parameter inside Teams URLs is not the team ID used for configuration. Instead, pull IDs from the URL path:
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 example19:Bk4j...@thread.tacv2; older tenants might show@thread.skype, which is also acceptable). - Channel key = the path segment following
/channel/(URL-decoded). - Ignore the
groupIdquery parameter for OpenClaw routing. It is the Microsoft Entra group ID, not the Bot Framework conversation ID used in incoming Teams activities.
Private channels
Bot support in private channels is restricted:
| 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 when private channels fail:
- Use standard channels for bot interactions.
- Use DMs; users can always message the bot directly.
- Use Graph API for historical access (requires
ChannelMessage.Read.All).
Troubleshooting
Common issues
- Images not appearing in channels: Graph permissions or admin consent is 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 and re-add the app, then fully quit Teams to refresh.
- 401 Unauthorized from webhook: expected when testing manually without an Azure JWT; it means the endpoint is reachable but authentication 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. Locate 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