🏠 Home ← Docs Index
Open Beta 📖 Interactive Reader Raw .md llms.txt

Microsoft Teams Bot Setup

GoClaw can run as a Microsoft Teams bot in direct messages, group chats and team channels (the generated app package declares the personal, groupChat and team scopes). The implementation uses Bot Framework activities, a mounted webhook path, conversation reference persistence, optional mention filtering, file consent for outbound files, and attachment handling for inbound Teams files.

What You Need

Teams setup has two separate Microsoft resources:

Resource Where Purpose
App Registration Microsoft Entra ID -> App registrations Identity, client ID, tenant ID, client secret
Azure Bot Azure Portal -> Azure Bot Registers the bot with Microsoft Bot Framework and Teams

The Teams app manifest botId must match the Azure Bot Microsoft App ID. If the Azure Bot was created from an existing app registration, this is usually the app registration Application ID. If Azure created a new Microsoft App ID, use the value shown in Azure Bot -> Configuration.

For single-tenant bots, set channels.msteams.tenant_id: GoClaw requests its Bot Framework token from that tenant. For multi-tenant bots, leave it empty; GoClaw then uses the botframework.com tenant.

Runtime Paths

GoClaw serves the Teams webhook only when the Teams channel has started: enabled: true with app_id and app_password set, and the token request made at startup succeeded. When the start fails, the gateway log shows Teams channel failed: ...; until the channel starts, the path answers 404.

Config Messaging endpoint
No gateway.webhook_secret_slug https://<public-url>/hook/msteams
With gateway.webhook_secret_slug: "abc" https://<public-url>/hook/abc/msteams

The public URL should normally come from gateway.ingress.url, either through ngrok or through a reverse proxy.

Minimal Config

gateway:
  ingress:
    type: "ngrok"                  # "ngrok", "proxy", "nginx", or empty
    url: "https://example.ngrok.app"
    authtoken: "${NGROK_AUTHTOKEN}"
    allow_preset: "microsoft-teams"

channels:
  dm_policy: "pairing"
  msteams:
    enabled: true
    app_id: "00000000-0000-0000-0000-000000000000"      # Azure Bot Microsoft App ID
    app_password: "${secret:TEAMS_APP_PASSWORD}"
    tenant_id: "11111111-1111-1111-1111-111111111111"   # empty for a multi-tenant bot
    bot_name: "GoClaw"
    require_mention: true
    app_package:                        # shown on the app's page in Teams; see below
      developer_name: "Example LLC"
      website_url: "https://example.com"
      privacy_url: "https://example.com/privacy"
      terms_of_use_url: "https://example.com/terms"

app_id and tenant_id are not secrets, so plain values are simplest; ${secret:NAME} and ${VAR} references also work in both. Keep app_password (the client secret) in the vault.

Use gateway.webhook_secret_slug if you want the public Teams path to be harder to guess:

gateway:
  webhook_secret_slug: "team-prod-9f4c"

Then set the Azure Bot messaging endpoint to:

https://<public-url>/hook/team-prod-9f4c/msteams

Setup Flow

  1. Create an App Registration in Microsoft Entra ID.
  2. Create a client secret and save the secret value.
  3. Create an Azure Bot resource and connect it to the app registration.
  4. Enable the Microsoft Teams channel on the Azure Bot.
  5. Set the Azure Bot messaging endpoint to the GoClaw Teams webhook path.
  6. Generate a Teams App Package with goclaw teams package.
  7. Upload the package in Microsoft Teams (custom app upload must be allowed in the tenant).
  8. Start GoClaw and send a test message.

Generate The Teams App Package

If config.yaml already contains channels.msteams.*, run:

goclaw teams package

Override values from the command line when needed:

goclaw teams package \
  --app-id="00000000-0000-0000-0000-000000000000" \
  --tenant-id="11111111-1111-1111-1111-111111111111" \
  --app-name="Ops Assistant" \
  --out="./teams-app.zip"

