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 -
change_requestrow created with the proposed payload, status=draft - Dry-run - Policytab computes a field-level diff against the latest snapshot + per-kind warnings (e.g. "removing MFA from a Critical-marked policy"). Status=
dry_run_complete(ordry_run_blockedif critical warnings). Result is valid for 30 minutes. - Approve + apply - same admin (or another admin/owner when
require_approvalis on) clicks Approve. Policytab takes a pre-change snapshot, applies via Graph, takes a post-change snapshot. Status=applied. - Optional rollback - re-applies the pre-change snapshot's policy shape. 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 server-side. You can't apply without a fresh dry-run; you can't rollback without an applied state with snapshots.
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 UI (components/app/change-payload-builder/) generates JSON; advanced kinds can still be pasted manually following shapes in lib/changes/dry-run.ts.
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 critical-severity warning blocks apply (you can't click Approve). Any other warning is informational - apply is allowed.
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).