Microsoft Teams Bot Setup, Capabilities, and Configuration

Learn how to configure Microsoft Teams bots in OpenClaw, including supported features like file uploads, Adaptive Cards, and bundled plugin installation. Essential for developers setting up Teams integration.

Read this when

  • Working on Microsoft Teams channel features

Status: text and DM attachments are supported; sending files to channels or groups requires sharePointSiteId plus Graph permissions (see Sending files in group chats). Polls and approval prompts go out as Adaptive Cards. Message actions surface explicit upload-file for file-first sends.

Bundled plugin

Microsoft Teams comes bundled as a plugin in current OpenClaw releases; the standard packaged build needs no separate installation.

On an older build or a custom install without bundled Teams, install the npm package directly:

openclaw plugins install @openclaw/msteams

Use the bare package to track the current official release tag. Pin an exact version only when you need a reproducible install.

Local checkout (running from a git repo):

openclaw plugins install ./path/to/local/msteams-plugin

Details: Plugins

Quick setup

@microsoft/teams.cli handles bot registration, manifest creation, and credential generation in one command.

1. Install and log in

npm install -g @microsoft/teams.cli@preview
teams login
teams status   # verify you're logged in and see your tenant info

Note

The Teams CLI is currently in preview. Commands and flags may change between releases.

2. Start a tunnel (Teams cannot reach localhost)

Install and authenticate the devtunnel CLI if needed (getting started guide).

# One-time setup (persistent URL across sessions):
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto

# Each dev session:
devtunnel host my-openclaw-bot
# Your endpoint: https://<tunnel-id>.devtunnels.ms/api/messages

Note

--allow-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

  • Talk to OpenClaw via Teams DMs, group chats, or channels.
  • Keep routing deterministic: replies always go back to the channel they arrived on.
  • Default to safe channel behavior (mentions required unless configured otherwise).

Config writes

By default, Microsoft Teams can write config updates triggered by /config set|unset (requires commands.config: true).

Disable with:

{
  channels: { msteams: { configWrites: false } },
}

Access control (DMs + groups)

Microsoft Teams has one account per channel configuration. Set policies directly under channels.msteams; an accounts map is not supported.

DM access

  • Default: channels.msteams.dmPolicy = "pairing". Unknown senders are ignored until approved.
  • channels.msteams.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). Set channels.msteams.groupPolicy explicitly to choose another policy; the root schema default takes precedence over channels.defaults.groupPolicy.
  • channels.msteams.groupAllowFrom controls which senders, static sender access groups, or group/channel conversation IDs can trigger in group chats/channels (falls back to channels.msteams.allowFrom). Conversation IDs can use 19:...@thread.tacv2, 19:...@thread.v2, or 19:...@thread.skype; preserve the exact ID casing. OpenClaw ignores ;messageid=... suffixes. Conversation IDs never grant personal-DM access.
  • Set groupPolicy: "open" to allow any member (still mention-gated by default).
  • To block all channels, set channels.msteams.groupPolicy: "disabled".

Example:

{
  channels: {
    msteams: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["00000000-0000-0000-0000-000000000000", "accessGroup:core-team"],
    },
  },
}

Team + channel allowlist

  • To limit replies to specific groups and channels, enumerate the teams and channels beneath channels.msteams.teams.
  • Prefer stable Teams conversation IDs obtained from Teams links as lookup keys over changeable display names (refer to Team and Channel IDs).
  • If groupPolicy="allowlist" is set and a teams allowlist exists, only the teams and channels on that list are permitted (mention-gated).
  • groupAllowFrom grants permission to group senders, not to delegated Graph access for other channels. When an existing configuration only specifies groupAllowFrom, retain the default groupPolicy: "allowlist" and set the target under channels.msteams.teams.<team>.channels.
  • As an alternative, explicitly choose groupPolicy: "open" to allow broader delegated reads. This also lets any group sender through (still mention-gated by default), making it less restrictive than a scoped team or channel route.
  • Reads performed directly by the operator or within the active conversation do not require an extra team or channel route.
  • The configuration wizard accepts Team/Channel entries and saves them on your behalf.
  • At startup, OpenClaw converts team, channel, and user allowlist names into IDs (provided Graph permissions allow it) and records the translation. Names that cannot be resolved stay as entered but are skipped for routing unless channels.msteams.dangerouslyAllowNameMatching: true is configured.

