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-anonymous is 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, set channels.msteams.groupAllowFrom, or use groupPolicy: "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.allowFrom should use stable AAD object IDs or static sender access groups such as accessGroup: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 add groupAllowFrom). channels.defaults.groupPolicy can override the shared default when channels.msteams.groupPolicy is unset.
  • channels.msteams.groupAllowFrom controls which senders or static sender access groups can trigger in group chats/channels (falls back to channels.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/Channel entries 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: true is set.

Example:

{
  channels: {
    msteams: {
      groupPolicy: "allowlist",
      teams: {
        "My Team": {
          channels: {
            General: { requireMention: true },
          },
        },
      },
    },
  },
}

Manual setup (without the Teams CLI)

How it works

  1. Confirm the Microsoft Teams plugin is available (bundled in current releases).
  2. Create an Azure Bot (App ID + secret + tenant ID).
  3. Build a Teams app package referencing the bot, including the RSC permissions below.
  4. Upload/install the Teams app into a team (or personal scope for DMs).
  5. Configure msteams in ~/.openclaw/openclaw.json (or env vars) and start the gateway.
  6. The gateway listens for Bot Framework webhook traffic on /api/messages by default.

Step 1: Create Azure Bot

  1. Navigate to Create an Azure Bot.

  2. Configure the Basics tab with these values:

    FieldValue
    Bot handleA unique name for your bot, for instance openclaw-msteams
    SubscriptionChoose your Azure subscription
    Resource groupEither create a new one or pick an existing
    Pricing tierSelect Free for development or testing
    Type of AppSingle Tenant (recommended; see the warning below)
    Creation typeCreate 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.

  1. Select Review + create, then Create (deployment takes about 1, 2 minutes).

Step 2: Get credentials

  1. From the Azure Bot resource, open Configuration and copy the Microsoft App ID (this is your appId).
  2. Under Manage Password, go to App Registration, then Certificates & secrets. Click New client secret and copy the Value (this is your appPassword).
  3. On the Overview page, copy the Directory (tenant) ID (this is your tenantId).

Step 3: Configure messaging endpoint

  1. In your Azure Bot, go to Configuration.
  2. Specify the Messaging endpoint:
    • For production: https://your-domain.com/api/messages
    • For local development: use a tunnel (refer to Local development)

Step 4: Enable Teams channel

  1. In your Azure Bot, open Channels.
  2. Select Microsoft Teams, then Configure, and finally Save.
  3. Accept the Terms of Service when prompted.

Step 5: Build Teams app manifest

  • Add a bot entry that contains botId = <App ID>.
  • Set scopes to personal, team, and groupChat.
  • Include supportsFiles: true (mandatory when handling files in the personal scope).
  • Configure RSC permissions (see RSC permissions).
  • Prepare two icon files: outline.png (32x32) and color.png (192x192).
  • Package manifest.json, outline.png, and color.png into 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:

  1. Obtain or create a certificate in PEM format that includes the private key.
  2. 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=federated
  • MSTEAMS_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:

  1. The bot's pod or VM is assigned a managed identity (either system-assigned or user-assigned).
  2. A federated identity credential connects that managed identity to the Entra ID app registration.
  3. At runtime, OpenClaw calls @azure/identity to request tokens from the Azure IMDS endpoint.
  4. 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=federated
  • MSTEAMS_USE_MANAGED_IDENTITY=true
  • MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id> (only for user-assigned)

AKS Workload Identity setup

For AKS deployments using workload identity:

  1. Enable workload identity on your AKS cluster.

  2. 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"]
    }'
    
  3. 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>"
    
  4. Label the pod so that workload identity is injected:

    metadata:
      labels:
        azure.workload.identity/use: "true"
    
  5. Allow network access to IMDS (169.254.169.254): if you use NetworkPolicy, add an egress rule for 169.254.169.254/32 on port 80.

Auth type comparison

MethodConfigProsCons
Client secretappPasswordStraightforward to set upRequires secret rotation, less secure
CertificateauthType: "federated" + certificatePathNo shared secret transmittedOverhead of certificate management
Managed IdentityauthType: "federated" + useManagedIdentityNo passwords, no secrets to handleOnly 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:

  1. Deploy the Teams app (use the install link from teams app get <id> --install-link).
  2. Locate the bot in Teams and send it a direct message.
  3. 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 varConfig keyNotes
MSTEAMS_APP_IDappId
MSTEAMS_APP_PASSWORDappPassword
MSTEAMS_TENANT_IDtenantId
MSTEAMS_AUTH_TYPEauthType"secret" or "federated"
MSTEAMS_CERTIFICATE_PATHcertificatePathfederated plus certificate
MSTEAMS_CERTIFICATE_THUMBPRINTcertificateThumbprintaccepted, not mandatory for auth
MSTEAMS_USE_MANAGED_IDENTITYuseManagedIdentityfederated plus managed identity
MSTEAMS_MANAGED_IDENTITY_CLIENT_IDmanagedIdentityClientIdonly 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.Group and TeamMember.Read.Group RSC 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.historyLimit controls how many recent channel or group chat messages are included in the prompt. Falls back to messages.groupChat.historyLimit, then defaults to 50. Set 0 to 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 @mention
  • ChannelMessage.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[].botId must be identical to the Azure Bot App ID.
  • webApplicationInfo.id is required to be the same as the Azure Bot App ID.
  • bots[].scopes has to list the surfaces you intend to utilize (personal, team, groupChat).
  • bots[].supportsFiles: true is mandatory for handling files in a personal scope.
  • authorization.permissions.resourceSpecific needs 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)

  1. Modify manifest.json with the updated configuration.
  2. Raise the version value (for instance, 1.0.01.1.0).
  3. Recreate the zip archive containing the manifest and icons (manifest.json, outline.png, color.png).
  4. 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

