Change management
See also: workspaces with
require_approvalenabled split the apply step into a separate review - see Approval workflow. To push an approved change at a future time, see Scheduled changes.
The model
Every CA write follows the same gated workflow:
- Draft - Policytab saves the proposed change without touching Entra (
status=draft). - Preview - Policytab computes a field-level diff against the latest snapshot and shows per-kind safety warnings. Status becomes
dry_run_complete(ordry_run_blocked). An immediate preview is valid for 30 minutes; explicitly scheduled changes use the separate seven-day window. - Approval when required - workspace-gated and critical changes require a different persisted admin. Approval does not write to Entra.
- Apply - Apply now claims the row, captures and links a complete pre-change snapshot, rechecks live divergence and safety, writes through the guarded Edge path, then captures and verifies a post-change snapshot. Status=
appliedonly reflects that the Graph mutation landed;error_messagecan carry a verification warning. - Optional rollback - restores from the linked pre-change evidence only when the linked post evidence and live material state still match. Divergence or unreadable evidence blocks rather than overwrites an out-of-band edit. Status=
rolled_back. - Cancel - from a change detail page while status is
draft,dry_run_complete,dry_run_blocked,awaiting_approval, orfailed, an admin can Cancel change (cancelChange/triggerCancel). Status becomescancelled; no Graph write occurs.
The state machine is enforced at the app and Edge boundaries. You cannot apply without a fresh successful dry-run, the exact in-flight claim, linked rollback evidence, and any required distinct-admin approval. You cannot rollback without an applied state plus verifiable pre/post/live evidence.
Microsoft Graph does not offer an idempotency key for policy, group, or named-location creates. Policytab therefore records a create-attempt claim before its single POST and reconciles only newly observed exact matches. A lost or ambiguous response is held for review; it is never converted into permission to POST the create again.
Change kinds
| Kind | What it changes |
|---|---|
policy.state_change |
enabled / disabled / report-only |
policy.assignment_change |
include/exclude users + groups (add/remove deltas) |
policy.condition_change |
locations, platforms, client app types |
policy.risk_condition_change |
sign-in risk levels, user risk levels |
policy.grant_change |
built-in controls (MFA, compliant device), operator (AND/OR), AuthStrength id |
policy.application_change |
include/exclude applications |
policy.session_change |
sign-in frequency, persistent browser session |
policy.device_filter_change |
device filter rule |
policy.terms_of_use_change |
terms of use reference |
policy.auth_context_change |
authentication context class references |
policy.create |
new policy from baseline or copy |
policy.delete |
soft-delete (recoverable) |
policy.restore |
restore a soft-deleted policy from Entra recycle bin |
policy.rename |
display name change |
location.create / update / delete |
named location CRUD |
For most kinds, the Change payload builder generates the request. Advanced operators can use the JSON editor for supported change shapes.
Restoring deleted policies
Soft-deleted policies appear under Policies → Deleted. Restore creates a policy.restore change request and runs it through the same dry-run → approve → apply path as other writes (via graph-policies-deleted, not a direct Graph bypass). Bulk change does not include policy.delete or policy.restore.
What dry-run will warn you about
| Code | Severity | Triggered by |
|---|---|---|
policy_not_found |
critical | Policy id not in the most recent snapshot - resync first |
no_op |
info | The proposed state matches current |
critical_policy_disable |
critical | Disabling a policy with metadata.criticality = 'Critical' |
reportonly_to_enforced |
warning | Flipping report-only → enforced (run Impact first) |
critical_assignment_empty |
critical | Critical policy's assignment would scope it to nobody |
large_exclude_add |
warning | Adding >10 user exclusions in one change |
exclude_without_include |
warning | Adding location excludes without an include scope |
grant_remove_mfa |
critical* | Removing MFA from a Critical policy's grant controls (*warning otherwise) |
grant_remove_compliant_device |
warning | Same shape for compliant-device removal |
grant_loosen_operator |
warning | Changing AND → OR (loosens the policy) |
app_remove_admin_portals |
critical | Removing MicrosoftAdminPortals from include scope - admin bypass risk |
A blocking critical warning keeps apply unavailable until the operator resolves or explicitly overrides the safety gate. Other warnings remain visible for review but do not block apply.
Bulk changes
For applying the same payload across many tenants:
- Bulk tab → New bulk change
- Multi-select tenants (imported only)
- Pick a kind + payload (same as single-tenant)
- Dry-run fans out per tenant. Each tenant gets its own
change_requestrow with its own dry-run result. - If all per-tenant dry-runs pass, you can Approve and apply all. Each tenant's apply takes its own pre/post snapshots.
A partial failure (some apply, some fail) lands in status applied_partial. The detail page shows per-tenant outcomes with errors.
On the bulk detail page, the Sign-in impact (cached) column shows per-tenant would block counts from the Impact analysis cache (7-day window by default) when you've already run Impact for that policy on a tenant; otherwise use Run impact to populate the cache. This reads stored sign-in summaries-it does not replay Graph during bulk dry-run, and it is only available for policy document changes (not every bulk kind).