Skip to content
CloudOps
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 10 min read

Translate Any Webhook Payload Into Adaptive Cards With AI

Every tool sends a different JSON shape. Use an LLM to generate the mapping from arbitrary webhook payloads to clean Teams Adaptive Cards, then bake it into code.

  • #microsoft-teams
  • #adaptive-cards
  • #webhooks
  • #ai
  • #integration

Here’s a problem that scales with every tool you adopt: each one sends webhooks in its own special JSON dialect. GitHub’s payload looks nothing like Datadog’s, which looks nothing like your CI system’s, which looks nothing like the custom service one of your teams wrote. If you want all of them showing up as clean Adaptive Cards in Teams, you’re writing a bespoke mapping for each, and that’s a lot of fiddly payload["deeply"]["nested"]["field"] code.

This is one of the best uses of an LLM I’ve found in Teams work. The model is excellent at looking at an unfamiliar JSON payload and producing a mapping to a target card shape. The important discipline is that you use AI to generate the mapping once, then bake that mapping into deterministic code — you do not run a live LLM call on every webhook. The model is a fast junior engineer writing the adapter; a human reviews it, and it runs as plain code in production.

Generate the mapping, not a per-request transform

The temptation is to pipe every incoming webhook straight through the model and ask for a card. Don’t. That’s slow, costs tokens per event, is non-deterministic, and puts an LLM in your hot path during an incident — the worst possible time for latency. Instead, give the model a sample payload and the card shape you want, and ask it to write the transform function.

Here is a sample webhook payload from our CI system. Write a Python
function `to_card(payload: dict) -> dict` that returns an Adaptive Card
(version 1.5) for Teams with:
- Title: the build name and status, status color (failed=Attention,
  passed=Good).
- Facts: branch, commit (short), duration, triggered_by.
- An OpenUrl action "View build" -> the build URL.
Handle missing fields gracefully (show "—"). Return only the function.

Sample payload:
{ "build": { "name": "api-ci", "status": "failed", "url": "...",
  "branch": "main", "commit": "a1b2c3d4e5", "duration_s": 142 },
  "actor": "jjoyner" }

What you get back is a real adapter function:

def to_card(payload: dict) -> dict:
    b = payload.get("build", {})
    status = b.get("status", "unknown")
    color = {"failed": "Attention", "passed": "Good"}.get(status, "Warning")
    commit = (b.get("commit") or "")[:7] or "—"
    return {
        "type": "AdaptiveCard", "version": "1.5",
        "body": [
            {"type": "TextBlock", "weight": "Bolder", "size": "Large",
             "color": color,
             "text": f"{b.get('name','build')}{status}"},
            {"type": "FactSet", "facts": [
                {"title": "Branch", "value": b.get("branch", "—")},
                {"title": "Commit", "value": commit},
                {"title": "Duration", "value": f"{b.get('duration_s','—')}s"},
                {"title": "By", "value": payload.get("actor", "—")},
            ]},
        ],
        "actions": [
            {"type": "Action.OpenUrl", "title": "View build",
             "url": b.get("url", "")}
        ],
    }

This runs in microseconds with zero AI involvement at request time. You generated it once with the model’s help and now it’s just code.

Test the adapter against real and broken payloads

The model handles the happy path well. Where it needs review is the edge cases: missing fields, null values, unexpected types. Write tests — or have the model write them — covering a payload with missing keys, a null URL, and a status the mapping doesn’t recognize.

def test_missing_fields():
    card = to_card({"build": {}})
    assert card["body"][0]["color"] == "Warning"  # unknown status fallback
    assert card["actions"][0]["url"] == ""

Pro Tip: Feed the model two or three real payloads, not one. Webhook payloads vary by event type — a “build started” payload has different fields than “build failed” — and giving the model the variants up front means the generated adapter handles them instead of crashing on the second one in production.

Validate the card output, not just the function

Even a correct-looking adapter can emit invalid card JSON if a field contains something unexpected. Run the adapter’s output through Adaptive Card schema validation in your test suite so a malformed payload can’t post a broken card to a live channel.

import jsonschema, requests
SCHEMA = requests.get("https://adaptivecards.io/schemas/adaptive-card.json").json()

def test_output_is_valid_card():
    card = to_card(SAMPLE_PAYLOAD)
    jsonschema.Draft7Validator(SCHEMA).validate(card)

A broken card in front of the on-call channel during an incident erodes trust in the whole alerting setup. Catch it in CI.

Keep production payloads and secrets out of the prompt

The payloads you give the model to design the mapping should be sanitized. Real webhook payloads carry internal hostnames, user identifiers, sometimes signed URLs with embedded tokens. Strip those to representative placeholders before pasting. The model needs the shape of the data — field names and types — not your production values.

And because the adapter runs as plain code, the model is never in your runtime at all. It never sees a live payload, never holds the Teams webhook URL the card gets posted to, and never touches a credential. That webhook URL is itself a secret — store it in your secrets manager and verify the posting endpoint’s security yourself, independent of anything the AI suggested.

Review the adapter like any other code

The generated function is code your service runs, so it goes through the same review as anything else. A human reads it, checks the field mappings against the real payload, confirms the error handling, and approves the merge. The AI wrote a fast first draft; the human owns what ships.

This is the pattern that makes AI safe and useful in integration work: the model is a fast junior engineer that generates the adapter, a human reviews before it deploys, you keep the LLM out of the request hot path, you verify the webhook security yourself, and you never hand the model real production payloads or credentials. Do that and you can onboard a new tool’s webhooks into clean Teams cards in the time it used to take to read the tool’s payload docs.

For the surrounding pieces, see the Microsoft Teams category and the monitoring alerts dashboard. The prompt library has adapter-generation prompts to start from, the code-review dashboard helps review the generated adapters, and Claude and ChatGPT both handle the payload-to-code generation well.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.