Example:

{
  channels: {
    msteams: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["00000000-0000-0000-0000-000000000000"],
      teams: {
        "19:team-id@thread.tacv2": {
          channels: {
            "19:channel-id@thread.tacv2": { requireMention: true },
          },
        },
      },
    },
  },
}

Manual configuration (bypassing the Teams CLI)

How it works

  1. Confirm the Microsoft Teams plugin is present (included with current releases).
  2. Set up an Azure Bot (App ID, secret, and tenant ID).
  3. Assemble a Teams app package that references the bot and includes the RSC permissions listed below.
  4. Deploy the Teams app into a team (or personal scope for direct messages).
  5. Define msteams in ~/.openclaw/openclaw.json (or via environment variables) and launch the gateway.
  6. By default, the gateway listens for Bot Framework webhook traffic on /api/messages.

Step 1: Create Azure Bot

  1. Navigate to Create Azure Bot

  2. Complete the Basics tab:

    FieldValue
    Bot handleYour bot name, e.g., openclaw-msteams (must be unique)
    SubscriptionSelect your Azure subscription
    Resource groupCreate new or use existing
    Pricing tierFree for dev/testing
    Type of AppSingle Tenant (recommended; see note below)
    Creation typeCreate new Microsoft App ID

Warning

After 2025-07-31, creating new multi-tenant bots was deprecated. For new bots, choose Single Tenant.

  1. Press Review + create, then Create (takes about 1-2 minutes).

Step 2: Get credentials

  1. In the Azure Bot resource, open Configuration and copy the Microsoft App ID (your appId).
  2. Under Manage Password, go to App Registration, select Certificates & secrets, create a New client secret, and copy the Value (your appPassword).
  3. From Overview, copy the Directory (tenant) ID (your tenantId).

Step 3: Configure messaging endpoint

  1. Open the Azure Bot and go to Configuration.
  2. Specify the Messaging endpoint:
    • Production: https://your-domain.com/api/messages
    • Local development: rely on a tunnel (see Local development)

Step 4: Enable Teams channel

  1. In the Azure Bot, select Channels.
  2. Choose Microsoft Teams, click Configure, then Save.
  3. Accept the Terms of Service.

Step 5: Build Teams app manifest

  • Add a bot entry that includes botId = <App ID>.
  • Scopes: personal, team, groupChat.
  • supportsFiles: true (needed for handling files in personal scope).
  • Include RSC permissions (see RSC permissions).
  • Prepare icons: outline.png (32x32) and color.png (192x192).
  • Combine manifest.json, outline.png, and color.png into a zip file.

Step 6: Configure OpenClaw

{
  channels: {
    msteams: {
      enabled: true,
      appId: "<APP_ID>",
      appPassword: "<APP_PASSWORD>",
      tenantId: "<TENANT_ID>",
      webhook: { port: 3978, path: "/api/messages" },
    },
  },
}

Environment variables: MSTEAMS_APP_ID, MSTEAMS_APP_PASSWORD, MSTEAMS_TENANT_ID.

Step 7: Run the gateway

The Teams channel launches automatically once the plugin is available and msteams configuration contains credentials.

Federated authentication (certificate plus managed identity)

In production, OpenClaw offers federated authentication as a substitute for client secrets, using channels.msteams.authType: "federated". There are two approaches:

Option A: Certificate-based authentication

Utilize a PEM certificate that is registered with your Entra ID app registration.

Setup steps:

  1. Create or acquire a certificate (PEM format with a private key).
  2. In Entra ID, go to App Registration, then Certificates & secrets, open Certificates, and upload the public certificate.

Configuration:

{
  channels: {
    msteams: {
      enabled: true,
      appId: "<APP_ID>",
      tenantId: "<TENANT_ID>",
      authType: "federated",
      certificatePath: "/path/to/cert.pem",
      webhook: { port: 3978, path: "/api/messages" },
    },
  },
}

Environment variables:

  • MSTEAMS_AUTH_TYPE=federated
  • MSTEAMS_CERTIFICATE_PATH=/path/to/cert.pem

Option B: Azure Managed Identity

Leverage Azure Managed Identity for passwordless authentication on Azure infrastructure (AKS, App Service, Azure VMs).

Operation:

  1. The bot pod or VM is assigned a managed identity (system or user assigned).
  2. A federated identity credential connects that managed identity to the Entra ID app registration.
  3. At runtime, OpenClaw obtains tokens from the Azure IMDS endpoint via @azure/identity.
  4. Those tokens are handed to the Teams SDK for bot authentication.

Requirements:

  • Azure infrastructure with managed identity enabled (AKS workload identity, App Service, VM).
  • A federated identity credential established on the Entra ID app registration.
  • Network access from the pod or VM to IMDS (169.254.169.254:80).

Configuration (system-assigned managed identity):

{
  channels: {
    msteams: {
      enabled: true,
      appId: "<APP_ID>",
      tenantId: "<TENANT_ID>",
      authType: "federated",
      useManagedIdentity: true,
      webhook: { port: 3978, path: "/api/messages" },
    },
  },
}

Configuration (user-assigned managed identity): append managedIdentityClientId: "<MI_CLIENT_ID>" to the block above.

Environment variables:

  • MSTEAMS_AUTH_TYPE=federated
  • MSTEAMS_USE_MANAGED_IDENTITY=true
  • MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id> (user-assigned only)

AKS Workload Identity setup

For AKS deployments that rely on workload identity:

  1. Turn on workload identity for the AKS cluster.

  2. Set up a federated identity credential on the Entra ID app registration:

    az ad app federated-credential create --id <APP_OBJECT_ID> --parameters '{
      "name": "my-bot-workload-identity",
      "issuer": "<AKS_OIDC_ISSUER_URL>",
      "subject": "system:serviceaccount:<NAMESPACE>:<SERVICE_ACCOUNT>",
      "audiences": ["api://AzureADTokenExchange"]
    }'
    
  3. Add an annotation to the Kubernetes service account carrying the app client ID:

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: my-bot-sa
      annotations:
        azure.workload.identity/client-id: "<APP_CLIENT_ID>"
    
  4. Label the pod so workload identity gets injected:

    metadata:
      labels:
        azure.workload.identity/use: "true"
    
  5. Open network access to IMDS (169.254.169.254): when NetworkPolicy is in use, include an egress rule targeting 169.254.169.254/32 over port 80.

Auth type comparison

MethodConfigProsCons
Client secretappPasswordStraightforward configurationRotation of secret needed, weaker security
CertificateauthType: "federated" + certificatePathNo secret transmitted over the networkExtra work managing certificates
Managed IdentityauthType: "federated" + useManagedIdentityNo password, nothing to manageDepends on Azure infrastructure

certificateThumbprint may be configured next to certificatePath, yet the auth path does not consult it at present; it exists purely for future compatibility.

Default behavior: when authType is not defined, OpenClaw authenticates with a client secret (appPassword). Configurations already in place continue to function without changes.

Local development (tunneling)

Teams has no way to reach localhost. Keep a persistent dev tunnel active so the URL does not change between sessions:

# One-time setup:
devtunnel create my-openclaw-bot --allow-anonymous
devtunnel port create my-openclaw-bot -p 3978 --protocol auto

# Each dev session:
devtunnel host my-openclaw-bot

Other options: ngrok http 3978 or tailscale funnel 3978 (these may produce a different URL on every session).

When the tunnel URL changes, point the endpoint at the new value:

teams app update <teamsAppId> --endpoint "https://<new-url>/api/messages"

Testing the bot

Run diagnostics:

teams app doctor <teamsAppId>

This single pass verifies the bot registration, AAD app, manifest, and SSO configuration.

Send a test message:

  1. Install the Teams app via the install link from teams app get <id> --install-link.
  2. Locate the bot inside Teams and send it a direct message.
  3. Watch the gateway logs for incoming activity.

Environment variables

The auth-related config keys below can be supplied through environment variables rather than openclaw.json (other keys, for instance groupPolicy or historyLimit, only work in config files):

Env 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 for user-assigned managed identity

Member info action

OpenClaw provides a Graph-backed member-info action for Microsoft Teams, letting agents and automations fetch verified roster details for a specified conversation.

Prerequisites:

  • ChannelSettings.Read.Group and TeamMember.Read.Group RSC permissions (these are already part of the recommended manifest).

The action is present whenever Graph credentials are configured; no separate channels.msteams.actions.memberInfo switch exists. Queries on standard channels return the matching team-roster identity, display name, email, and roles. Inside the current DM or group chat, the action can return the trusted sender's stable user ID. Lookups for private/shared-channel members or non-current chat participants demand extra roster permissions and get refused by the default permission baseline.

