Microsoft Teams¶
Microsoft Teams works with pg_relay through the webhook transport — the same zero-Processor-code pattern as Slack and Mattermost, with pg_relay_notifier supplying the payload and interpreting the response.
One thing to get out of the way first: if you have used Teams webhooks before, the mechanism you remember is probably the classic Office 365 Connector incoming webhook. Microsoft has retired connectors; their URLs no longer accept posts. The current mechanism is a Workflow (Power Automate) with the "when a webhook request is received" trigger, and that is what this chapter sets up. The recipe below only works against a Workflow URL.
One-time Teams setup¶
- In Teams, go to the channel notifications should land in → ⋯ (more options) → Workflows → pick the template "Post to a channel when a webhook request is received". (The same template is available from the Workflows app in Teams, or from
make.powerautomate.com, if the channel menu doesn't offer it.) - Name it (e.g.
pg-relay), confirm the team and channel, and create it. - Teams shows the workflow's URL — a long
https://...logic.azure.com/...address ending in asig=...signature. Copy it now; it can be retrieved later from the flow's trigger step in Power Automate, but nowhere in Teams itself.
That signature in the URL is the only credential — anyone who has the URL can post through the workflow. As with Mattermost's hook key, treat the whole URL as a secret and keep it out of the database with an _env: reference on the url field itself:
The workflow belongs to whoever created it
A workflow is owned by the account that created it, posts appear under that account's name (suffixed "via Workflows"), and if that account is ever disabled — someone leaves the company — the workflow stops with it. For anything production-grade, create the workflow from a service account rather than a personal one, or add co-owners in Power Automate.
Profile and message¶
// profile
{
"url": "_env:TEAMS_WORKFLOW_URL",
"auth": {"style": "bearer_header", "secret": "unused"},
"timeout_seconds": 30
}
// message — the Workflows trigger's expected shape, sent verbatim
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "disk alert: volume is filling", "wrap": true}
]
}
}
]
}
The auth block is the same placeholder arrangement as Mattermost's incoming webhook: the URL alone authenticates, Teams ignores the Authorization header, but the webhook transport has no "no authentication" option — so a harmless Bearer unused goes along for the ride.
The message envelope, though, is not negotiable the way Slack's is. The Workflows template expects an Adaptive Card wrapped in exactly this type: "message" / attachments structure — a bare Slack-style {"text": ...} body is rejected, and plain text goes inside a TextBlock element instead. Keep "wrap": true on every TextBlock, or Teams truncates long lines to a single row.
Formatting: Adaptive Cards, not mrkdwn or Markdown¶
This is the third text dialect in three chat chapters. A TextBlock renders only a small Markdown subset — **bold**, _italic_, [links](url), bullet and numbered lists — with no headings, tables, or code fences; structure beyond that comes from card elements (a FactSet for label/value pairs is the natural alert layout, Container with a style for a coloured header). The Adaptive Cards designer previews a card as Teams will render it, and version: "1.4" is a safe ceiling for what Teams supports. As with Slack and Mattermost, if pg_relay_notifier composes for more than one of these targets, each needs its own rendering — none of the three dialects survives a round trip through the others.
Interpreting the response¶
Have classify_webhook_response treat:
202(Accepted) assent. The body is empty — there is no message id, so returnNULLas the provider reference.400asfailed— the body wasn't valid JSON, or wasn't the card envelope the trigger expects.401/404asfailed— a bad or regenerated signature, or a workflow that has been deleted; either way the stored URL is stale and needs replacing.429, any5xx, andhttp_status = 0(no response) asretry— Power Automate throttles per-flow, and a burst of notifications self-heals through the retry chain.
202 means accepted, not delivered
The status answers for the trigger, not the post: the workflow run that actually writes the message into the channel happens after the response is already sent, and can still fail on its own (the flow's owner lost access, the channel was deleted, the card exceeds Teams' size limit). Those failures never reach classify_webhook_response — they show up in the workflow's run history in Power Automate, which is the place to look when pg_relay records sent but no message appears.
Why there is no bot-token route¶
Slack and Mattermost each got a second recipe: a revocable API token instead of a secret-bearing URL. Teams has no equivalent within pg_relay's reach — posting a channel message through the Microsoft Graph API requires delegated permissions (a signed-in user), and the client-credentials flow pg_relay's Microsoft 365 transport uses is not permitted to send Teams channel messages. The workflow URL is the supported route, not a compromise.
Continue to SMS API Providers — a compatibility assessment of twelve SMS APIs, with worked webhook profiles for the eleven that fit.