--app-id, --tenant-id and --app-password are also written to config.yaml as plain values (--app-id also sets channels.msteams.enabled: true), but only once the package has been built. The package itself needs only the App ID and the name, so leave --app-password out and keep the client secret in the vault. --app-name defaults to channels.msteams.bot_name, then to GoClaw Bot.

The zip holds three files at its root, which is where Teams looks for them:

File Content
manifest.json Manifest version 1.17. id and bots[0].botId are both the App ID; version is --app-version (default 1.0.0); name.short is the bot name cut to 30 characters; developer is your organization and its pages (below); validDomains is the host of gateway.ingress.url, if set. The bot is installable in the personal, groupChat and team scopes
color.png 192×192 color icon
outline.png 32×32 outline icon: white on a transparent background

Publisher and links

Teams shows the manifest's developer block on the app's details page: the publisher's name and links to its website, privacy policy and terms of use. Set them to your organization's own:

Setting Flag Manifest field Rule
channels.msteams.app_package.developer_name --developer-name developer.name At most 32 characters
channels.msteams.app_package.website_url --website-url developer.websiteUrl An https:// URL
channels.msteams.app_package.privacy_url --privacy-url developer.privacyUrl An https:// URL
channels.msteams.app_package.terms_of_use_url --terms-of-use-url developer.termsOfUseUrl An https:// URL
goclaw teams package \
  --developer-name="Example LLC" \
  --website-url="https://example.com" \
  --privacy-url="https://example.com/privacy" \
  --terms-of-use-url="https://example.com/terms" \
  --app-version=1.0.1

A flag wins over config.yaml and is saved there once the package is built, so the next package keeps it. goclaw teams setup asks for the four values too. Teams requires all four fields, so one left unset keeps GoClaw's, and the command names the ones it kept:

Setting GoClaw's value
developer_name Ulab LLC
website_url https://goclaw.ulab.mn/
privacy_url https://goclaw.ulab.mn/privacy.html
terms_of_use_url https://goclaw.ulab.mn/terms.html

An app already in Teams shows the new links after you upload the new package with a higher --app-version.

Icons

Without --icon-source, both icons are GoClaw's own (a white speech bubble). To use your logo:

goclaw teams package --icon-source=/path/to/logo.png

