Skip to content

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

PieceWhere it livesWhy it’s needed
Entra app registrationYour Microsoft tenantThe bot’s identity — proves who it is to Microsoft, and resolves a Teams user to their email
Azure Bot resourceYour Azure subscriptionConnects that identity to the Teams channel and tells Microsoft where to deliver messages
teamsbotYour cluster (Helm)Receives messages, routes them to the AI Assistant, replies
Teams app package (.zip)Your Teams org catalogHow 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

ItemValue
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 endpointshttps://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-platform chart release that includes the teamsbot service. If <release>-teamsbot does 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).

  1. Name: RunWhen. Supported account types: Single tenant. No redirect URI. → Register.
  2. From Overview, copy the Application (client) ID and the Directory (tenant) ID.
  3. Certificates & secrets → New client secret → copy the Value immediately (Microsoft shows it once).
  4. API permissions → Add a permission → Microsoft Graph → Application permissions → add User.Read.AllGrant admin consent and confirm the green ✔.

User.Read.All is 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.

  1. Type of App: Single Tenant, Creation type: Use existing app registration.
  2. App ID = the Application (client) ID from Step 1. App tenant ID = the Directory (tenant) ID.
  3. Create the resource, then open it:
    • Configuration → Messaging endpoint = https://teamsbot.<domain>/api/messagesApply.
    • Leave Enable Streaming Endpoint unchecked — the bot uses plain HTTP request/response.
    • Channels → Microsoft Teams → accept the terms → Apply.

Step 3 — Feed credentials into Helm

teamsbot reads three environment variables, named by the Microsoft 365 Agents SDK:

Env var / Secret keyValue
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTIDApplication (client) ID (Step 1)
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRETClient secret value (Step 1)
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTIDDirectory (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.

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:

Terminal window
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-credentials

When 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 host

Step 4 — Deploy and smoke-test the transport

  1. Deploy or upgrade the chart with the credentials from Step 3.
  2. Confirm teamsbot is healthy:
    Terminal window
    kubectl rollout status deploy/<release>-teamsbot -n <namespace>
    kubectl logs deploy/<release>-teamsbot -n <namespace> --tail=30
    Expect a clean aiohttp startup with no missing client id / secret warnings.
  3. 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 changedRestart needed?
You set, changed, or removed teams.existingSecretNo — 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 / teamsTenantIdYes — the chart does not checksum secrets into the pod template
Terminal window
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>/rwsupportTeams 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.


  1. Open a direct message with the RunWhen bot in Teams (or a channel where it is installed).
  2. Send settings and connect the RunWhen workspace you want the bot to operate against.
  3. Ask a real question — for example “what issues are currently open?” — and confirm the Assistant answers from that workspace.

Available bot commands:

CommandWhat it does
helpShow available commands
settingsConnect and configure the RunWhen workspace
searchFind SLXs and tasks in your workspace
runsessionsReview recent run sessions and issues
tasksOpen 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:

PlaneGranted byCovers
Receive and reply to messages sent to the botThe Azure Bot + Teams channel, authenticated with the app’s client ID and secretEvery DM to the bot and every @mention. No Graph scope involved.
Read broader channel/chat contextResource-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 teamReading recent messages in the installed team/chat for conversational context
Resolve a Teams user to their emailUser.Read.All Graph application permission, admin-consented in Step 1Mapping 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

LimitationDetails
Workspace connections are tenant-scopedConnected 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 listingDistribution is private (org catalog upload) only. There is no discoverability through the public Teams Store.
No disable toggleThe teamsbot workload and ingress are always rendered. To fully disable, scale the deployment to zero and remove the teamsbot ingress host.
Air-gapMicrosoft 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.allowSnippetAnnotations is set to false.

Troubleshooting

SymptomResolution
Bot never responds; Web Chat times outConfirm 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 / secretCheck 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 TeamsUser.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 contextRSC was not consented — have a team owner re-add the app to the team to trigger the prompt
Admin cannot upload the packageCustom app upload is disabled — enable it in Org-wide app settings first
Package download returns 409The bot client ID is not set on teamsbot — complete Step 3 and restart
Package download returns 502PAPI cannot reach teamsbot in-cluster — check the <release>-teamsbot Service and pod health
App not visible in Teams after uploadCatalog 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 workspaceRun settings and reconnect; remember connections are tenant-scoped