Setup
This guide walks through everything needed to connect a Microsoft Teams bot to OctoMesh,
step by step — first in the Azure Portal, then the equivalent Azure CLI (az)
commands, and finally the OctoMesh side (configuration, pipeline, Teams app package, test).
Prerequisites
- An Azure subscription and permission to create an App registration and an Azure Bot resource.
- A Microsoft Teams account with permission to sideload a custom app (if greyed out, a Teams admin must enable Upload custom apps in the app setup policy).
- A running OctoMesh mesh adapter for the target tenant (the bot's messaging endpoint points at it).
- For local development: a tunnelling tool such as dev tunnels or ngrok to expose the local adapter over HTTPS.
The examples use the resource group my-rg, the bot handle octo-accounting-bot, and the
tenant id salzburgdev. Replace them with your own values.
Step 1 — App registration + client secret
The App registration is the bot's identity. Its client ID/secret are used both for the
inbound authentication and for TeamsBotReply@1's outbound token.
Portal
- Microsoft Entra ID → App registrations → + New registration.
- Name:
octo-accounting-bot. - Supported account types: Accounts in this organizational directory only (Single tenant).
- Register, then note Application (client) ID and Directory (tenant) ID.
- Certificates & secrets → + New client secret → set an expiry → Add → copy the Value immediately (it is shown only once).
Azure CLI
# create a single-tenant app registration
az ad app create --display-name "octo-accounting-bot" --sign-in-audience AzureADMyOrg
# -> note the "appId" from the output
# add a client secret (prints the secret once)
az ad app credential reset --id <APP_ID> --append --display-name teams-bot --years 1 \
--query password -o tsv
# your tenant id
az account show --query tenantId -o tsv
Keep three values for later: App (client) ID, client secret, tenant ID.
Step 2 — Azure Bot resource
Portal
-
Create a resource → search "Azure Bot" → Create.
-
Bot handle:
octo-accounting-bot. -
Choose your Subscription and Resource group.
-
Pricing tier: Change plan → F0 (Free) is sufficient for most scenarios.
-
Microsoft App ID:
- Type of App: Single Tenant.
- Creation type: Use existing app registration → paste the App ID from Step 1.
Do not pick "User-Assigned Managed Identity"A Managed Identity bot has no client secret and cannot authenticate the self-hosted mesh adapter. Use Single Tenant with the app registration from Step 1.
-
Review + Create → Create.
-
On the bot resource, open Channels → Microsoft Teams, accept the terms and Apply.
-
On Configuration, set the Messaging endpoint (see Step 3 for the URL) and Apply.
Azure CLI
RG=my-rg
BOT=octo-accounting-bot
APP_ID=<APP_ID> # from Step 1
TENANT_ID=<TENANT_ID> # from Step 1
ENDPOINT="https://<public-host>/salzburgdev/teamsBot" # from Step 3
# create the bot (single tenant, free tier)
az bot create -g $RG -n $BOT --app-type SingleTenant --appid $APP_ID \
--tenant-id $TENANT_ID --endpoint "$ENDPOINT" --sku F0
# enable the Microsoft Teams channel
az bot msteams create -n $BOT -g $RG
# (later) update just the messaging endpoint, e.g. after the tunnel URL changes
az bot update -n $BOT -g $RG --endpoint "$ENDPOINT"
# verify
az bot show -n $BOT -g $RG \
--query "{appId:properties.msaAppId, appType:properties.msaAppType, endpoint:properties.endpoint}" -o json
az bot msteams show -n $BOT -g $RG --query "properties.properties.isEnabled" -o tsv
A missing messaging endpoint or a disabled Teams channel both surface in Teams as
"Invalid bot". The two show commands above confirm the endpoint is set and
isEnabled is true.
Step 3 — Expose the messaging endpoint
The messaging endpoint is https://<public-host>/{tenant}/teamsBot, where {tenant} is the
adapter's tenant id (for example salzburgdev) and <public-host> reaches the mesh adapter.
Local development
Expose the adapter's HTTP listener through a tunnel (avoids the self-signed-TLS hop):
# dev tunnels
devtunnel host -p 5041 --allow-anonymous
# or ngrok
ngrok http 5041
Take the tunnel's public HTTPS URL and set the messaging endpoint to
https://<tunnel-host>/salzburgdev/teamsBot. With ngrok's free plan the URL changes on each
restart — reserve a static domain or re-run az bot update --endpoint … after each start.
Production
Point the messaging endpoint at the adapter's public HTTPS address (for example behind an
ingress / reverse proxy), keeping the /{tenant}/teamsBot path.
Step 4 — Configure OctoMesh
1. Bot credentials (MicrosoftGraphConfiguration)
Provide the App ID / secret / tenant id as a System.Communication/MicrosoftGraphConfiguration
entity. Store the secret outside of source control (a local secrets file, a key vault, …).
- rtId: <config-rtId>
ckTypeId: System.Communication/MicrosoftGraphConfiguration
rtWellKnownName: MicrosoftGraphDocuments
attributes:
- id: System.Communication/AzureTenantId
value: "<tenant id>" # REQUIRED for a single-tenant bot
- id: System.Communication/ClientId
value: "<app (client) id>"
- id: System.Communication/ClientSecret
value: "<client secret>"
2. Pipeline with FromTeamsBot@1 and TeamsBotReply@1
The pipeline must carry a System.Communication/Uses association to the
MicrosoftGraphConfiguration (the GlobalConfiguration is built only from Uses
associations — the name alone is not resolved).
# on the Pipeline entity
associations:
- roleId: System.Communication/Uses
targetRtId: <config-rtId>
targetCkTypeId: System.Communication/MicrosoftGraphConfiguration
A minimal echo pipeline definition:
triggers:
- type: FromTeamsBot@1
serverConfiguration: MicrosoftGraphDocuments
route: /teamsBot
validateInboundToken: false # local dev; enable + harden before public exposure
transformations:
- type: TeamsBotReply@1
serverConfiguration: MicrosoftGraphDocuments
messageBody: "Hello from OctoMesh!"
# serviceUrlPath / conversationIdPath default to $.Conversation.ServiceUrl / .ConversationId
A real pipeline branches on $.Emails[0].Attachments: attachments → ingest the file(s), plain
text → answer (e.g. with AnthropicAiQuery@1), then reply through TeamsBotReply@1 reading
$.Conversation.
3. Deploy
Import the configuration and pipeline and deploy the data flow with the octo-cli
(ImportRt, DeployDataFlow). After deployment the adapter logs
FromTeamsBot: listening on /teamsBot.
Step 5 — Teams app package
A bot is reached in Teams through a small app package (a zip containing manifest.json and
two icons). The manifest's bots[].botId must be the bot's App ID, and supportsFiles: true
is required so the bot can receive file uploads in 1:1 chats.
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
"manifestVersion": "1.17",
"version": "1.0.0",
"id": "<app (client) id>",
"developer": {
"name": "Your Company",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"termsOfUseUrl": "https://example.com/terms"
},
"icons": { "color": "color.png", "outline": "outline.png" },
"name": { "short": "Accounting Assistant", "full": "OctoMesh Accounting Assistant" },
"description": { "short": "Upload invoices and ask questions.", "full": "…" },
"accentColor": "#0076D7",
"bots": [
{ "botId": "<app (client) id>", "scopes": ["personal"], "supportsFiles": true }
],
"permissions": ["identity", "messageTeamMembers"],
"validDomains": []
}
Provide color.png (192×192) and outline.png (32×32, transparent), zip the three files at the
root of the archive, then in Teams: Apps → Manage your apps → Upload an app →
Upload a custom app and select the zip.
Step 6 — Test
- Open the bot from Teams (the Add dialog after sideloading, or search its app name).
- Send a text message → the pipeline runs and
TeamsBotReply@1answers in the chat. - Attach a file (e.g. a PDF) → the bot ingests it (
$.Emails[0].Attachments[0].Data) and replies.
Watch the adapter log to confirm the round-trip:
FromTeamsBot: listening on /teamsBot
FromTeamsBot: processed activity from <user> with <n> attachment(s)
Node reference
FromTeamsBot@1 (trigger)
| Property | Description |
|---|---|
serverConfiguration | WellKnownName of the MicrosoftGraphConfiguration (bot App ID/secret + tenant). |
route | Relative route of the messaging endpoint; the tenant prefix is added by the adapter. Default /teamsBot. |
validateInboundToken | Validate the inbound Bot Framework JWT. Default false (local dev). See Security below. |
botAppId | Expected token audience; defaults to the configuration's ClientId. |
Output: $.Emails[] (message + attachments, same shape as the e-mail triggers) and
$.Conversation (ServiceUrl, ConversationId, ActivityId, FromId, FromName,
FromAadObjectId).
TeamsBotReply@1 (load)
| Property | Description |
|---|---|
serverConfiguration | WellKnownName of the MicrosoftGraphConfiguration. |
serviceUrlPath | JSONPath to the Bot Framework serviceUrl. Default $.Conversation.ServiceUrl. |
conversationIdPath | JSONPath to the conversation id. Default $.Conversation.ConversationId. |
replyToActivityIdPath | Optional JSONPath for a threaded reply. Default $.Conversation.ActivityId. |
messageBodyPath | JSONPath to the reply text (e.g. an AI answer). |
messageBody | Literal reply text (used when messageBodyPath is blank). |
timeoutSeconds | HTTP timeout. Default 30. |
continueOnError | Continue the pipeline if sending fails. Default true. |
Security
validateInboundToken controls inbound authentication:
false(default) — no authentication. Acceptable only for local development behind a private dev tunnel or the Bot Framework Emulator.true— the inbound Bot Framework JWT is checked (audience + expiry).
The current inbound check validates the token's audience and expiry but not yet its cryptographic signature. Before exposing the endpoint publicly (test/production), place it behind full Bot Framework token validation and reject unauthenticated requests. Until then any caller who knows the URL can trigger the pipeline.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "Invalid bot" / "Ungültiger Bot" in Teams | Messaging endpoint empty or Teams channel not enabled. Verify with az bot show … --query properties.endpoint and az bot msteams show … --query properties.properties.isEnabled; fix with az bot update --endpoint … and az bot msteams create. |
| Bot Framework POSTs arrive (200) but no pipeline runs | The first activities on opening a chat are conversationUpdate/typing events, which the trigger ignores by design. Only message activities run the pipeline. |
invalid_client / no reply is delivered | Wrong token authority. For a single-tenant bot, AzureTenantId must be set on the MicrosoftGraphConfiguration (the reply node then uses the tenant authority). |
| Files are not received in a 1:1 chat | The Teams app manifest is missing "supportsFiles": true in the bots entry. |
| Reply fails with a DNS/socket error to the service URL | Expected when testing with a fake serviceUrl; a real Teams conversation provides a reachable serviceUrl. |
No FromTeamsBot: listening on /teamsBot in the log | The data flow is not deployed, or the adapter build predates the node — redeploy and restart the adapter. |