CapabilityRSC permissionsGraph API
Real-time messagesYes (via webhook)No (polling only)
Historical messagesNoYes (can query history)
Setup complexityApp manifest onlyRequires admin consent + token flow
Works offlineNo (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:

  1. In Entra ID (Azure AD) App Registration, add Graph Application permissions:
    • ChannelMessage.Read.All for channel attachments and channel history.
    • Chat.Read.All for group-chat attachments and group-chat history.
    • Files.Read.All when downloading attachment bytes from SharePoint or OneDrive storage; not needed for history-only setups.
  2. Grant admin consent for the tenant.
  3. Increase the Teams app manifest version, re-upload it, and reinstall the app in Teams.
  4. 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.cloud selects the Teams SDK cloud preset for authentication, JWT validation, token services, and Graph scope.
  • channels.msteams.serviceUrl selects 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 SDK China preset 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 environmentOpenClaw configProactive serviceUrl
Publicno cloud/serviceUrl config neededhttps://smba.trafficmanager.net/teams
GCCset serviceUrl; no separate Teams SDK cloud preset existshttps://smba.infra.gcc.teams.microsoft.com/teams
GCC Highcloud: "USGov" + serviceUrlhttps://smba.infra.gov.teams.microsoft.us/teams
DoDcloud: "USGovDoD" + serviceUrlhttps://smba.infra.dod.teams.microsoft.us/teams
China/21Vianetcloud: "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 are Public, USGov, USGovDoD, or China; Public is the default). For USGov or DoD SDK clouds, configure it with serviceUrl. 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 to 3978).
  • channels.msteams.webhook.path (defaults to /api/messages).
  • channels.msteams.dmPolicy: pairing | allowlist | open | disabled (defaults to pairing).
  • 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 to 4000, and is hard-capped at 4000 even if a higher value is configured).
  • channels.msteams.streaming.chunkMode: set to length (default) or newline to 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 to false; refer to Channel/group file recovery).
  • channels.msteams.mediaMaxMb: a per-channel media size limit override in MB. Falls back to agents.defaults.mediaMaxMb when not set.
  • channels.msteams.requireMention: requires an @mention in channels or groups (defaults to true).
  • 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 toolsBySender keys should be explicit: channel:, id:, e164:, username:, name: (older keys without prefixes still map only to id:).
  • 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 is true), channels.msteams.feedbackReflection (default is true), 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: true requires sso.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>

Reply style: threads vs posts

Teams provides two channel UI styles that share the same underlying data model:

StyleDescriptionRecommended replyStyle
Posts (classic)Messages appear as cards with threaded replies underneaththread (default)
Threads (Slack-like)Messages flow linearly, more like Slacktop-level

The problem: the Teams API does not reveal which UI style a channel uses. Using the wrong replyStyle leads to:

  • thread in a Threads-style channel → replies appear nested awkwardly.
  • top-level in 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:

  1. Per-channel - channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle
  2. Per-team - channels.msteams.teams.<teamId>.replyStyle
  3. Global - channels.msteams.replyStyle
  4. Implicit default - derived from requireMention:
    • requireMention: truethread
    • requireMention: falsetop-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-file with media / filePath / path; optional message becomes the accompanying text or comment, and filename (or title) 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:

ContextHow files are sentSetup needed
DMsFileConsentCard → user accepts → bot uploadsWorks out of the box
Group chats/channelsUpload to SharePoint → native file cardRequires sharePointSiteId + Graph permissions
Images (any context)Base64-encoded inlineWorks 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

  1. 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.All also works and already covers this when group-chat history is enabled. As a per-chat alternative, use the ChatMember.Read.Chat resource-specific consent permission.
  2. Grant admin consent for the tenant.

  3. 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"
    
  4. Configure OpenClaw:

    {
      channels: {
        msteams: {
          // ... other config ...
          sharePointSiteId: "contoso.sharepoint.com,guid1,guid2",
        },
      },
    }
    

Sharing behavior

Context and permissionSharing behavior
Channel + Sites.ReadWrite.AllOrganization-wide sharing link (anyone in org can access)
Group chat + Sites.ReadWrite.All + a supported chat-member read grantPer-user sharing link (only chat members can access)
Group chat without a supported chat-member read grantSend 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

ScenarioResult
Group chat + file + SharePoint and member permissions configuredUpload to SharePoint, send a native file card
Group chat + file + missing SharePoint or member permissionsFail with an actionable configuration error
Channel + file + sharePointSiteId configuredUpload to SharePoint, send a native file card
Personal chat + fileFileConsentCard flow (works without SharePoint)
Any context + imageBase64-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.json files are imported by openclaw 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 typeFormatExample
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/channelconversation:<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 prepend user: 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 dmPolicy and 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 example 19:Bk4j...@thread.tacv2; older tenants might show @thread.skype, which is also acceptable).
  • Channel key = the path segment following /channel/ (URL-decoded).
  • Ignore the groupId query 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:

FeatureStandard channelsPrivate channels
Bot installationYesLimited
Real-time messages (webhook)YesMay not work
RSC permissionsYesMay behave differently
@mentionsYesIf bot is accessible
Graph API historyYesYes (with permissions)

Workarounds when private channels fail:

  1. Use standard channels for bot interactions.
  2. Use DMs; users can always message the bot directly.
  3. 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=false or 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 for color.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

  1. Verify webApplicationInfo.id matches your bot's App ID exactly.
  2. Re-upload the app and reinstall in the team/chat.
  3. Check if your org admin has blocked RSC permissions.
  4. Confirm you are using the right scope: ChannelMessage.Read.Group for teams, ChatMessage.Read.Chat for group chats.

References