History context

  • channels.msteams.historyLimit determines how many recent channel/group messages get wrapped into the prompt. It falls back to messages.groupChat.historyLimit, then to 50 by default. Set 0 to switch it off.
  • Thread history that gets fetched is filtered by sender allowlists (allowFrom / groupAllowFrom), so thread context seeding only includes messages coming from approved senders.
  • Quoted attachment context (extracted from the Skype Reply-schema HTML inside a reply's own attachments) goes through without filtering; only thread-history seeding applies the sender-allowlist filter at this time.
  • DM history can be capped with channels.msteams.dmHistoryLimit (user turns). Per-user overrides: channels.msteams.dms["<user_id>"].historyLimit.

Current Teams RSC permissions (manifest)

The resourceSpecific permissions listed below are the ones already defined in our Teams app manifest. Their effect is limited to the team or chat where the app has been installed.

For channels (team scope):

  • ChannelMessage.Read.Group (Application) - get every channel message, no @mention needed
  • 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) - get every group chat message, no @mention needed

The Teams CLI is the tool to use for adding RSC permissions:

teams app rsc add <teamsAppId> ChannelMessage.Read.Group --type Application

Example Teams manifest (redacted)

A minimal, valid example containing all required fields. Swap in your own IDs and URLs.

{
  $schema: "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
  manifestVersion: "1.23",
  version: "1.0.0",
  id: "00000000-0000-0000-0000-000000000000",
  name: { short: "OpenClaw" },
  developer: {
    name: "Your Org",
    websiteUrl: "https://example.com",
    privacyUrl: "https://example.com/privacy",
    termsOfUseUrl: "https://example.com/terms",
  },
  description: { short: "OpenClaw in Teams", full: "OpenClaw in Teams" },
  icons: { outline: "outline.png", color: "color.png" },
  accentColor: "#5B6DEF",
  bots: [
    {
      botId: "11111111-1111-1111-1111-111111111111",
      scopes: ["personal", "team", "groupChat"],
      isNotificationOnly: false,
      supportsCalling: false,
      supportsVideo: false,
      supportsFiles: true,
    },
  ],
  webApplicationInfo: {
    id: "11111111-1111-1111-1111-111111111111",
  },
  authorization: {
    permissions: {
      resourceSpecific: [
        { name: "ChannelMessage.Read.Group", type: "Application" },
        { name: "ChannelMessage.Send.Group", type: "Application" },
        { name: "Member.Read.Group", type: "Application" },
        { name: "Owner.Read.Group", type: "Application" },
        { name: "ChannelSettings.Read.Group", type: "Application" },
        { name: "TeamMember.Read.Group", type: "Application" },
        { name: "TeamSettings.Read.Group", type: "Application" },
        { name: "ChatMessage.Read.Chat", type: "Application" },
      ],
    },
  },
}

Manifest caveats (must-have fields)

  • The value of bots[].botId must equal the Azure Bot App ID.
  • The value of webApplicationInfo.id must equal the Azure Bot App ID.
  • bots[].scopes has to cover the surfaces you intend to work with (personal, team, groupChat).
  • File handling in personal scope depends on bots[].supportsFiles: true being set.
  • Channel traffic needs authorization.permissions.resourceSpecific to include both read and send permissions for channels.

Updating an existing app

# Download, edit, and re-upload the manifest
teams app manifest download <teamsAppId> manifest.json
# Edit manifest.json locally...
teams app manifest upload manifest.json <teamsAppId>
# Version is auto-bumped if content changed

After the update, the app needs to be reinstalled in every team. Also, completely exit and restart Teams (closing the window alone is not enough) so that cached app metadata gets cleared.

Manual manifest update (without CLI)

  1. Apply the new settings to manifest.json.
  2. Raise the version value (for instance, 1.0.01.1.0).
  3. Recreate the zip for the manifest along with its icons (manifest.json, outline.png, color.png).
  4. Upload the fresh zip:
    • Teams Admin Center: Teams apps → Manage apps → locate your app → Upload new version.
    • Sideload: Teams → Apps → Manage your apps → Upload a custom app.

Capabilities: RSC only vs Graph

With Teams RSC only (app installed, no Graph API permissions)

