Skip to content

feat(automations): Microsoft Teams trigger provider - #6592

Merged
saddlepaddle merged 1 commit into
mainfrom
feat/trigger-provider-teams
Aug 18, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
feat/trigger-provider-teams

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adds microsoft_teams as a trigger provider for automations, following the additive seam from #6571. Enum values are already on main (#6581) — no migration.

Events (fixed list from the brief):

event sentence
message_in_channel [Any message / containing "…"] in team › channel by Anyone/Me
channel_created Channel created in team [name contains "…"]

Shape — Google-shaped, fetch-on-notify

  • Connection = tenant admin consent, not a user sign-in. connect redirects to Entra's organizations/v2.0/adminconsent with .default; callback proves consent by acquiring an app-only client-credentials token for the returned tenant (the tenant param alone is attacker-controlled), upserts integration_connections (externalOrgId = tenant id, token cached with expiry, config = {tenantId, clientState, subscriptions}), creates the Graph subscriptions, then chains an OpenID auth-code sign-in (identity/callback) so the consenting admin's Entra oid lands in user_identities (provider microsoft_teams, externalScopeId = tenant) — that is what me resolves through. Declining that leg costs only "Me", not the connection.
  • Subscriptions: two tenant-wide per connection — /teams/getAllMessages and /teams/getAllChannels, changeType: created, includeResourceData: false (no encryption certificate). Requested for 2 days (Graph allows 3 for these), renewed once within 12h by POST /api/integrations/microsoft-teams/jobs/renew-subscriptions (QStash-signed, same pattern as automations/evaluate). All subscription writes go through the per-connection advisory lock.
  • Notify + lifecycle routes answer inside Graph's 3-second window: echo ?validationToken= as text/plain; otherwise parse the collection, look up the connection by tenant, compare clientState with timingSafeEqual (the only auth Graph offers without resource data), enqueue each accepted notification to QStash (deduped on subscription + changeType + resource), and answer 202. Refused notifications are acknowledged too — they will not verify on retry either.
  • Process route (/api/integrations/microsoft-teams/process, QStash-signed) does the slow half: re-reads the connection and re-checks clientState, GETs the chatMessage / channel from Graph, records it in automation_events (idempotent on connection + channel + message id; payload is the fetched resource) and dispatches through triggerMatches. For lifecycle items (reauthorizationRequired / subscriptionRemoved) it forces the renew/recreate step. Non-2xx → QStash retries. System messages (messageType != "message", e.g. "X added Y") are skipped.
  • Matcher automation-matching/microsoft-teams.ts + one case: null team/channel scope matches nothing; channel_created ignores channel + actor (its sentence has neither); messageFilter is substring over the message text (HTML stripped) or the new channel's name. MatchContext.microsoftTeams = {teamId, channelId, ownerIds}.
  • Desktop: providers/microsoftTeams/ (grammar, renderer, useTeamsOptions → integration.microsoftTeams.listTeams/listChannels, channels labelled "Team › Channel"), one registry line, one useProviderOptions line. ActorChip offers Anyone/Me only — the shared people list is GitHub ids. (Option keys will move to the provider-namespaced shape once the consolidated seam lands.)
  • Web: /integrations/microsoft-teams page (copy of Slack's) with Connect/Disconnect and subscription state; card on /integrations.

Prerequisites (documented, not yet provisioned)

  1. Entra app registration, multi-tenant, with application permissions ChannelMessage.Read.All, Channel.ReadBasic.All, Team.ReadBasic.All (Organization.Read.All optional — only the tenant display name). Redirect URIs: {API}/api/integrations/microsoft-teams/callback and {API}/api/integrations/microsoft-teams/identity/callback.
  2. MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET in api + trpc env (both optional — unset means the integration is off and nothing else changes).
  3. App-only channel-message reads are a Microsoft protected API: Graph 403s subscription create until the app id is approved via Microsoft's request form.
  4. A QStash schedule hitting /api/integrations/microsoft-teams/jobs/renew-subscriptions hourly.
  5. /teams/getAllMessages is subscribed without ?model= → Graph evaluation mode, capped at 500 messages / app / tenant / month. Flipping to model A (E5 licences) or B (per-message billing) is one constant in subscriptions.ts.

Verification

  • typecheck 0: shared, db, trpc, api, desktop, web. Lint: clean except the pre-existing TerminalAgentAutoResume a11y failure on main.
  • Desktop over CDP (this worktree's renderer, vite :8865, api :8861): Add Trigger → Microsoft Teams → both events; Save with empty scopes refused with team/channel chips marked; configured (Any team › Any channel, filter "deploy"; Channel created in Any team, "incident"), saved, persisted across a full reload; DB rows carry the expected config.
  • Notify route: ?validationToken=abc%20123%2Bxyz → 200 text/plain abc 123+xyz (lifecycle route too); bad JSON 400; unknown tenant refused; wrong clientState refused; correct clientState accepted (before the queue split, the same request reached the real Graph GET and got the expected 401 on a placeholder token; with the queue, QStash rejects loopback URLs locally, as it does for every other automation route).
  • Ingest (ingestChannelMessage / ingestChannel, what the process route calls after the Graph GET) with Graph-shaped chatMessage / channel payloads against the real DB: rows recorded with title/url/actor/resourceKey, redelivery deduped, matcher selects 1/2 triggers for a "deploy" message and 1/2 for an "incident-…" channel, 0/2 for non-matching.
  • Not verified live: consent flow, identity leg, subscription create/renew, and a real Graph push — need the tenant + protected-API approval above.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@saddlepaddle, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Limit details: You’ve used all 10 included reviews currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 07068f91-c04b-4991-84a5-4d0ded03e57f

📥 Commits

Reviewing files that changed from the base of the PR and between 92a73da and f3426fe.

📒 Files selected for processing (37)
  • .env.example
  • apps/api/src/app/api/integrations/microsoft-teams/callback/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/connect/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/identity/callback/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/identity/identityFlow.ts
  • apps/api/src/app/api/integrations/microsoft-teams/jobs/renew-subscriptions/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/lifecycle/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/notify/handleNotification.ts
  • apps/api/src/app/api/integrations/microsoft-teams/notify/notifications.ts
  • apps/api/src/app/api/integrations/microsoft-teams/notify/queue.ts
  • apps/api/src/app/api/integrations/microsoft-teams/notify/recordTeamsEvent.ts
  • apps/api/src/app/api/integrations/microsoft-teams/notify/route.ts
  • apps/api/src/app/api/integrations/microsoft-teams/process/route.ts
  • apps/api/src/env.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/providers/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/providers/microsoftTeams/grammar.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/providers/microsoftTeams/microsoftTeams.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/providers/microsoftTeams/useTeamsOptions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/components/providers/useProviderOptions.ts
  • apps/web/src/app/(dashboard-legacy)/integrations/microsoft-teams/components/ConnectionControls/ConnectionControls.tsx
  • apps/web/src/app/(dashboard-legacy)/integrations/microsoft-teams/components/ConnectionControls/index.ts
  • apps/web/src/app/(dashboard-legacy)/integrations/microsoft-teams/components/ErrorHandler/ErrorHandler.tsx
  • apps/web/src/app/(dashboard-legacy)/integrations/microsoft-teams/components/ErrorHandler/index.ts
  • apps/web/src/app/(dashboard-legacy)/integrations/microsoft-teams/page.tsx
  • apps/web/src/app/(dashboard-legacy)/integrations/page.tsx
  • packages/db/src/schema/types.ts
  • packages/shared/src/automation-matching/index.ts
  • packages/shared/src/automation-matching/microsoft-teams.ts
  • packages/shared/src/automation-triggers.ts
  • packages/trpc/src/env.ts
  • packages/trpc/src/lib/integrations/microsoft-teams/index.ts
  • packages/trpc/src/router/integration/integration.ts
  • packages/trpc/src/router/integration/microsoft-teams/graph.ts
  • packages/trpc/src/router/integration/microsoft-teams/index.ts
  • packages/trpc/src/router/integration/microsoft-teams/microsoft-teams.ts
  • packages/trpc/src/router/integration/microsoft-teams/resources.ts
  • packages/trpc/src/router/integration/microsoft-teams/subscriptions.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Admin (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

Two triggers: "Message in channel" (team › channel, actor, text filter)
and "Channel created" (team, name filter), matched against Graph change
notifications for a tenant the admin consented to.

Shape is fetch-on-notify: the Entra app registration holds application
permissions (ChannelMessage.Read.All, Channel.ReadBasic.All,
Team.ReadBasic.All), consent is the tenant-wide adminconsent endpoint, and
the connection caches an app-only client-credentials token. Two tenant-wide
subscriptions per connection (/teams/getAllMessages, /teams/getAllChannels,
created only, no resource data so no encryption certificate), requested for
two days and renewed by a QStash-signed job once within twelve hours; the
lifecycle route answers reauthorizationRequired / subscriptionRemoved by
forcing that renewal.

Graph gives a notification endpoint three seconds and throttles endpoints
that miss it, so the notify and lifecycle routes only echo the
validationToken handshake, compare clientState in constant time against
the tenant's connection and hand each accepted notification to QStash; a
signed process route re-checks the connection, GETs the message or channel
from Graph, records it in automation_events (idempotent on connection +
resource id) and dispatches through the shared dispatchMatchingTriggers.

After consent the admin is sent through an OpenID sign-in so their Entra
object id lands in user_identities (provider microsoft_teams, scoped to the
tenant), which is what `me` on a Teams trigger resolves through.

Desktop: providers/microsoftTeams (grammar, renderer, useTeamsOptions under
options.microsoftTeams), registry line, useProviderOptions line. tRPC:
integration.microsoftTeams {getConnection, disconnect, listTeams,
listChannels}. Web: the /integrations/microsoft-teams page carrying the
Connect button. Env: MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET, optional
so every other environment keeps booting.

No migration: the enum values landed in #6581. The consent-callback upsert
names the partial-index predicate (provider <> 'google') so Postgres infers
the org-scoped uniqueness from #6603.
@saddlepaddle
saddlepaddle force-pushed the feat/trigger-provider-teams branch from 86a49c0 to f3426fe Compare August 18, 2026 05:03
@saddlepaddle
saddlepaddle merged commit 90df7e4 into main Aug 18, 2026
13 checks passed

This branch was previously deployed

1 inactive deployment
preview — f3426fe7 Deployed Aug 18, 2026 by saddlepaddle via Deploy Docs #7992
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant