What changed since last week
19 Oct 2026
What changed in my tenant since last week?
Every Microsoft admin centre shows you the state of your tenant: the devices, the roles, the consents as they are right now. None of them answers the question you actually have on a Monday morning, which is what moved since the last time you looked. Comparing two exports by hand is possible and nobody does it twice.
The GraphPaaS connector for Claude has a tool
for that question, compare_snapshots. It takes two stored syncs of one
domain and returns the rows added, removed and changed. That sounds like a plain diff.
It isn't quite one, and the places where it isn't are where a diff of tenant data
usually goes wrong. Everything below was asked against Fabrikam HQ, our demo tenant
(synthetic people, real tool calls, latest sync 18 October at 05:45 UTC).
"Last week" is a snapshot, not a date
What changed on our devices since last week?
compare_snapshots(domain="devices", tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e",
since="2026-10-11", limit=5)
→ from 2026-09-18 to 2026-10-18 · key device_id
→ 0 added · 0 removed · 1 changed · 35 unchanged
One device moved: FBK-IOS-017 went from compliant to noncompliant. Nothing was enrolled or removed. The comparison starts on 18 September, the newest stored snapshot on or before the day you asked about.
The tool never compares against a date. since and until each
pick the newest stored snapshot on or before that day, and the answer says which two it
picked. The demo keeps eight snapshots of devices between 14 September and
18 October, so "last week" landed on 18 September. A tenant that syncs every day lands
on the day itself. Read the from line before reading the result.
The one change is the kind worth acting on. A device turns noncompliant as soon as it checks in and fails a policy, unless the policy sets a grace period on the Mark device noncompliant action. If Conditional Access requires compliant devices, its owner lost access at that check-in. The admin centre shows the device as noncompliant today. It doesn't show that it was compliant last week.
A row is whatever its id says it is
Before anything can be "changed", the tool has to decide that a row in the old snapshot
and a row in the new one are the same thing. It matches on the id columns the domain
has (device_id, upn, app_id, id and
so on), lower-cased, because a re-cased UPN or GUID is still the same object. Some
domains have no single id, and the tool refuses rather than guess:
Did anything change in governance since last week?
compare_snapshots(domain="governance", tenant_id="ce48e256-…", since="2026-10-11") → 400: 'governance' rows have no id column — pass key with the columns that name a row. compare_snapshots(..., key=["kind", "role", "member_upn"]) → 12 unchanged · warning: 24 shared an id with another row compare_snapshots(..., key=["kind", "role", "member_upn", "ca_name", "app_name", "risky_upn"]) → 0 added · 0 removed · 0 changed · 24 unchanged
No change. All 24 rows (admins, Conditional Access policies, expiring secrets and risky users) match the 18 September snapshot.
The middle call is the one to look at. With a key that only names admins, the Conditional Access and secret rows collide on empty values, and "12 unchanged" would have been a confident answer about half the data. The warning is there so that never passes as a clean result.
The opposite problem is a key that names too much. In licence_waste the id
columns are the object id and the UPN, so a rename doesn't look like a change:
Who joined or left the licensed users since the 14th?
compare_snapshots(domain="licence_waste", tenant_id="ce48e256-…", since="2026-10-14") → 1 added (Liam O'Hara) · 1 removed (Liam Moreira) · 41 changed both rows: azure_object_id d3m0-user-0011 compare_snapshots(..., key=["azure_object_id"], watch=["upn", "display_name", "assigned_products"]) → 0 added · 0 removed · 1 changed · 41 unchanged
Nobody joined or left. The added and removed rows are the same account: Liam Moreira was renamed to Liam O'Hara, with a new UPN and the same licences.
Microsoft's own guidance on UPN changes is to treat the object ID as the immutable identifier, not the UPN or the email address. The diff follows the same rule when you give it the key. Without it, a leaver report built on this would have sent someone to offboard a person who still works there.
The column that changes on every row
Widen the window to the oldest stored day and the same 36 devices tell a different story:
And since mid-September?
compare_snapshots(domain="devices", tenant_id="ce48e256-…", since="2026-09-14", limit=5)
→ 36 changed · 0 unchanged
→ changed_fields: enrolled_at 36, last_sync 36, enrollment_type 21, join_type 12,
grace_period_ends 5, user 1, compliance_state 1
compare_snapshots(..., watch=["compliance_state", "user", "os_version", "encrypted"])
→ 2 changed · 34 unchanged
Two devices changed in a way that matters. FBK-IOS-017 is the same compliance drop as before. FBK-AND-012's user went from liam.moreira to liam.o'hara, the rename again, seen from the device side.
Every device "changed", and almost none of it is news. changed_fields counts
the changed rows per column, and a column that moved on all 36 rows is nearly always a
timestamp. In the demo, the dates move because the synthetic devices are regenerated. In
your tenant it is last_sync, which Microsoft defines on the
managedDevice
resource as the last time the device completed a successful sync with Intune. It
moves whenever the device checks in.
watch is the fix: only the listed columns count as a change. The first call
tells you what to drop; the second asks the real question. The gap between 36 and
2 is what a diff without a watch list buries.
Lists compare as sets, and a log forgets on purpose
Some columns hold a list: the products a user is assigned, the scopes an app was
consented, the methods a user registered. Microsoft stores a delegated consent as
one
space-separated scope string, and a string compare flags a reorder as a
change. The tool splits those columns into items and reports the items
added and removed instead of a before and after. So a new
Mail.Read on a consented app would show up as just that, one scope. It knows
MICROSOFT 365 E3+ENTERPRISE MOBILITY + SECURITY E3 is two products, split on
the bare plus and not on the one inside a product name. In the demo windows above no list moved: with
assigned_products watched, the rename was still the only change.
Log-shaped domains need the opposite caution:
Which sign-ins disappeared since the 14th?
compare_snapshots(domain="signins", tenant_id="ce48e256-…", since="2026-10-14", limit=2) → 0 added · 0 removed · 420 changed · changed_fields: when 420, user 6 → rolling_window: true
None were deleted. Sign-ins are a rolling window: a removed entry aged out of it, it wasn't erased.
Entra keeps sign-in and audit logs for
seven
days on Free and 30 days on P1 or P2, so every sync drops the oldest entries. A diff
that reads "removed" there as "deleted" tells you someone wiped the log. The flag stops
Claude from saying that. (In the demo, the 420 entries are regenerated under the same
ids, so they come back as changed on when. In a real tenant every sign-in
has its own id, and the window moving forward shows up as added and removed.)
- managedDevice resource type — complianceState values, and lastSyncDateTime as the last successful sync with Intune.
- Configure actions for noncompliant devices in Intune — Mark device noncompliant runs at check-in, immediately unless a grace period is set.
- Plan and troubleshoot UserPrincipalName changes — use the object ID as the immutable identifier, not the UPN or email.
- oAuth2PermissionGrant resource type — delegated consents stored as one space-separated scope string.
- Microsoft Entra data retention — sign-in and audit logs kept 7 days on Free, 30 days on P1/P2.
Ask your own tenant what changed since last week, and get the rows, not a feeling.
Add the connector →