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

Building Teams Message Extensions for DevOps Self-Service

Message extensions let engineers query deploys, search runbooks, and file tickets without leaving the Teams compose box. Here's how to build ones people use.

  • #microsoft-teams
  • #message-extensions
  • #chatops
  • #self-service
  • #bot-framework
  • #devops

Most ChatOps bots make people leave the conversation. You type /deploy status, the bot dumps a wall of text, and the thread you were actually working in is now buried. Message extensions solve a different, narrower problem: they live inside the Teams compose box, so an engineer can search for a deploy, a runbook, or a ticket and drop a rich card into the conversation without breaking flow. After a few years of running ChatOps for platform teams, this is the surface I reach for when I want adoption rather than novelty.

Two kinds of message extension, and when each fits

Teams gives you two flavors, and picking the wrong one is the most common mistake I see.

  • Search-based (query) — the user types a keyword, your service returns a list of results, and they pick one to insert. This is what you want for “find me the runbook for X” or “search recent deploys.”
  • Action-based (fetchTask / submitAction) — the user opens a form (an adaptive card task module), fills it in, and your service does something. This is what you want for “open an incident” or “request a deploy approval.”

You declare both in the app manifest under composeExtensions. A search command looks like this:

{
  "composeExtensions": [
    {
      "botId": "${{BOT_ID}}",
      "commands": [
        {
          "id": "searchDeploys",
          "type": "query",
          "title": "Search deploys",
          "description": "Find a recent deployment by service or environment",
          "initialRun": false,
          "parameters": [
            { "name": "q", "title": "Service", "description": "Service name" }
          ]
        }
      ]
    }
  ]
}

Handling the search query

The Bot Framework SDK surfaces the query through handleTeamsMessagingExtensionQuery. The key thing is that you return a list of cards, not a single answer — the user is choosing, so give them enough to choose well.

async handleTeamsMessagingExtensionQuery(context, query) {
  const term = (query.parameters[0]?.value || "").trim();
  const deploys = await deployStore.search(term, { limit: 8 });

  const attachments = deploys.map((d) => {
    const preview = CardFactory.heroCard(
      `${d.service} → ${d.env}`,
      `${d.status} · ${d.sha.slice(0, 7)} · ${d.when}`
    );
    const full = CardFactory.adaptiveCard(deployCard(d));
    return { ...full, preview };
  });

  return {
    composeExtension: {
      type: "result",
      attachmentLayout: "list",
      attachments,
    },
  };
}

Note the preview on each attachment. That’s the compact row shown in the dropdown; the full adaptive card is what actually gets inserted. Skipping the preview gives you an ugly raw-JSON row, which is the fastest way to make people stop using your extension.

Action commands: forms without a web app

The under-appreciated win is action-based extensions. You can pop a form directly in Teams, collect structured input, and never stand up a frontend. Declare the command with "type": "action" and "fetchTask": true, then return an adaptive card from handleTeamsMessagingExtensionFetchTask:

async handleTeamsMessagingExtensionFetchTask(context, action) {
  return {
    task: {
      type: "continue",
      value: {
        title: "Open incident",
        height: "medium",
        card: CardFactory.adaptiveCard({
          type: "AdaptiveCard",
          version: "1.5",
          body: [
            { type: "Input.Text", id: "summary", label: "Summary", isRequired: true },
            {
              type: "Input.ChoiceSet", id: "sev", label: "Severity",
              choices: [
                { title: "SEV1", value: "1" },
                { title: "SEV2", value: "2" },
                { title: "SEV3", value: "3" },
              ],
            },
          ],
          actions: [{ type: "Action.Submit", title: "Create" }],
        }),
      },
    },
  };
}

On submit, handleTeamsMessagingExtensionSubmitAction receives action.data with the form values. Create the incident in your tracker, then return a result card that gets posted into the channel so everyone sees the new incident with a deep link to it.

Auth: do it once, cache it

Self-service tools touch real systems, so you need the caller’s identity. Message extensions support an OAuth sign-in flow: if your handler returns a type: "auth" response, Teams renders a sign-in prompt, and on completion you get a token you can exchange. Cache that token keyed by the Azure AD object ID so a returning user never sees the prompt twice in a session. I cover the full token-on-behalf-of pattern in the SSO-for-Teams-apps guide; for a search extension that only reads non-sensitive metadata, you can skip auth entirely and ship faster.

Make it discoverable

A message extension nobody can find is dead. Three things move the needle:

  • Good description text in the manifest — it’s the hover tooltip and the only documentation most people read.
  • Pin the app via an app setup policy so the icon is always in the compose toolbar, not buried under the ... overflow.
  • Seed it — post one yourself in a busy channel so people see the card and ask “how’d you do that?”

Where this fits

Message extensions are the highest-adoption-per-line-of-code surface in Teams because they meet engineers where they already are: mid-conversation. Start with one read-only search command, prove people use it, then add an action command that writes. If you want prompt templates for generating the adaptive card JSON and the SDK handlers, we keep a set in the prompt library, and there’s more Teams tooling in the Microsoft Teams category.

Examples here are starting points. Validate manifest schema versions and SDK method names against the current Teams platform docs 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.