The source is a PNG, JPEG or GIF file, or an http(s) URL to one. It is fitted into both squares keeping its proportions: on a blue (#0078D4) background for color.png, and as a white silhouette for outline.png. The silhouette comes from the image's transparency; an image without any is cut from its background, taken to be the color of its top-left pixel. When that gives no usable shape (a photo, or an image of one color), the package keeps GoClaw's outline icon and the command says so. A logo on a transparent background gives the best outline. No external tool is needed: GoClaw resizes the image itself (--ffmpeg is ignored).

Checks and versions

Before writing the zip, the command checks what Teams checks on upload, and refuses with an error instead of producing a package Teams would answer with Bad Request:

Teams installs a changed package over an earlier one with the same App ID only when its version is higher. Raise --app-version (1.0.1, 1.0.2, …) every time you upload a package again, for example after changing the name or the icons:

goclaw teams package --app-version=1.0.1

Interactive setup

For an interactive flow, run:

goclaw teams setup

Upload To Teams

For yourself, a chat or a team, in the Teams desktop or web client:

  1. Select Apps > Manage your apps > Upload an app.
  2. Select Upload a custom app.
  3. Select teams-app.zip, then Add.
  4. Select Open for a direct chat with the bot, or choose a team or chat to add it to.

"Upload a custom app" is shown only when the tenant allows it: a Teams administrator turns on Upload custom apps under Teams admin center > Teams apps > Setup policies (the Global policy, or one assigned to you), and under Teams apps > Manage apps > Actions > Org-wide app settings > Custom apps. A changed policy can take up to 24 hours to apply.

For the whole organization, a Teams administrator uploads the same zip in Teams admin center > Teams apps > Manage apps > Actions > Upload new app. Who can then install it follows the tenant's app permission policies.

To check a package before uploading it, import it in the Teams Developer Portal (https://dev.teams.microsoft.com, Apps > Import app) and open its validation: it names the field Teams objects to, where the upload in the client answers only Bad Request.

Message Handling

Inbound Teams activities are handled by internal/channel/msteams/msteams.go.

Outbound markdown tables and standalone HTTPS images are sent as Adaptive Cards (msteams_format.go, msteams_adaptive.go), because Teams does not render markdown tables in bot messages. A card table cannot scroll sideways, so each column's width is estimated from its longest word (about 8px a character plus padding), and a table of more than six columns or wider than about 520px is reshaped first:

Column widths follow the longest word in each column, table cards use the full message width, and at most 50 data rows are shown (a table turned sideways keeps every column). A table too big for one message — Teams refuses one over about 100 KB — is sent as several cards of at most 60 KB each. An agent or SOP needs no change; it writes an ordinary markdown table.

File Handling

Inbound Teams attachments are handled by msteams_attachments.go.

Outbound files use the Teams file consent flow in msteams_consent.go.

Ingress Notes

For local testing, ngrok is the fastest option:

gateway:
  ingress:
    type: "ngrok"
    authtoken: "${NGROK_AUTHTOKEN}"
    url: "https://my-agent.ngrok.app"
    allow_preset: "microsoft-teams"

For production, prefer a reverse proxy with TLS and a dedicated ingress port. See Ingress Configuration.

With the built-in ngrok tunnel, requests reach GoClaw from the local tunnel (loopback, which the allowlist always lets through), so with trust_proxy: false (the default) allow_preset does not filter them; the Teams webhook is still protected by its Bot Framework token check (below). The allowlist filters callers that connect directly, or through a reverse proxy.

Without gateway.ingress.port, the webhooks share the main port, and allow_preset / allow_ips then apply to that whole port: the Web UI and the app API answer 403 Forbidden to every address outside the list, except loopback. Set gateway.ingress.port so the allowlist covers only the webhook listener.

The Azure Bot endpoint should never point at the WebUI-only port unless that port also serves the Teams webhook. If gateway.ingress.port is configured, point the proxy or tunnel to that dedicated webhook port.

Troubleshooting

Symptom Check
Bad Request while uploading a package an earlier GoClaw built without --icon-source Its placeholder icons were 1×1 and color.png was not a valid PNG. Build the package again with the current goclaw teams package
Bad Request while uploading the package Import it in the Teams Developer Portal to see which field fails. If an app with this App ID is already in Teams, build the package again with a higher --app-version
invalid BotId while uploading the package The manifest botId must match Azure Bot -> Configuration -> Microsoft App ID
goclaw teams package says the App ID is not a GUID or not resolved channels.msteams.app_id holds something other than the Azure Bot's Microsoft App ID, or a ${secret:NAME} the shell cannot read from the vault. Give the ID with --app-id
The app's page in Teams links to GoClaw's site instead of yours Set channels.msteams.app_package.* (or the flags) and upload a new package with a higher --app-version: see Publisher and links
No "Upload a custom app" in Teams Custom app upload is off for you: see Upload To Teams
Azure Bot test shows 404 The endpoint path must be /hook/msteams or /hook/<slug>/msteams. The path exists only when the Teams channel started: look for Teams channel failed: in the gateway log (wrong App ID or client secret, or no app_password)
Azure Bot test shows 401, log shows [teams] inbound auth rejected channels.msteams.app_id must equal the Azure Bot's Microsoft App ID, which is the audience of the token Bot Framework sends. Check the host clock too (5 minutes of skew are allowed), and that the gateway can reach login.botframework.com, where Microsoft's signing keys are fetched
Web UI or app API answers 403 Forbidden allow_preset or allow_ips without gateway.ingress.port covers the whole gateway port. Set gateway.ingress.port
Group chat messages are ignored Check channels.msteams.require_mention and mention the bot
Proactive messages fail with teams: no conversation reference GoClaw holds no reference for that conversation: nobody there has messaged the bot, or not within 30 days (older references are dropped at startup). Have someone send it a message, and confirm the gateway database is writable so the reference persists
Attachments are not visible to the agent Check Teams attachment type, download host, file size, and gateway logs
Outbound file transfer fails Confirm the recipient accepted the Teams file consent card

Implementation Files