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

Updating Cards In Place vs Posting New Messages in Teams Bots

Keep ChatOps channels readable by deciding per interaction whether your Teams bot updates a card in place or posts a new message — with activity-id tracking and audit-trail rules.

  • #microsoft-teams
  • #ai
  • #bot-framework
  • #chatops
  • #ux

Every ChatOps bot eventually gets a complaint that has nothing to do with whether it works: the incident channel is unreadable. The bot is doing its job, posting status as things change, but it’s posting a new card every time, and now there are forty near-identical cards stacked up and the on-call lead can’t find the one that matters. The fix is rarely “post less.” It’s deciding, per interaction, whether to update an existing card in place or post a fresh message. That single decision, made consistently, is the difference between a channel people can read under pressure and a scroll-wall they mute.

Two API calls, two very different effects

Bot Framework gives you both options. sendActivity posts a new message. updateActivity replaces an existing message — identified by its activity id — in place, so the card visually morphs rather than duplicating.

// Post a new card and remember its id
const sent = await context.sendActivity({ attachments: [statusCard(incident)] });
await saveActivityId(incident.id, sent.id);   // keyed by incident, not by user

// Later, update that same card in place
const activityId = await loadActivityId(incident.id);
await context.updateActivity({
  id: activityId,
  attachments: [statusCard(incident)],
});

The capability is simple. The judgment — when to do which — is the whole game.

The heuristic: transient updates in place, audit events post anew

The rule I’ve landed on after enough noisy channels: transient, evolving state updates in place; audit-worthy events post a new message.

A deploy progressing from “queued” to “building” to “deploying” to “live” is one evolving thing. Updating a single card through those states keeps the channel clean and gives a clear current-status view. Nobody needs four messages for one deploy’s lifecycle.

But a decision — an approval granted, a severity escalation, an incident declared — is an event that happened at a point in time, and the channel history should record it as such. Update those in place and you’ve erased the fact that they ever happened. Worse, you’ve erased when they happened, which is exactly what a postmortem or an auditor needs.

A decision table for a typical ops bot:

InteractionUpdate or new?Why
Deploy progress (queued/building/live)Update in placeOne evolving thing; current state is what matters
Alert acknowledgedUpdate in placeTransient state on the alert card
Approval granted/deniedNew messageAudit event; history must show it
Severity escalationNew messagePoint-in-time decision worth retaining
Live metric refreshUpdate in place (debounced)Pure current-state display

Tracking activity ids is the unglamorous prerequisite

In-place updates only work if you can find the card to update. That means persisting the activity id, keyed by your correlation id — the incident or deploy id, not the user — so any later update targets the right card regardless of who triggered it.

Lose that id and you can’t update; you’re forced into posting a new message, which defeats the point. Store it in conversation state alongside the incident record, and treat “we don’t have an activity id for this” as a clean fallback to posting fresh, not an error.

Debounce, or you’ll hit throttling

Update-in-place has a sharp edge: a value that changes rapidly — a progress percentage ticking every second — will hammer updateActivity and run you straight into Bot Framework throttling. Debounce. Pick a sane cadence (every few seconds, or on meaningful state transitions) and update at that rate, not on every tick.

const debounced = debounce((incidentId) => updateCard(incidentId), 3000);
onProgress((incidentId) => debounced(incidentId));   // many ticks, one update per 3s

You also have to handle the case where the target activity was deleted, or is too old to update. When updateActivity fails for those reasons, fall back to posting a new message rather than swallowing the error and silently dropping the update.

The compliance line nobody draws until they have to

Here’s the part that turns this from UX polish into something a regulated org cares about. Updating a card in place erases what it used to say. For a progress bar, that’s fine — nobody audits a progress bar. For an approval card, it’s dangerous: the live “Approved by Bob at 14:32” overwrites the prior “Pending,” and three months later, when someone needs to reconstruct who approved what and when, the history isn’t there.

So the line is: anything that’s compliance- or audit-relevant gets a new immutable message even if a live card also exists. You can keep the tidy live card for at-a-glance status and drop an immutable record into the channel history for the audit trail. They serve different masters, and trying to make one card do both is how you end up with a clean channel and an unprovable approval.

Letting AI build the decision table

This is a great task to draft with an AI assistant, because the mechanical parts (debounce, id tracking, fallback) are well-trodden and the judgment can be encoded as rules the model applies consistently:

Prompt: “For a Teams ChatOps bot, produce an update-in-place vs new-message decision table for these interactions: deploy progress, alert ack, approval granted, severity change, live metric refresh. Apply the rule that transient state updates in place and audit events post a new immutable message. Include activity-id tracking and a debounce strategy.”

Output (excerpt): A decision table matching the heuristic above, an activity-id store keyed by incident id, a 3-second debounce on progress updates, and an explicit note that approvals and severity changes post new immutable messages for the audit trail.

The model applies the rule consistently across the interaction list, which is genuinely useful — but I verify the audit classification myself, since whether a given event is “audit-relevant” depends on your compliance obligations, and that’s a human call the model shouldn’t make for you.

The payoff

Update transient state in place, post audit events as new immutable messages, key your activity ids by correlation id, debounce rapid updates, and fall back to a new message when an update can’t land. Do that and your incident channels stay readable for humans and defensible for auditors at the same time. For the surrounding ChatOps card patterns, the Microsoft Teams category has the related guides, and the prompts library includes an update-vs-new strategy prompt you can adapt to your own bot’s interactions instead of deciding case by case in the heat of an incident.

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.