What functions:

  • Reading the text portion of channel messages.
  • Sending text content to channels.
  • Getting personal (DM) file attachments.

What does not function:

  • Channel/group images or file contents (the payload carries only an HTML stub).
  • Fetching attachments that live in SharePoint/OneDrive.
  • Accessing message history beyond the live webhook event.

With Teams RSC + Microsoft Graph Application permissions

What it adds:

  • Fetching hosted content (for example, images pasted into messages).
  • Fetching file attachments that live in SharePoint/OneDrive.
  • Using Graph to read channel/chat message history.

RSC vs Graph API

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)

Bottom line: RSC handles real-time listening; Graph API handles historical access. If you need to retrieve missed messages while offline, Graph API with ChannelMessage.Read.All is the way to go (admin consent is required).

Graph-enabled media + history

Turn on only the Microsoft Graph application permissions that match the Teams scopes and data you actually use:

  1. In Entra ID (Azure AD) App Registration, add Graph Application permissions:
    • ChannelMessage.Read.All covers channel attachments and channel history.
    • Chat.Read.All covers group-chat attachments and group-chat history.
    • Files.Read.All is needed only when attachment bytes must be pulled from SharePoint/OneDrive storage; history-only setups can skip it.
  2. Grant admin consent at the tenant level.
  3. Increase the Teams app manifest version, re-upload it, and reinstall the app in Teams.
  4. Completely exit and restart Teams so cached app metadata is cleared.

Channel/group file recovery (graphMediaFallback)

Teams has the ability to strip file markers from the HTML activity sent to a bot. When that happens, the Bot Framework activity looks exactly like a normal HTML message; the full attachment reference exists only on the Graph copy of the message.

After granting the permissions above, enable the fallback:

{
  channels: {
    msteams: {
      graphMediaFallback: true,
    },
  },
}

This applies only to channels and group chats. It triggers one Graph message lookup whenever an HTML activity came through without any directly downloadable media, including ordinary or mention-only messages. The default is false, so existing installations will not automatically see extra Graph traffic or permission errors.

User mentions: @mentions function immediately for users who are already part of the conversation. To search for and mention users who are not in the current conversation, add the User.Read.All (Application) permission and get admin consent.

Known limitations

Webhook timeouts

Messages arrive through an HTTP webhook from Teams. OpenClaw sets fixed HTTP server timeouts on that webhook listener: 30s of inactivity, 30s for the total request, and 15s to receive headers. Optional inbound media and context enrichment share a 10-second budget. The SDK returns once the raw activity is durably appended; the agent turn drains independently and replies proactively. If request handling or durable admission misses the transport window, Teams may retry the activity, and the ingress tombstone rejects a repeated event ID.

Teams cloud and service URL support

This SDK-backed Teams path is live-validated for Microsoft Teams public cloud.

Inbound replies use the incoming Teams SDK turn context. Out-of-context proactive operations - sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies - use the stored conversation reference serviceUrl. Public cloud defaults to the Teams SDK public cloud environment and allows stored references on the public Teams Connector host: https://smba.trafficmanager.net/.

Public cloud is the default. You do not need to set channels.msteams.cloud or channels.msteams.serviceUrl for normal public-cloud bots.

For non-public Teams clouds, set cloud and the matching proactive boundary when Microsoft publishes one:

  • channels.msteams.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/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

For GCC, Microsoft documents a distinct proactive service URL, yet the Teams SDK lacks a dedicated GCC cloud preset. Here is an example:

{
  "channels": {
    "msteams": {
      "serviceUrl": "https://smba.infra.gcc.teams.microsoft.com/teams"
    }
  }
}

And here is one for GCC High:

{
  "channels": {
    "msteams": {
      "cloud": "USGov",
      "serviceUrl": "https://smba.infra.gov.teams.microsoft.us/teams"
    }
  }
}

Only supported Microsoft Teams Bot Connector hosts can use channels.msteams.serviceUrl. When a service URL is set, OpenClaw verifies that the stored conversation serviceUrl matches that host before executing proactive sends, edits, deletes, cards, polls, or queued long-running replies. Under the default public-cloud setup, OpenClaw fails closed if a stored conversation points to a host outside the public Teams Connector. After altering cloud or service URL settings, receive a fresh message from the conversation to ensure the stored reference is up to date.

