Microsoft Graph Change Notifications for Event-Driven Teams Automation
Stop polling Graph on a timer. Change notifications push events to your webhook when channels, messages, and teams change — here's how to wire them safely.
- #microsoft-teams
- #microsoft-graph
- #webhooks
- #event-driven
- #devops
- #automation
My first Teams automation polled the Graph API every 60 seconds to see if anyone had posted in an incident channel. It worked, it cost me throttling headaches, and it was always either too slow (a minute of lag) or too aggressive (hammering Graph for nothing 99% of the time). Polling is the wrong shape for “tell me when something happens.”
Microsoft Graph change notifications are the right shape. You create a subscription that says “POST to my webhook whenever messages in this channel change,” and Graph pushes you the event. This is the backbone of event-driven Teams automation, and it has enough sharp edges that it’s worth walking through carefully.
The subscription model
You POST /subscriptions with a resource to watch, the change types you care about, a notification URL, and an expiration. Graph validates your endpoint, then starts delivering.
POST https://graph.microsoft.com/v1.0/subscriptions
{
"changeType": "created",
"notificationUrl": "https://ops.example.com/api/graph-notifications",
"resource": "/teams/{team-id}/channels/{channel-id}/messages",
"expirationDateTime": "2026-06-15T18:00:00Z",
"clientState": "a-secret-you-generated"
}
A few things every newcomer gets wrong:
- Expirations are short and resource-dependent. Channel message subscriptions max out around an hour. You must run a renewal loop that PATCHes the subscription before it expires, or your automation silently goes dark.
clientStateis your authenticity check. Graph echoes it back in every notification. If the value doesn’t match what you set, drop the request — it’s not from Graph.- Teams message resources require Microsoft 365 licensing / sometimes payment. Subscribing to chat and channel messages can require a billing model (
includeResourceData-style pay-per-notification) for some resources. Check the current licensing notes before you build on it.
The validation handshake
When you create the subscription, Graph immediately calls your notificationUrl with a validationToken query parameter. You must echo it back as text/plain within 10 seconds or the subscription creation fails:
app.post("/api/graph-notifications", (req, res) => {
if (req.query.validationToken) {
return res
.status(200)
.type("text/plain")
.send(req.query.validationToken);
}
// ... real notification handling below
});
This trips everyone up once. If your subscription create returns a timeout error, it’s almost always that your endpoint isn’t reachable or isn’t echoing the token fast enough.
Handling the notification
Real notifications arrive as a batch with a value array. Validate clientState, acknowledge fast with a 202, and process asynchronously:
app.post("/api/graph-notifications", async (req, res) => {
if (req.query.validationToken) { /* handshake above */ }
for (const n of req.body.value) {
if (n.clientState !== process.env.GRAPH_CLIENT_STATE) {
return res.sendStatus(401);
}
}
res.sendStatus(202); // ack immediately
for (const n of req.body.value) {
await queue.enqueue(n.resource); // process out of band
}
});
The acknowledge-then-process pattern matters. Graph expects a fast 2xx. If you do heavy work inline and respond slowly, Graph retries with backoff and eventually drops you. Treat the webhook like an SNS endpoint: validate, ack, queue.
Thin notifications vs. resource data
By default notifications are thin — they tell you a message changed and give you its ID, not its content. You then GET the resource to read it. That’s an extra round trip but avoids you needing rich permissions on the push payload.
You can request includeResourceData: true to get the content inline, but then you must provide an encryption certificate and decrypt the payload, because Graph won’t push message bodies in the clear. For a DevOps automation that reacts to “someone posted in the incident channel,” I usually take the thin notification and GET the message — simpler, and the latency is fine.
The renewal loop is not optional
This is the failure mode that bites teams in production: the subscription expires at 3am, nobody renews it, and the automation is dead until someone notices alerts stopped flowing. Run a scheduled job:
async function renewSubscriptions() {
const subs = await graph.get("/subscriptions");
for (const sub of subs.value) {
await graph.patch(`/subscriptions/${sub.id}`, {
expirationDateTime: new Date(Date.now() + 55 * 60_000).toISOString()
});
}
}
// run every ~30 min, well inside the expiry window
Renew at half the lifetime, not at the last second — a renewal that fails needs a retry before the real expiry. And alert yourself if a subscription disappears entirely; lost subscriptions don’t error, they just stop delivering.
What to build on it
Once you have reliable change notifications, the event-driven patterns open up:
- Auto-tag or summarize new messages in an incident channel.
- Trigger a runbook when a specific keyword appears in a channel post.
- Sync channel membership changes to an on-call roster.
- Mirror approval decisions posted in Teams into your deployment system.
The shift from polling to push is the same shift as cron-job-to-webhook everywhere else in DevOps: lower latency, lower cost, and a system that reacts instead of checks. Just respect the two operational realities — the validation handshake on create, and the renewal loop forever after.
For related Graph automation patterns, see the Microsoft Teams guides, and the prompt library has prompts for generating subscription and renewal scaffolding.
Graph subscription limits, expirations, and licensing for Teams message resources change over time. Verify current constraints in the Microsoft Graph docs before building.
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.