Microsoft Teams Integration — Self-Hosted
Self-hosted RunWhen deployments connect Microsoft Teams through the in-cluster teamsbot service. You create a dedicated Entra app and Azure Bot in your own Microsoft tenant, expose teamsbot at https://teamsbot.<domain>, supply the app credentials to the runwhen-platform Helm chart, and upload a private Teams app package into your organization’s app catalog.
Everything runs in your tenant and your cluster. There is no public Teams Store listing and no RunWhen-hosted bot — every deployment runs its own.
Teams is optional. The self-hosted helm chart always deploys teamsbot, but it stays idle until you create the Azure app, wire credentials, and install the app package.
See Self-Hosted Deployment Requirements for cluster, DNS, TLS, and registry prerequisites before configuring Teams.
What you create, and why
| Piece | Where it lives | Why it’s needed |
|---|---|---|
| Entra app registration | Your Microsoft tenant | The bot’s identity — proves who it is to Microsoft, and resolves a Teams user to their email |
| Azure Bot resource | Your Azure subscription | Connects that identity to the Teams channel and tells Microsoft where to deliver messages |
| teamsbot | Your cluster (Helm) | Receives messages, routes them to the AI Assistant, replies |
Teams app package (.zip) | Your Teams org catalog | How users find and add the bot. Built from your deployment; contains no secrets |
The Entra app is single-tenant — it exists only in your tenant, cannot see any other organization’s data, and RunWhen never gets access to it.
How the chart exposes teamsbot
| Item | Value |
|---|---|
| Public URL (Microsoft → platform) | https://teamsbot.<domain> (override: ingress.services.teamsbot.host) |
| Kubernetes Service | <release>-teamsbot:3978 (override: teamsbot.port) |
| Messaging endpoint (Azure Bot) | https://teamsbot.<domain>/api/messages |
| Graph change-notification endpoints | https://teamsbot.<domain>/graph/notifications, /graph/lifecycle |
| Health probes | /healthz, /livez |
| Must stay private | /internal/* — cluster-only routes (see Ingress hardening) |
Replace <domain> with your global.domain Helm value everywhere below. If you set a custom ingress.services.teamsbot.host, use that host instead — the Azure Bot messaging endpoint must match exactly.
Prerequisites
- A
runwhen-platformchart release that includes the teamsbot service. If<release>-teamsbotdoes not render, ask your RunWhen contact for the chart version. - Public DNS and TLS for
teamsbot.<domain>, reachable by Microsoft over HTTPS. Microsoft delivers Teams activities from the public internet — this does not work on a fully air-gapped cluster. - Entra admin access to register an app and grant admin consent.
- Teams admin access to enable custom app upload and upload the package.
- A staff RunWhen user to download the app package from the support portal.
- At least one AI Assistant configured in the target RunWhen workspace.
Step 1 — Register the Entra app
Microsoft Entra ID → App registrations → New registration (an App registration, not an Enterprise application).
- Name:
RunWhen. Supported account types: Single tenant. No redirect URI. → Register. - From Overview, copy the Application (client) ID and the Directory (tenant) ID.
- Certificates & secrets → New client secret → copy the Value immediately (Microsoft shows it once).
- API permissions → Add a permission → Microsoft Graph → Application permissions → add
User.Read.All→ Grant admin consent and confirm the green ✔.
User.Read.Allis read-only and is used for exactly one thing: mapping a Teams user’s Entra object ID to their email address so RunWhen can apply that user’s workspace permissions. Without it the bot replies “I don’t recognize you”.
Step 2 — Create the Azure Bot
Azure Portal → Create a resource → Azure Bot.
- Type of App: Single Tenant, Creation type: Use existing app registration.
- App ID = the Application (client) ID from Step 1. App tenant ID = the Directory (tenant) ID.
- Create the resource, then open it:
- Configuration → Messaging endpoint =
https://teamsbot.<domain>/api/messages→ Apply. - Leave Enable Streaming Endpoint unchecked — the bot uses plain HTTP request/response.
- Channels → Microsoft Teams → accept the terms → Apply.
- Configuration → Messaging endpoint =
Step 3 — Feed credentials into Helm
teamsbot reads three environment variables, named by the Microsoft 365 Agents SDK:
| Env var / Secret key | Value |
|---|---|
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID | Application (client) ID (Step 1) |
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET | Client secret value (Step 1) |
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID | Directory (tenant) ID (Step 1) |
Option A — Helm values
secrets: method: "k8s-secret" values: teamsClientId: "00000000-0000-0000-0000-000000000000" teamsClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" teamsTenantId: "00000000-0000-0000-0000-000000000000"The chart renders these into platform-secrets under the three env var names above.
Do not commit plaintext secrets. Inject at deploy time (for example
--set secrets.values.teamsClientSecret="$TEAMS_CLIENT_SECRET") or use the BYO Secret path below.
Option B — BYO Secret (recommended for production)
Create the Secret yourself, in your own cluster — the one where the RunWhen platform is deployed, in the same namespace as the RunWhen release. The client secret never has to leave your environment or pass through Helm values.
The Secret’s data keys must be the exact env var names — the chart mounts the Secret with envFrom, so there is no key mapping:
kubectl create secret generic rw-teams-credentials -n <namespace> \ --from-literal=CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID='<client-id>' \ --from-literal=CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET='<client-secret>' \ --from-literal=CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID='<tenant-id>'teams: existingSecret: rw-teams-credentialsWhen teams.existingSecret is set, the chart does not write those keys to platform-secrets; backend pods load your Secret instead. A typo in a key name is silent — the bot starts with empty credentials and never authenticates.
URL wiring (automatic)
BOT_PUBLIC_BASE_URL and TEAMSBOT_INTERNAL_URL are derived by the chart from your ingress host and Service name — no manual env vars unless you override ingress.services.teamsbot.host.
Sovereign clouds
For GCC High, DoD, or 21Vianet tenants, point Graph at the correct endpoint:
teamsbot: msgraphBaseUrl: "https://graph.microsoft.us/v1.0" # example — use your cloud's Graph hostStep 4 — Deploy and smoke-test the transport
- Deploy or upgrade the chart with the credentials from Step 3.
- Confirm teamsbot is healthy:
Expect a clean aiohttp startup with no missing client id / secret warnings.
Terminal window kubectl rollout status deploy/<release>-teamsbot -n <namespace>kubectl logs deploy/<release>-teamsbot -n <namespace> --tail=30 - In the Azure Portal, open the Azure Bot → Test in Web Chat and send any message. A reply means the endpoint is reachable, the ingress allows
/api/messages, and Bot Framework authentication works.
Expected in Web Chat: there is no real Teams identity behind Web Chat, so the bot cannot resolve you to a RunWhen user and answers with a “I don’t recognize you”-style response. That is normal — this step tests the pipe, not the Teams experience.
When teamsbot needs a restart
Credentials are injected as environment variables when the container starts, so they are never picked up while a pod is running.
| What changed | Restart needed? |
|---|---|
You set, changed, or removed teams.existingSecret | No — this changes the pod spec, so helm upgrade rolls the deployment for you |
| You edited the contents of your BYO Secret (for example rotating the Azure client secret) | Yes — the pod spec is unchanged, so nothing rolls on its own |
You changed secrets.values.teamsClientId / teamsClientSecret / teamsTenantId | Yes — the chart does not checksum secrets into the pod template |
kubectl rollout restart deploy/<release>-teamsbot -n <namespace>Step 5 — Build the Teams app package
The app package is a small .zip (manifest + two icons) generated from your own deployment’s configuration — your bot’s client ID is substituted into the manifest at build time. It contains no secrets; the client ID is a public app identifier.
Sign in as a staff user at https://app.<domain>/rwsupport → Teams App tab → Download package. PAPI proxies the running teamsbot, which builds runwhen-teamsbot.zip on demand from its own configured client ID — no shell or cluster access needed.
When to rebuild
The manifest is static per deployment, so you only rebuild when a RunWhen upgrade changes a manifest-level field — a new bot command, a changed permission or scope, or a new surface (tab, message extension). Re-upload it the same way; because the manifest id is your bot’s client ID, it always updates the same catalog entry rather than creating a second app.
Step 6 — Upload to your Teams org catalog
Step 0 — Allow custom app upload (the most common blocker)
Teams Admin Center → Teams apps → Manage apps → Org-wide app settings → turn on custom app upload (also called “upload custom apps” / line-of-business apps). Regulated organizations often disable this by default; if it is off, the upload below is rejected.
Upload
Teams Admin Center → Teams apps → Manage apps → Upload new app → Upload → select runwhen-teamsbot.zip. It appears in the catalog as RunWhen, status Allowed.
Make it available
Using your app permission / setup policies, allow RunWhen for a pilot group first, or org-wide. To push it directly to specific people, open the app → Users and groups → Install app → pick the users.
Users add it
In the Teams client: Apps → search “RunWhen” → Add. The Add button is normal even after an admin push. Catalog changes usually appear within minutes but can take up to 24 hours to propagate; refreshing the client often surfaces it sooner.
Step 7 — Link a RunWhen workspace
- Open a direct message with the RunWhen bot in Teams (or a channel where it is installed).
- Send
settingsand connect the RunWhen workspace you want the bot to operate against. - Ask a real question — for example “what issues are currently open?” — and confirm the Assistant answers from that workspace.
Available bot commands:
| Command | What it does |
|---|---|
help | Show available commands |
settings | Connect and configure the RunWhen workspace |
search | Find SLXs and tasks in your workspace |
runsessions | Review recent run sessions and issues |
tasks | Open the task-run form |
The bot works in personal (DM) and team/channel scopes.
Permissions explained
Three separate permission planes are involved. They are often confused, and only one of them is a Graph permission:
| Plane | Granted by | Covers |
|---|---|---|
| Receive and reply to messages sent to the bot | The Azure Bot + Teams channel, authenticated with the app’s client ID and secret | Every DM to the bot and every @mention. No Graph scope involved. |
| Read broader channel/chat context | Resource-specific consent (RSC) declared in the app manifest — ChannelMessage.Read.Group, ChatMessage.Read.Chat — consented by the team owner when the app is added to a team | Reading recent messages in the installed team/chat for conversational context |
| Resolve a Teams user to their email | User.Read.All Graph application permission, admin-consented in Step 1 | Mapping the Teams user to a RunWhen user so workspace permissions apply |
RSC consent happens automatically at install time — the team owner sees a one-time prompt, not a separate admin review.
Known limitations
| Limitation | Details |
|---|---|
| Workspace connections are tenant-scoped | Connected workspaces are keyed on your Microsoft tenant, not on the individual bot app. If more than one RunWhen Teams app exists in the same tenant, they share the same pool of connected workspaces. The design assumes one RunWhen Teams app per tenant. |
| No public Store listing | Distribution is private (org catalog upload) only. There is no discoverability through the public Teams Store. |
| No disable toggle | The teamsbot workload and ingress are always rendered. To fully disable, scale the deployment to zero and remove the teamsbot ingress host. |
| Air-gap | Microsoft must reach https://teamsbot.<domain>/api/messages from the public internet, and the bot must reach Microsoft. Teams is effectively unavailable in air-gapped installs. |
Ingress hardening
Only three teamsbot routes need to be reachable from the internet: /api/messages, /graph/notifications, and /graph/lifecycle. The /internal/* routes are cluster-only — they deliberately bypass the Microsoft JWT check and rely on not being publicly reachable.
When the chart renders the Ingress itself — ingress.enabled: true with type: ingress, an nginx controller, and ingress.allowSnippetAnnotations: true — it adds an nginx server-snippet that denies /internal/* for you, and there is nothing further to do.
In any other setup you own that deny yourself, because the snippet is either not rendered or not honoured:
- You front the platform with your own ingress or load balancer (
ingress.enabled: false— the chart default). - Gateway API mode (
ingress.type: gateway) — snippet annotations have no effect there. - A non-nginx controller (Traefik, HAProxy, cloud LB) — the nginx annotation is ignored.
- A cluster whose ingress-nginx admission webhook rejects snippets, so
ingress.allowSnippetAnnotationsis set tofalse.
Troubleshooting
| Symptom | Resolution |
|---|---|
| Bot never responds; Web Chat times out | Confirm https://teamsbot.<domain>/api/messages is publicly reachable with valid TLS, and that the Azure Bot messaging endpoint matches it exactly |
| Pod logs show missing client id / secret | Check the three CONNECTIONS__SERVICE_CONNECTION__SETTINGS__* keys — in secrets.values.teams* (Option A) or, for a BYO Secret, spelled exactly as the env var names; restart the deployment after changing the Secret |
| Bot replies “I don’t recognize you” in Teams | User.Read.All is not admin-consented on the Entra app, or the Teams user’s email does not match a RunWhen user with workspace permissions |
| Bot can’t read channel messages for context | RSC was not consented — have a team owner re-add the app to the team to trigger the prompt |
| Admin cannot upload the package | Custom app upload is disabled — enable it in Org-wide app settings first |
| Package download returns 409 | The bot client ID is not set on teamsbot — complete Step 3 and restart |
| Package download returns 502 | PAPI cannot reach teamsbot in-cluster — check the <release>-teamsbot Service and pod health |
| App not visible in Teams after upload | Catalog propagation can take minutes to ~24h; refresh the client and confirm your app permission policy allows RunWhen |
| Commands work but results are from the wrong workspace | Run settings and reconnect; remember connections are tenant-scoped |
Related pages
- Self-Hosted Deployment Requirements — cluster, DNS, TLS, and registry prerequisites
- Slack Integration — Self-Hosted — the equivalent setup for Slack
- Workspace Chat — AI Assistant capabilities available through Teams
- AI Assistants — assistant profiles and when to use each one