Microsoft's Teams proactive endpoint table lists no separate global smba URL for China/21Vianet. Set cloud: "China" so the Teams SDK uses Azure China auth, token, and JWT endpoints. Proactive sends then need either a stored conversation reference from an incoming China Teams activity or an explicitly configured service URL on the Azure China Bot Framework channel boundary (*.botframework.azure.cn). Graph-backed Teams helpers stay disabled for cloud: "China" until OpenClaw directs Graph requests through the Azure China Graph endpoint.

Formatting

Teams markdown is more limited than Slack or Discord:

  • Basic formatting works: bold, italic, code, links.
  • Complex markdown (tables, nested lists) may not render correctly.
  • Adaptive Cards are supported for approval prompts, polls, and semantic presentation sends (see below).

Configuration

Key settings (see /gateway/configuration for shared channel patterns):

  • channels.msteams.enabled: turns the channel on or off.
  • channels.msteams.appId, channels.msteams.appPassword, channels.msteams.tenantId: credentials for the bot.
  • channels.msteams.cloud: which Teams SDK cloud environment applies (Public, USGov, USGovDoD, or China; Public is the default). For USGov or DoD SDK clouds, set this via serviceUrl; China relies on the SDK preset and stored Azure China Bot Framework conversation references, with Graph-backed helpers staying disabled until Azure China Graph routing becomes available.
  • channels.msteams.serviceUrl: the Bot Connector service URL boundary for SDK proactive operations. The SDK default is used for the public cloud; for GCC (https://smba.infra.gcc.teams.microsoft.com/teams), GCC High, or DoD, you must set it. China accepts Azure China Bot Framework channel hosts when the stored conversation reference originates from Teams operated by 21Vianet.
  • channels.msteams.webhook.port (defaults to 3978).
  • channels.msteams.webhook.path (defaults to /api/messages).
  • channels.msteams.dmPolicy: pairing | allowlist | open | disabled (defaults to pairing).
  • channels.msteams.allowFrom: DM allowlist, with AAD object IDs being the recommended entries. Stable AAD object IDs also grant permission for approval actions. When Graph access is present, the wizard resolves names to IDs during setup.
  • channels.msteams.defaultTo: the default outbound target; a stable AAD object ID can also grant permission for approval actions.
  • channels.msteams.dangerouslyAllowNameMatching: a break-glass toggle that re-enables mutable UPN/display-name matching and direct team/channel name routing.
  • channels.msteams.textChunkLimit: outbound text chunk size in characters (default 4000, hard-capped at 4000 even if a higher value is configured).
  • channels.msteams.streaming.chunkMode: either length (the default) or newline to split on blank lines (paragraph boundaries) before length chunking.
  • channels.msteams.mediaAllowHosts: allowlist for inbound attachment hosts, defaulting to Microsoft/Teams domains: Graph, SharePoint/OneDrive, Teams CDN, Bot Framework, Azure Media Services.
  • channels.msteams.mediaAuthAllowHosts: allowlist for attaching Authorization headers on media retries, defaulting to Graph plus Bot Framework hosts.
  • channels.msteams.graphMediaFallback: opt into Graph message lookups when channel/group HTML omits file markers (default false; see Channel/group file recovery).
  • channels.msteams.mediaMaxMb: per-channel media size limit override in MB. When unset, it falls back to agents.defaults.mediaMaxMb.
  • channels.msteams.requireMention: require @mention in channels/groups (default true).
  • channels.msteams.replyStyle: thread | top-level (see Reply style).
  • channels.msteams.teams.<teamId>.replyStyle: per-team override.
  • channels.msteams.teams.<teamId>.requireMention: per-team override.
  • channels.msteams.teams.<teamId>.tools: default per-team tool policy overrides (allow/deny/alsoAllow) applied when a channel override is absent.
  • channels.msteams.teams.<teamId>.toolsBySender: default per-team per-sender tool policy overrides ("*" wildcard supported).
  • channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle: per-channel override.
  • channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention: per-channel override.
  • channels.msteams.teams.<teamId>.channels.<conversationId>.tools: per-channel tool policy overrides (allow/deny/alsoAllow).
  • channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender: per-channel per-sender tool policy overrides ("*" wildcard supported).
  • toolsBySender keys must carry explicit prefixes: channel:, id:, e164:, username:, name: (older keys without prefixes still resolve to id: only).
  • channels.msteams.authType: authentication type, either "secret" (the default) or "federated".
  • channels.msteams.certificatePath: file path for the PEM certificate (used with federated and certificate authentication).
  • channels.msteams.certificateThumbprint: certificate thumbprint, optional for authentication but accepted.
  • channels.msteams.useManagedIdentity: turns on managed identity authentication (federated mode).
  • channels.msteams.managedIdentityClientId: client ID for a user-assigned managed identity.
  • channels.msteams.sharePointSiteId: SharePoint site ID for uploading files in group chats or channels (details in Sending files in group chats).
  • channels.msteams.welcomeCard, channels.msteams.groupWelcomeCard, channels.msteams.promptStarters: the welcome Adaptive Card shown on first DM or group contact, along with its suggested prompt buttons.
  • channels.msteams.responsePrefix: text added to the start of outbound replies.
  • channels.msteams.feedbackEnabled (defaults to true), channels.msteams.feedbackReflection (defaults to 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 share the main session (agent:<agentId>:<mainKey>).
    • Channel/group messages use the conversation id:
      • agent:<agentId>:msteams:channel:<conversationId>
      • agent:<agentId>:msteams:group:<conversationId>

Reply style: threads vs posts

Teams offers two channel UI styles on 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 expose which UI style a channel uses. If you use the wrong replyStyle:

  • 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 you set requireMention: false globally without an explicit replyStyle, mentions in Posts-style channels surface as top-level posts even when the inbound was a thread reply. Pin replyStyle: "thread" at the global, team, or channel level to avoid surprises.

For proactive sends into a stored channel conversation (queued tool-call replies, long-running agents), the same team/channel resolution applies; group chats and personal (DM) conversations always resolve to top-level for proactive sends regardless of replyStyle.

Thread context preservation

When replyStyle: "thread" is active and the bot receives an @mention inside a channel thread, OpenClaw reconnects the original thread root to the outbound conversation reference (19:...@thread.tacv2;messageid=<root>), ensuring the response appears in that same thread. This behavior applies both to immediate in-turn sends and to proactive sends that occur after the Bot Framework turn context has lapsed (for example, long-running agents or queued tool-call replies via mcp__openclaw__message).

The thread root comes from the stored threadId on the conversation reference. References saved before threadId revert to activityId (whatever inbound activity last populated the conversation), so current deployments continue functioning without needing a re-seed.

When replyStyle: "top-level" is active, inbound channel-thread messages are deliberately answered as new top-level posts, with no thread suffix added. This is the right behavior for Threads-style channels; if you see top-level posts where threaded replies were expected, replyStyle is misconfigured for that channel.

Attachments and images

Current limitations:

  • DMs: images and file attachments work through Teams bot file APIs.
  • Channels/groups: attachments reside in M365 storage (SharePoint/OneDrive). The webhook payload delivers only an HTML stub, not the actual file bytes. Graph API permissions are required to download channel attachments.
  • For explicit file-first sends, use action=upload-file with media / filePath / path; optional message becomes the accompanying text/comment, and filename (or title) overrides the uploaded name.

Without Graph permissions, channel messages containing images arrive as text-only (the image content is inaccessible to the bot). By default, OpenClaw downloads media only from Microsoft/Teams hostnames. Override this with channels.msteams.mediaAllowHosts (use ["*"] to permit any host). Authorization headers attach only for hosts listed in channels.msteams.mediaAuthAllowHosts (defaulting to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes).

Sending files in group chats

Bots can send files in DMs using the built-in FileConsentCard flow. Sending files in group chats/channels requires additional setup:

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, while Microsoft Graph's /me resource requires a signed-in user. To send files in group chats/channels, the bot uploads to a SharePoint site and generates a sharing link.

Setup

  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 since only chat participants can access the file. OpenClaw requires a successful member lookup for group chats; timeouts, transport failures, empty results, and Graph API denials fail the send instead of widening access to the organization.

Fallback behavior

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 stored in a /OpenClawShared/ folder in the configured SharePoint site's default document library.

Native approval cards

Microsoft Teams can deliver exec and plugin approval requests as Adaptive Cards in the originating conversation. Each card describes the requested command or plugin action and provides only the decisions allowed for that request, such as Approve once, Always allow, and Deny. After a decision or expiration, OpenClaw updates the original card with its final status.

Enable the existing top-level approval forwarding settings for each approval type you want to receive:

{
  approvals: {
    exec: { enabled: true, mode: "session" },
    plugin: { enabled: true, mode: "session" },
  },
  channels: {
    msteams: {
      allowFrom: ["00000000-0000-0000-0000-000000000000"],
    },
  },
}

approvals.exec and approvals.plugin are independent; enabling one does not enable the other. Native card delivery also requires a configured Teams bot and at least one approver resolved from channels.msteams.allowFrom or channels.msteams.defaultTo. Approvers must be stable AAD object IDs; display names, email addresses, group entries, and conversation IDs do not grant approval access. OpenClaw checks the clicking user's AAD object ID before resolving the request.

No Teams-specific approval configuration is required. The existing /approve <id> <decision> command remains available as a text fallback when native delivery is unavailable. For forwarding modes and supported decisions, see Approval forwarding to chat channels.

Polls (Adaptive Cards)

OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).

  • CLI: openclaw message poll --channel msteams --target conversation:<id> --poll-question "..." --poll-option "..." --poll-option "...".
  • Votes are recorded by the gateway in OpenClaw plugin-state SQLite under state/openclaw.sqlite.
  • Existing msteams-polls.json files are imported by openclaw doctor --fix, not by the running plugin.
  • The gateway must stay online to record votes.
  • Polls do not auto-post result summaries, and there is no poll-results CLI yet.

Presentation cards

Send semantic presentation payloads to Teams users or conversations using the message tool, CLI, or normal reply delivery. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.

The presentation parameter accepts semantic blocks. When presentation is provided, the message text is optional. Buttons render as Adaptive Card submit or URL actions. Select menus are not native in the Teams renderer, so OpenClaw downgrades them to readable text before delivery.

Agent tool:

{
  action: "send",
  channel: "msteams",
  target: "user:<id>",
  presentation: {
    title: "Hello",
    blocks: [{ type: "text", text: "Hello!" }],
  },
}

CLI:

openclaw message send --channel msteams \
  --target "conversation:19:abc...@thread.tacv2" \
  --presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello!"}]}'

For target format details, see Target formats below.

Target formats

MSTeams targets rely on prefixes to tell users apart from conversations:

Target 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 absent, names are resolved as groups or teams. For targeting individuals by display name, user: must always be used.

Proactive messaging

  • Since OpenClaw only records conversation references once a user has engaged, proactive messages are possible solely after that interaction.
  • Gating via dmPolicy and allowlists is covered in /gateway/configuration.

Team and Channel IDs (Common Gotcha)

The groupId query parameter found in Teams URLs is not the team ID for configuration. Pull IDs from the URL path instead:

Team URL:

https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
                                    └────────────────────────────┘
                                    Team conversation ID (URL-decode this)

Channel URL:

https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=...
                                      └─────────────────────────┘
                                      Channel ID (URL-decode this)

For config:

  • Team key = the path segment following /team/ (URL-decoded, for instance 19:Bk4j...@thread.tacv2; older tenants might display @thread.skype, which also works).
  • Channel key = the path segment following /channel/ (URL-decoded).
  • For OpenClaw routing, ignore the groupId query parameter. It represents the Microsoft Entra group ID, not the Bot Framework conversation ID present in incoming Teams activities.

Private channels

Private channels offer only limited bot support:

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 if private channels do not work:

  1. For bot interactions, stick with standard channels.
  2. Use DMs; users can always message the bot directly.
  3. For historical access, employ Graph API (needs ChannelMessage.Read.All).

Troubleshooting

Common issues

  • Images not showing in channels: Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
  • No responses in channel: mentions are required by default; set channels.msteams.requireMention=false or configure per team/channel.
  • Version mismatch (Teams still shows old manifest): remove + re-add the app and fully quit Teams to refresh.
  • 401 Unauthorized from webhook: expected when testing manually without an Azure JWT; means the endpoint is reachable but auth failed. Use Azure Web Chat to test properly.

Manifest upload errors

  • "Icon file cannot be empty": the manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for outline.png, 192x192 for color.png).
  • "webApplicationInfo.Id already in use": the app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation.
  • "Something went wrong" on upload: upload via https://admin.teams.microsoft.com instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error.
  • Sideload failing: try "Upload an app to your org's app catalog" instead of "Upload a custom app"; this often bypasses sideload restrictions.

RSC permissions not working

  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

6,819 words · updated Sep 1, 2026