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

Conditional and Localized Content in Teams Adaptive Cards

One card, many audiences. Use toggleVisibility, $when templating, and host config to show the right content per role and language without building five cards.

  • #microsoft-teams
  • #adaptive-cards
  • #templating
  • #localization
  • #json

We run incident cards into channels that span three regions and two roles — engineers who need the runbook link and stakeholders who need the plain-English status. For a while I maintained separate cards for each combination, which meant a one-line copy change turned into editing four nearly-identical JSON files. Adaptive Cards have two features that kill this duplication: $when conditional rendering in templating, and toggleVisibility for show/hide interactions. Add a localization pass and one card template serves everyone.

This guide covers conditional content via templating, interactive show/hide, and a practical approach to localizing card text. I lean on an AI assistant to generate the $when expressions and the localized string tables, because translating “show the runbook only to engineers” into a binding expression is exactly its sweet spot. But the model will write a $when that references a field that does not exist in your data and produce a card that renders empty with no error. Treat AI as a fast junior engineer: it drafts the conditionals, you verify them against the real data shape, and no actual tenant content goes into the prompt.

Conditional rendering with $when

In Adaptive Card templating, any element can carry a $when property — a binding expression that evaluates against the data. If it is falsy, the element is dropped from the rendered card entirely. This is how you show the runbook link only to the engineering audience:

{
  "type": "ActionSet",
  "$when": "${audience == 'engineering'}",
  "actions": [
    {
      "type": "Action.OpenUrl",
      "title": "Open runbook",
      "url": "${runbookUrl}"
    }
  ]
}

When you merge this template with { "audience": "stakeholder", ... }, the entire ActionSet vanishes. One template, two rendered outputs depending on the data you bind. You can $when individual TextBlocks, whole Containers, or actions — anywhere granularity is needed.

The trap, and the thing to review in AI-generated cards, is referencing a field the data does not contain. ${audience == 'engineering'} where the data has no audience key evaluates falsy, the element disappears, and you debug an “empty card” with no error. Always confirm the $when fields exist in the data object you bind.

Interactive show/hide with toggleVisibility

$when decides visibility at render time based on data. For visibility the user controls — “show details” expanders, collapsible sections — use Action.ToggleVisibility. Give the target elements an id and a default isVisible: false, then toggle them:

{
  "type": "Container",
  "id": "details",
  "isVisible": false,
  "items": [
    { "type": "TextBlock", "text": "${stackTrace}", "wrap": true, "fontType": "Monospace" }
  ]
},
{
  "type": "ActionSet",
  "actions": [
    {
      "type": "Action.ToggleVisibility",
      "title": "Show details",
      "targetElements": ["details"]
    }
  ]
}

This keeps the card compact by default — critical given the Teams card size limit — while letting anyone who wants the stack trace expand it. No round-trip to your bot, no refetch; it is pure client-side toggling.

Pro Tip: targetElements can take objects instead of bare ID strings, each with an explicit isVisible, so one button can show one section while hiding another. That is how you build a tab-like toggle (show “summary” / hide “timeline” and vice versa) inside a single card with no server involvement.

Localizing card text

Adaptive Cards have no built-in i18n, so localization is a templating concern: pick the right string table before you bind. Keep your translations keyed by locale and field, resolve the user’s locale, then build the data object with the localized strings.

const strings = {
  "en-us": { statusLabel: "Status", openRunbook: "Open runbook" },
  "de-de": { statusLabel: "Status", openRunbook: "Runbook öffnen" },
  "ja-jp": { statusLabel: "ステータス", openRunbook: "Runbook を開く" },
};

function buildCardData(incident, locale) {
  const t = strings[locale] || strings["en-us"];
  return { ...incident, ...t }; // labels come from the table, data from the incident
}

The card template references ${statusLabel} and ${openRunbook} instead of hardcoded English, and the localized table fills them in. For the user’s locale in a bot, you can read it from the Teams activity (activity.locale) or from Graph user preferences. Note that values like dates and numbers also vary by locale — Adaptive Cards has {{DATE(...)}} and {{NUMBER(...)}} functions that format according to the viewer’s client locale automatically, which saves you from formatting them server-side:

{ "type": "TextBlock", "text": "Detected {{DATE(${detectedIso}, SHORT)}}" }

That renders the timestamp in each viewer’s local format from a single ISO string — exactly what you want for a card seen across time zones.

Keeping it to one template

The whole point is to stop maintaining N near-identical cards. One template with $when conditionals plus a localized string table collapses “engineer/stakeholder × English/German/Japanese” — six cards — into one template and a small data layer. When the design changes, you edit it once. The generation of the $when expressions and the string-table scaffolding is where I let Claude or Gemma do the repetitive work, then verify the binding fields against my real data shape.

Where AI helps

Drafting conditional expressions and translation tables is genuinely faster with AI, and the starter prompts in my prompts library include a conditional-card skeleton I iterate on in the prompt workspace. The reviews I never skip: that every $when field exists in the bound data, that toggleVisibility targets resolve to real IDs, and that translations were checked by a human who reads the language — machine translation in a stakeholder-facing incident card is a credibility risk. And no real incident content goes into the prompt; synthetic examples are enough to build the structure.

Conclusion

One Adaptive Card template can serve many roles and languages: $when drops content per data, toggleVisibility gives users client-side show/hide, and a locale-keyed string table plus {{DATE}}/{{NUMBER}} handles localization. Let AI draft the conditionals and tables while you verify the data fields, have humans check translations, and keep tenant content out of prompts. More Adaptive Card patterns live in the Microsoft Teams category.

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.