Add Microsoft Teams
The Teams adapter speaks the Bot Connector REST protocol directly. It sends ordinary activities, quoted replies, typing activities, edits, rich cards and personal-chat file-consent cards.
Read Connect a chat app to Virgo first. Registering an adapter on an installation you already run is supported through adapter add, so the steps below end at that command rather than at a descriptor you can only prepare.
Before you start
- A Virgo installation you already run, with its Hub reachable.
- A Microsoft 365 tenant and a Teams team or chat you administer.
- A bot application registered for Teams, created through Microsoft's own documentation. Virgo does not create applications or bots for you.
- A public HTTPS messaging endpoint. Teams calls your bot; unlike Discord and Telegram, this adapter cannot work from a host that only makes outbound connections.
The endpoint and how it is authenticated
Mount the adapter's reusable HTTP ingress on the bot's public HTTPS messaging route. Every inbound request is validated exactly as Microsoft's Connector authentication procedure requires: RS256 signature against the published OpenID/JWKS documents, the fixed Connector issuer, your exact app-id audience, the validity window with five-minute skew, Teams key endorsement, a signed serviceUrl, and the configured tenant and conversation.
The HTTP response completes only after the adapter has acknowledged the event, so the durable ingress commit stays the acknowledgement boundary.
Only HTTPS service URLs are accepted, without inline credentials, query or fragment. An inbound activity must match the configured service URL, tenant, conversation and any configured team and channel IDs.
Store the client secret privately
credential references the bot application client secret; appId is the non-secret JWT audience and OAuth client id. The environment exchanges the resolved secret at Microsoft's tenant-specific Bot Framework OAuth endpoint for the https://api.botframework.com/.default scope and caches only the short-lived access token.
The installed configuration contains no access token and no client secret.
The descriptor you will need
Placeholders only.
{
"id": "teams-main",
"kind": "teams",
"session": "teams-session",
"vsp": "vsp:/your-account:your-space/teams/bot",
"credential": "credential:teams/main",
"appId": "00000000-0000-0000-0000-000000000000",
"targetVsp": "vsp:/your-account:your-space/your-repo/lead",
"inboundPort": "adapter-port:teams/inbound",
"conversation": {
"ref": { "value": "teams.conversation:main" },
"conversationId": "your-provider-conversation-id",
"serviceUrl": "https://smba.example.test/teams",
"tenantId": "your-tenant-id",
"teamId": "your-team-id",
"channelId": "your-channel-id"
}
}
conversation.ref is also the default outbound conversation. Dependency resolution and HTTP routing stay scoped by installed adapter id, so the same credential reference can intentionally appear twice for two conversations that share one bot.
Access and administrators
Teams defaults to open within that authenticated binding — the tenant and conversation you configured, proven by the validated JWT, not by anything in a message. The shared gate can override it with restricted or approval.
An administrator decides a pending request with /virgo-access approve <requestId> or /virgo-access deny <requestId>, or by submitting the typed card data { "kind": "virgo.channel_access.decision.v1", "decision": "approve" | "deny", "requestId": "..." }.
Card submit data is preserved as a verified interaction with the clicking actor and original context. It is untrusted input and grants no permission.
Cards, files and reactions
Supported output is Adaptive Card 1.4, Hero Card and Thumbnail Card. Adaptive bodies support text, facts, images and text input; actions support Action.Submit and HTTPS Action.OpenUrl. Action.Execute invoke-response handling is outside this adapter.
Limits are exact: message text 32,000 UTF-8 bytes; a card 64 KiB of JSON; submit data a JSON object of at most 16 KiB, eight nested levels, 256 nodes and 100 items per array or object, with strings at most 8 KiB. Unsupported card properties and insecure image or action URLs are rejected before any network call.
Sending a file is personal chat only. It uses Teams file consent, requires supportsFiles: true in the Teams app manifest, and needs conversation.personalRecipient plus a file port. Group and channel file delivery needs Microsoft Graph and user authorisation and is outside this adapter — do not plan for it.
Receiving a file is personal chat too, and it needs a managed port. Teams' direct file APIs are personal-chat features: in a personal chat an attached file arrives as an authenticated file.download.info attachment, which is what this adapter reads. Group and channel files are Microsoft Graph territory, and group or channel file transfer is not implemented here in either direction.
Two conditions carry the personal-chat case: supportsFiles: true in the Teams app manifest, and a managed file port supplying storeInbound. Given both, the adapter downloads those bytes itself — the download URL must be HTTPS and carry no credentials, and a redirect is refused — rejects anything over 50 MiB, and stores them through storeInbound, which the managed port writes to the artifact store with thirty-day retention. The stored artifact travels on the inbound event, so an agent sees the file with the message; a message carrying only a file arrives as [attachment <name>]. The store is keyed by tenant, activity and file id, so a redelivered activity stores once. Without that port the activity is refused as CAPABILITY_UNSUPPORTED rather than delivered without its file.
Reactions use the official Teams API client, built from this conversation's exact service URL and the bot token already configured for it. IDs come from a pinned catalog of 3,112 exact reaction IDs, plus the emoji aliases Microsoft's own reference documents. Nothing is matched fuzzily; an unknown value is refused.
The adapter records the reaction it placed before asking Teams to change anything, so a replacement removes the recorded prior reaction and a removal removes the recorded one, across a Host restart. It never deletes a reaction it did not record placing.
Registering it on an installation you already have
install accepts the descriptor through --adapters with --host-settings when you are building a new installation. When the installation already exists, the supported route is adapter add:
adapter add --file /absolute/path/to/teams.json
adapter list
adapter remove teams-main
The path must be absolute. An id already in the roster is refused as a conflict rather than replaced, only the new entry is prepared, and the whole roster is then validated the way a boot validates it — so an add cannot leave a configuration that will not start. A credentialBinding in the descriptor is registered in the same operation rather than as a separate step. adapter list reads the roster back with the credential reference each entry names, never the secret itself.
It becomes active on the next Host start. A running Host keeps the adapters it booted with, and native agent sessions are left running.
Do not reinstall your Hub to add Teams.
Resend safety
Bot Connector has no effect-key idempotency and no lookup for an unknown submission. An ambiguous send or upload result stays unknown and is not resubmitted by that adapter instance. The file-port commit is the durable handoff for consent completion, and repeated identical commits must be handled idempotently.
Troubleshooting and removal
- Nothing arrives. The endpoint must be publicly reachable over HTTPS and must present a certificate Microsoft accepts. Validation failures are refusals, not retries.
- Activities rejected. Compare the inbound activity's service URL, tenant and conversation against the configured ones; a mismatch is refused by design.
- A file card does nothing. Confirm the chat is a personal chat and the app manifest sets
supportsFiles: true. - An inbound file is refused. Confirm the chat is a personal chat and that a managed file port providing
storeInboundis configured; without one, an activity carrying a file is refused rather than delivered without the file. - Removal. Remove the bot from the tenant and retire the credential.
adapter remove <id>takes it out of an existing installation's configuration, and the Host stops serving it at the next start.
What is verified today
The adapter source is integrated and its configuration validates. That is not live bot acceptance on your tenant. Registration on an existing installation has been exercised on a canonical route for Discord only, and it reached Gateway READY there; an agent claiming its Seat, a human inbound message and a correlated reply are unaccepted on every provider, Teams included. See Availability and evidence.
Protocol references: send and receive messages, Connector authentication, rich cards, file consent.