Visor provides Microsoft Teams bot integration via the Azure Bot Framework, enabling interactive workflows, AI-powered chat assistants, and automated responses directly in Teams 1:1 chats, group chats, and channels.
- Overview
- Prerequisites
- Teams App Manifest
- Configuration
- Conversation Types
- Features
- Example Workflows
- Webhook Setup
- Team and Channel IDs
- Known Limitations
- Troubleshooting
- FAQ
- Related Documentation
The Teams integration enables:
- 1:1 Conversations: Respond to direct messages from users
- Group Chat Support: Respond in group chats when @mentioned
- Channel Support: Respond in Teams channels when @mentioned
- Webhook-Based: Receives messages via Azure Bot Framework webhooks
- Markdown Formatting: Teams renders standard Markdown natively — AI output passes through unchanged
- Message Chunking: Long responses auto-split at ~28KB Teams message limit
- @Mention Stripping: Bot @mentions are automatically removed from message text
- User Allowlist: Restrict which users can trigger workflows
- Hot Reload: Configuration changes picked up without restarting the bot
The integration uses the botbuilder SDK for JWT token validation and Bot Framework protocol handling.
-
Create a Microsoft Entra ID (Azure AD) App Registration:
- Go to Azure Portal > Microsoft Entra ID > App registrations
- Click "New registration"
- Save the Application (client) ID — this is your
TEAMS_APP_ID - Go to Certificates & secrets > New client secret
- Save the Client secret value immediately — this is your
TEAMS_APP_PASSWORD
-
Create an Azure Bot resource:
- Go to Azure Portal > Create a resource > Azure Bot
- Use the App ID from step 1
- Set the messaging endpoint to:
https://your-domain.com/api/messages
-
Enable the Teams channel:
- In your Azure Bot resource > Channels
- Add "Microsoft Teams" channel
- Accept the Terms of Service and enable "Messaging"
-
Install in Teams:
- Create an app manifest (see Teams App Manifest below)
- Upload via Teams Admin Center or sideload in Teams
- Or use the "Open in Teams" link from the Azure Bot resource
- Single-tenant (recommended): Restricts the bot to your organization's Azure AD directory. More secure for self-hosted Visor.
- Multi-tenant: Allows installation in any Teams organization. Use if you need cross-org access.
Set TEAMS_TENANT_ID for single-tenant bots. Omit it for multi-tenant.
# Required
export TEAMS_APP_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export TEAMS_APP_PASSWORD="your-client-secret"
# Optional
export TEAMS_TENANT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export TEAMS_WEBHOOK_PORT="3978"To install your bot in Teams, you need an app manifest package — a ZIP file containing manifest.json, outline.png (32x32), and color.png (192x192).
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
"manifestVersion": "1.23",
"version": "1.0.0",
"id": "YOUR-APP-ID-HERE",
"name": { "short": "Visor Bot" },
"developer": {
"name": "Your Org",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"termsOfUseUrl": "https://example.com/terms"
},
"description": {
"short": "Visor AI assistant in Teams",
"full": "Visor AI-powered assistant for code review and workflow automation in Microsoft Teams."
},
"icons": { "outline": "outline.png", "color": "color.png" },
"accentColor": "#5B6DEF",
"bots": [
{
"botId": "YOUR-APP-ID-HERE",
"scopes": ["personal", "team", "groupChat"],
"isNotificationOnly": false,
"supportsFiles": false
}
],
"webApplicationInfo": {
"id": "YOUR-APP-ID-HERE"
}
}Replace YOUR-APP-ID-HERE with your Azure Bot App ID (same as TEAMS_APP_ID).
| Scope | Description |
|---|---|
personal |
1:1 direct messages with the bot |
team |
Bot can be added to team channels |
groupChat |
Bot can be added to group chats |
Include only the scopes your workflow needs.
To receive channel/group messages without requiring @mention, add RSC permissions to the manifest:
{
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
]
}
}
}| Permission | Scope | Description |
|---|---|---|
ChannelMessage.Read.Group |
Team | Receive channel messages without @mention |
ChannelMessage.Send.Group |
Team | Send channel messages |
ChatMessage.Read.Chat |
Group chat | Receive group messages without @mention |
Member.Read.Group |
Team | Read team members |
Without RSC, the bot must be @mentioned in channels and group chats to receive messages.
- Create icon files:
outline.png(32x32) andcolor.png(192x192) - ZIP all three files together:
manifest.json,outline.png,color.png - Upload via one of:
- Teams Admin Center: https://admin.teams.microsoft.com > Teams apps > Manage apps > Upload
- Sideload: In Teams, go to Apps > Manage your apps > Upload a custom app
- Teams Developer Portal: https://dev.teams.microsoft.com/apps
When updating your app:
- Increment the
versionfield (e.g.,1.0.0→1.1.0) - Re-ZIP and re-upload
- Reinstall the app in teams/chats for new permissions to take effect
- Fully quit and relaunch Teams to refresh cached metadata
Enable the Teams webhook runner with the --teams flag:
visor --config workflow.yaml --teams| Variable | Required | Description |
|---|---|---|
TEAMS_APP_ID |
Yes | Azure AD App (client) ID |
TEAMS_APP_PASSWORD |
Yes | Azure AD App client secret |
TEAMS_TENANT_ID |
No | Tenant ID (for single-tenant apps) |
TEAMS_WEBHOOK_PORT |
No | Webhook server port (default: 3978) |
Add Teams-specific configuration in your workflow YAML:
version: "1"
teams:
user_allowlist: # Optional: limit to specific AAD user IDs
- "user-aad-object-id-1"
- "user-aad-object-id-2"
# Frontend configuration for posting to Teams
frontends:
- name: teams
checks:
# Your workflow checks...| Option | Type | Default | Description |
|---|---|---|---|
app_id |
string | $TEAMS_APP_ID |
Azure AD App (client) ID |
app_password |
string | $TEAMS_APP_PASSWORD |
Azure AD App client secret |
tenant_id |
string | $TEAMS_TENANT_ID |
Tenant ID for single-tenant apps |
port |
number | 3978 |
Webhook HTTP server port |
host |
string | 0.0.0.0 |
Webhook HTTP server bind address |
user_allowlist |
string[] | [] |
Limit to specific AAD user IDs (empty = all) |
workflow |
string | — | Optional workflow name to dispatch |
- Messages are always processed (no mention requirement)
- Ideal for personal assistant workflows
- Each conversation gets its own workspace
- Bot must be @mentioned to receive messages
- Teams automatically includes the @mention in the message text
- The adapter strips @mentions before passing text to workflows
- Bot must be @mentioned to receive messages
- Works in any channel where the bot app is installed
- Channel and team IDs are available in webhook context
Teams renders standard Markdown natively. AI output passes through unchanged:
| Markdown | Teams Rendering |
|---|---|
**bold** |
bold |
_italic_ |
italic |
~~strikethrough~~ |
|
`code` |
code |
```block``` |
Code block |
[label](url) |
label |
> quote |
Blockquote |
# Header |
Header |
- item |
Bullet list |
Teams bot messages have a ~28KB size limit. Long AI responses are automatically split into multiple messages at newline boundaries.
In group chats and channels, Teams includes <at>BotName</at> in the message text when users @mention the bot. The adapter automatically strips these mentions so your workflow receives clean text.
When a Teams message triggers a workflow, the full event context is available in templates:
checks:
reply:
type: ai
prompt: |
User: {{ webhook.event.from_name }}
Message: {{ webhook.event.text }}Available webhook fields:
| Field | Description |
|---|---|
webhook.event.type |
Always teams_message |
webhook.event.text |
Message text (with @mentions stripped) |
webhook.event.from_id |
Sender's AAD user ID |
webhook.event.from_name |
Sender's display name |
webhook.event.conversation_id |
Conversation ID |
webhook.event.conversation_type |
personal, groupChat, or channel |
webhook.event.activity_id |
Bot Framework activity ID |
webhook.event.team_id |
Team ID (for channel conversations) |
webhook.event.channel_id |
Channel ID (for channel conversations) |
webhook.event.tenant_id |
Azure AD tenant ID |
webhook.teams_conversation |
Normalized conversation context |
Messages are deduplicated by activity ID. Duplicate webhook deliveries are automatically filtered.
version: "1"
checks:
respond:
type: ai
schema: text
on:
- teams_message
- manual
prompt: |
You are a helpful assistant running inside Visor.
Respond concisely to the user's message.
User message: {{ webhook.event.text }}version: "1"
teams:
user_allowlist:
- "user-aad-id-1"
- "user-aad-id-2"
frontends:
- name: teams
checks:
reply:
type: ai
schema: text
on:
- teams_message
prompt: |
You are a team support assistant.
Answer the question briefly and professionally.
User: {{ webhook.event.from_name }}
Message: {{ webhook.event.text }}# Set environment variables
export TEAMS_APP_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export TEAMS_APP_PASSWORD="your-client-secret"
# Start the bot
visor --config workflow.yaml --teams
# With hot reload
visor --config workflow.yaml --teams --watch
# With debug logging
VISOR_DEBUG=true visor --config workflow.yaml --teamsThe bot will:
- Start an HTTP server on the configured port (default: 3978)
- Handle POST requests at
/api/messageswith JWT token validation - Parse Bot Framework activities and extract message text
- Filter by user allowlist and dedup by activity ID
- Process messages through your workflow
- Send formatted responses back to the originating conversation
Azure Bot Framework requires a public HTTPS endpoint. Options:
Development (ngrok):
ngrok http 3978
# Then set the messaging endpoint in Azure Portal:
# https://abc123.ngrok.io/api/messagesDevelopment (Tailscale Funnel):
tailscale funnel 3978
# Use the funnel URL as your messaging endpoint:
# https://your-machine.ts.net/api/messagesProduction:
- Use a reverse proxy (nginx, Caddy) with HTTPS and a valid certificate
- Or deploy to Azure App Service, which provides HTTPS by default
- Go to your Azure Bot resource > Configuration
- Set Messaging endpoint to:
https://your-domain.com/api/messages - Save the configuration
- Start the bot:
visor --config workflow.yaml --teams - Open Teams and find your bot (search by name or use the "Open in Teams" link)
- Send a message — the bot should respond
When configuring per-team or per-channel settings, you may need the team or channel ID. These can be extracted from Teams URLs.
Team URL:
https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations
^-- URL-decode this = team ID
Channel URL:
https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName
^-- URL-decode this = channel ID
The groupId query parameter in Teams URLs is not the team ID — extract the ID from the URL path instead.
Team and channel IDs are available in webhook event data:
checks:
log-ids:
type: logger
on: [teams_message]
message: |
Team ID: {{ webhook.event.team_id }}
Channel ID: {{ webhook.event.channel_id }}
Conversation ID: {{ webhook.event.conversation_id }}These are Microsoft Teams platform behaviors that affect all bots, not just Visor.
Microsoft has limited bot support in private channels. Bots must be explicitly added to each private channel — team-level installation does not automatically apply. Webhook message delivery may not work in all tenants. As of early 2026, Microsoft is rolling out expanded app support for private channels but availability varies by tenant.
The Bot Framework has a webhook timeout window. Slow LLM responses can exceed it, causing Azure to retry delivery. Visor handles this by accepting the webhook immediately and sending replies asynchronously via the stored conversation reference. The deduplication system filters out any retried deliveries.
Teams supports standard Markdown but some advanced formatting renders differently than other platforms:
- Complex nested tables may not render correctly
- Deeply nested lists may flatten
- HTML tags may be stripped or rendered unexpectedly
Teams bot messages are limited to ~28KB. Visor automatically chunks longer responses at newline boundaries, sending them as multiple sequential messages.
Teams voice messages are not supported by the Bot Framework webhook API — bots only receive text-based activities.
The current Visor integration handles text messages only. File attachments and images in inbound messages are not processed. For outbound, messages are plain text/Markdown (no Adaptive Cards). This is a Visor limitation — the Bot Framework does support file handling via FileConsentCard (DMs) and SharePoint (channels), which may be added in a future release.
"TEAMS_APP_ID is required"
- Set the
TEAMS_APP_IDenvironment variable - Or set
teams.app_idin your config file
"TEAMS_APP_PASSWORD is required"
- Set the
TEAMS_APP_PASSWORDenvironment variable - Or set
teams.app_passwordin your config file
Bot starts but never receives messages
- Verify the messaging endpoint URL in Azure Portal matches your server
- Ensure your server is reachable at the endpoint URL
- Check that the Teams channel is enabled in your Azure Bot resource
- Verify the App ID and App Password match your Azure AD registration
401 Unauthorized errors in logs
- The App ID or App Password is incorrect
- The bot registration in Azure may have expired credentials
- Regenerate the client secret in Azure AD
Bot not responding in group chats/channels
- The bot must be @mentioned in group chats and channels
- Ensure the bot app is installed in the team/chat
- Check
user_allowlistif configured — the user's AAD ID must be listed
Bot responding to its own messages
- This is handled automatically — the bot filters out messages from its own App ID
Duplicate responses
- The deduplication system handles this automatically
- If issues persist, check for multiple bot instances running
"Something went wrong" when uploading manifest
- Ensure icon files are valid PNGs (not empty files):
outline.png(32x32) andcolor.png(192x192) - Try uploading via https://admin.teams.microsoft.com instead of sideloading
- Check DevTools Network tab for detailed error messages
App ID conflict when uploading
- Uninstall the existing app first, or wait 5-10 minutes for propagation
- Ensure the
idfield in manifest matches your Azure Bot App ID
RSC permissions not working
- Verify
webApplicationInfo.idin manifest matches your App ID exactly - Re-upload and reinstall the app in each team/chat
- Confirm your org admin hasn't blocked RSC permissions
- Check the correct scope:
ChannelMessage.Read.Groupfor teams,ChatMessage.Read.Chatfor group chats
Old manifest still showing after update
- Remove and re-add the app in Teams
- Fully quit Teams (not just close the window) and relaunch to refresh cached metadata
Enable verbose logging:
VISOR_DEBUG=true visor --config workflow.yaml --teamsLog messages include:
[TeamsWebhook]— Webhook events, message dispatch, and filtering[teams-frontend]— Message posting and errors
Does Teams integration require a Microsoft 365 paid plan?
You need Azure Portal access (free-tier options available) and a Microsoft 365 tenant for Teams app installation. The Azure Bot resource itself is free for standard channels including Teams.
Can the bot respond without @mention in channels?
Yes, but it requires RSC permissions in the app manifest (ChannelMessage.Read.Group for channels, ChatMessage.Read.Chat for group chats). Without RSC, the bot only receives messages where it is @mentioned. In 1:1 (personal) chats, all messages are received regardless.
Do I need a public URL?
Yes. The Azure Bot Framework sends webhook events to your server's /api/messages endpoint, which must be publicly accessible via HTTPS. For local development, use ngrok or Tailscale Funnel to create a tunnel.
What happens if my server is slow to respond?
Visor accepts the webhook quickly and processes the message asynchronously. Responses are sent back proactively using the stored conversation reference. If Azure retries the webhook, the deduplication system prevents duplicate processing.
Can I use the same bot in multiple workflows?
Use the teams.workflow config option to route all Teams messages to a specific workflow, or use the on: [teams_message] event trigger on individual checks to handle messages in any workflow.
- Slack Integration — Bidirectional Slack integration via Socket Mode
- Telegram Integration — Telegram bot integration via long polling
- Email Integration — Bidirectional email integration via IMAP/SMTP or Resend
- WhatsApp Integration — WhatsApp bot integration via Cloud API webhooks
- Event Triggers — GitHub events and how to trigger checks
- Liquid Templates — Template syntax for dynamic content in prompts
- Configuration — Core configuration reference
- Bot Transports RFC — Technical design document for bot integrations
- examples/teams-assistant.yaml — Simple Teams chat assistant example