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

Adaptive Card Templating: Bind Live DevOps Data to One Card

Stop string-concatenating JSON for every alert. Adaptive Card templates let you define a card once and bind live data with a templating language.

  • #microsoft-teams
  • #adaptive-cards
  • #templating
  • #devops
  • #alerting
  • #json

The first version of my deploy-notification bot built Adaptive Cards with string concatenation. I had a function that glued JSON fragments together based on deploy status, and every time we added a field — commit author, rollback link, environment — I touched that function and prayed I didn’t produce malformed JSON. It worked until it didn’t, and debugging a card that “just doesn’t render” in Teams with no error message is a special kind of misery.

Adaptive Card templating is the fix. You define the card layout once, as a static template with binding expressions, then merge it with a data object at send time. The layout and the data are separate concerns, which is exactly how it should have been from the start.

The two pieces: template and data

A template is normal Adaptive Card JSON with ${...} binding expressions where values go. The data is a plain object. The Adaptive Cards Templating SDK merges them.

Here’s a deploy-status card template:

{
  "type": "AdaptiveCard",
  "version": "1.5",
  "body": [
    {
      "type": "TextBlock",
      "text": "Deploy ${status}: ${service}",
      "weight": "Bolder",
      "size": "Large",
      "color": "${if(status == 'succeeded', 'good', 'attention')}"
    },
    {
      "type": "FactSet",
      "facts": [
        { "title": "Environment", "value": "${environment}" },
        { "title": "Version", "value": "${version}" },
        { "title": "Author", "value": "${author}" },
        { "title": "Duration", "value": "${duration}" }
      ]
    }
  ]
}

And the data:

{
  "status": "succeeded",
  "service": "checkout-api",
  "environment": "production",
  "version": "v2.14.0",
  "author": "jjoyner",
  "duration": "3m12s"
}

Notice the ${if(...)} expression on color. That’s the templating language doing conditional styling — green for success, red for failure — without a line of imperative code in my bot.

Binding it in code

The SDK is adaptivecards-templating:

import * as ACData from "adaptivecards-templating";
import deployTemplate from "./cards/deploy.json";

function renderDeployCard(data) {
  const template = new ACData.Template(deployTemplate);
  return template.expand({ $root: data });
}

expand returns the final card JSON, ready to drop into a Bot Framework Attachment with the application/vnd.microsoft.card.adaptive content type. The template file is now a static asset I can lint, diff in PRs, and preview in the designer.

Iterating over collections

Templating earns its keep when you have a list. Say you’re posting a card that summarizes failing health checks. You don’t know how many there are. The $data keyword repeats a container for each array element:

{
  "type": "Container",
  "$data": "${checks}",
  "items": [
    {
      "type": "ColumnSet",
      "columns": [
        { "type": "Column", "width": "auto",
          "items": [{ "type": "TextBlock",
            "text": "${if(healthy, '✅', '❌')}" }] },
        { "type": "Column", "width": "stretch",
          "items": [{ "type": "TextBlock", "text": "${name}" }] }
      ]
    }
  ]
}

Feed it { "checks": [ {name, healthy}, ... ] } and it renders one row per check. Inside a $data scope, the bindings refer to the current element. To reach back up to the top-level data, use ${$root.someField}.

Use the designer, not your imagination

The single highest-leverage tool here is the Adaptive Card Designer at adaptivecards.io/designer. Paste your template, paste sample data into the “Sample Data Editor,” and it renders live with the host config for Teams selected. You see exactly what your engineers will see, including dark mode. I keep the designer open in a browser tab while iterating on any card — guessing at layout and round-tripping through a real Teams send is the slow path.

Patterns that have saved me

  • One template per message type, checked into the repo. Treat cards as code. They get reviewed, they get versioned, and a malformed card is caught in a unit test that renders the template with fixture data.
  • Keep logic in the template, data dumb. Resist the urge to pre-format strings in your bot. Let ${formatDateTime(timestamp, 'HH:mm')} and ${if(...)} live in the template so the layout owns presentation.
  • Provide defaults for missing fields. A binding to a missing property renders as empty, which can leave a stray label. Either guard with ${if(field, field, 'n/a')} or shape your data so optional fields are always present.
  • Version your cards. Set version to the minimum you actually need. Teams clients render newer schema features as fallback if the client is old, and 1.5 features failing silently on an old desktop client is a real support ticket.

Where this fits

The shift from “build JSON in code” to “merge data into a template” is the same shift as moving from printf HTML to a templating engine. Once you’ve made it, every new card is a data shape and a layout file, both reviewable, both diffable. Your alert routing code goes back to doing one job: deciding what data to send, not how to draw it.

For more on getting cards in front of engineers effectively, browse the Microsoft Teams category, and check the prompt library for prompts that generate card templates from a field list.

Adaptive Card schema versions and Teams client support evolve. Always preview templates in the designer with the Teams host config before shipping.

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.