Policy plans are the fix Conditional Access needs All Articles
Conditional Access · Policy plans deep dive · November 2026

Policy Plans Are the Fix Conditional Access Needs Before GSA and Agents Arrive at Scale

A plan holds the targeting and the break glass exclusion once. I pushed plans to their limits in my own tenant, and the numbers should shape how you name things.

Policy plans · Preview Lab tested · Microsoft MVP · Security: Identity & Access
Section 01

My Take: Plans Fix the Parts of Conditional Access That Break at Scale

Policy plans are the Conditional Access change I am most excited about this year. This section is opinion. The rest of the article is documentation and test results.

Anyone who has run Conditional Access across a large Zero Trust style deployment knows where it hurts. One break glass exclusion missed on one policy out of dozens is a lockout waiting for a bad day. Finding the right policy in a flat list means reading names and trusting that whoever wrote them followed the convention. Both problems have one cause. The policy is the only unit Conditional Access gives you, so every shared decision gets copied into every policy.

A plan moves that shared decision up a level. Exclusions and common targeting live in one place, every rule inherits them, and no rule can remove an exclusion the plan set. The interface shows you the objective first and the scenarios under it. That does not remove the need for a practical, well documented naming convention, and I get to that in section 04. It does clean up the interface, and it gives the Zero Trust policy engine room to grow past what a flat list can hold.

Growth is why this matters now. Microsoft's concept page says Global Secure Access deployments often need hundreds of network-scoped rules for the same users and apps, and it names agent activity as a second use case. My view is that GSA and agents will put a load on Conditional Access that it has never carried. Policy plans are the answer to that exponential growth. Today the documented quota is 240 policies per tenant, in any state. In my testing, plan rules did not count against it. The numbers are in section 05.

The classic policy limit is not 240 in practice

Microsoft documents 240 Conditional Access policies per tenant, in any state. Ali Tajran tested it and found a maximum of 244, in a post published 7 May 2026. His post adds a note that Microsoft raised the cap from 195 to 244 in April 2026. I have not found that change on Microsoft Learn, which still says 240, so treat 240 as the documented figure and 244 as an observed one. Read his post: Conditional Access policy maximum limit.

Preview

Policy plans are in preview. Behavior and limits described here could change before general availability.

Section 02

A Plan Is the Objective, the Shared Conditions Are the Common Ground, and Rules Are the Scenarios

This is how I describe a plan. The plan is the objective, such as requiring compliant devices or securing agent activity. The shared conditions are what every scenario under that objective has in common. The rules are the scenarios. Learn defines the same three parts, and every plan needs at least one rule.

Part 1

Plan details

Name, description, tags and owners. Owners record accountability and grant no permissions. Details do not affect enforcement.

Part 2

Shared conditions

Optional include and exclude targeting that applies to every rule in the plan.

Part 3

Rules

One enforcement scenario each, with its own assignments, conditions, grant controls and session controls.

Four inheritance rules from Learn decide how you design one:

  • A rule inherits the plan's targeting and cannot narrow it.
  • Exclusions accumulate. A rule can add its own, and it can never remove one set at plan level.
  • Identity type must be consistent in shared conditions. A plan that targets users cannot also target agents at plan level.
  • All users and All resources exist at rule level only.

At sign-in, each rule is evaluated as an independent decision, the same way as a standalone policy, and each rule appears separately in the sign-in logs. Plans sit beside your existing policies. Every applicable policy and every applicable rule must be satisfied, with no precedence between them. A plan rule and a classic policy can both enforce on the same sign-in.

Conditional Access Policy plans list showing four plans with owners, rule counts and tags
The Policy plans (Preview) list in my lab tenant. Rule count and tags show per plan.
CATech - Plan - Foundation overview showing a shared breakglass exclusion, client app types and six Report-only rules
CATech - Plan - Foundation. The breakglass group is excluded once in shared conditions and six rules sit beneath it, all in Report-only. The MFA for Admins rule targets 144 roles. The plan sits beside my classic policies.

Tags are the feature I find most interesting

Each plan can carry free-text tags. They show as a column in the list, you set them when you edit the plan, and Graph exposes them as a tags property. Learn describes them as a way to categorize and organize plans. Learn notes that classic policies have no built-in owner attribute, so its guidance is to encode ownership in the policy name and keep a registry outside Entra. Plans put owners on the object itself, with tags beside them. I tagged every test plan in section 05 so they are easy to find and remove afterward.

Section 03

Three Portal Behaviors Learn Does Not Cover Yet

Source: Lab observations, conditionalaccess.tech tenant, 28 October to 4 November 2026 · What are Conditional Access policy plans? (preview)

Guest conditions go in the plan or in the rule, not both

While building a GuestAccess plan I tried to save guest and external user types in the plan's shared identities while the plan's rules also targeted external users. The portal refused with this message:

The condition property 'Users.IncludeGuestsOrExternalUsers' cannot be set
on both shared conditions and rule conditions. Set it on one or the other,
but not both.

The concept page describes inheritance in general terms and does not list this property, so you meet it when you save. Decide per plan where guest targeting lives before you build the rules.

Edit Policy Plan page for CATech - Plan - GuestAccess with a validation error at the bottom about Users.IncludeGuestsOrExternalUsers
The save error in the Edit Policy Plan blade. Tags (GuestAccess) and the breakglass exclusion are visible above it.

Edit all rules changes every state in one step

The plan page has an Edit all rules action. It sets On, Report-only or Off for every rule in the plan at once and asks you to confirm that the update will have significant impact. That is a fast way to promote a whole plan out of Report-only, and just as fast a way to switch protection off. The dialog warns that turning all rules off or to Report-only leaves the organization unprotected. I have not tested whether protected actions cover plan edits, so check that before you rely on it as a control.

Edit all rules dialog with On, Report-only and Off options and an impact acknowledgement checkbox
The Edit all rules dialog on the GuestAccess plan.

I'm adding Policy Plans support to CA Policy Analyzer

I started building this in as soon as plans hit preview. There's now a Policy Plans (Preview) tab that lists each plan with its rules, their states and what they inherit from the plan. It's a read-only early look, and I have not wired plan data into the Findings, Templates or CIS tabs yet. Until I do, treat a clean Findings tab as incomplete if your protection now lives in plans.

CA Policy Analyzer Policy Plans preview tab showing the GuestAccess plan expanded with two Report-only rules
The Policy Plans (Preview) tab with the GuestAccess plan expanded. Rule names keep the policy naming schema.
Section 04

Plans Give You Fewer Things to Name, So Each Name Has to Carry More

Please think about a scalable naming convention before you build your first plan. Plans do not make naming optional. They move it up a level. The plan name is what an admin scans in the list, the rule names are what they read once inside, and the section 05 numbers show you will not have unlimited plans to waste on one-off scenarios.

Here is the structure I am starting with:

[Prefix] - Plan - [Domain] - [Tier]
SegmentWhat it holdsExamples
PrefixWho owns or deploys the planIAC
PlanA fixed literal, so plans are never confused with classic policies in exports or logsPlan
DomainThe product area the rules protectEntra, Intune, Purview, GSA, DFCA
TierThe objective or layer inside that domainFoundation, Baseline, Compliance, InsiderRisk, DLP, Network, SessionControls
Graphic listing eight plan names following the IAC - Plan - Domain - Tier convention
The convention applied across the stack: eight plans out of the 99 a tenant held in my test.

Rule names inside a plan keep your existing policy schema, such as IAC - GLOBAL - GRANT - MFA - B2B-Guest. The plan carries the objective and the rule carries the scenario, so neither name repeats the other.

I would rather scope a plan to a product area and an objective than create one plan per scenario. If a plan fills up, a GSA network plan being the likeliest, the convention extends without breaking. Split on the Tier segment, for example GSA - Network-EMEA and GSA - Network-AMER, and do not invent a new pattern.

A starting point

This is the convention I am testing while plans are in preview. It is not a standard. If yours works better, the point still stands: decide before the first plan exists.

Section 05

Testing the Limits: 100 Rules per Plan, and 99 Plans per Tenant in Practice

The only Conditional Access limit Microsoft documents is 240 policies per tenant, counting every state. I could not find a published maximum for rules per plan or plans per tenant, and the Graph reference for creating rules does not give one. If your design is one rule per network or per branch office, you need the real figure before you build, so I tested both.

LimitWhat I measuredError returned
Rules per plan100. The 101st was refused.2009
Plans per tenant99 created. The 100th was refused, and the message says the limit is 100.2165
Plan rules against the 240-policy quotaSeparate. 64 classic policies plus 206 plan rules is 270 objects, and nothing complained.None

Setup

I ran everything against my own tenant on 4 November 2026, through Lokka (an MCP server for Microsoft Graph), signed in as a Conditional Access Administrator. Plans do not exist in v1.0 yet, so every call went to the beta endpoint. Before touching anything I recorded the starting state:

ObjectCount
Classic Conditional Access policies64
Existing policy plans4
Rules across those plans12

No test object could touch a real sign-in. The sandbox plan, ZZ - LimitProbe - Sandbox, excludes the breakglass group in its shared conditions. Every rule is disabled, targets one demo user and uses block as the grant control.

The calls

A plan cannot exist without a rule, so the plan and its first rule go up in a single POST. The shared conditions carry the exclusion and the client app types. The rule carries the demo user, all applications and the block grant.

Create the plan with Probe-001
POST https://graph.microsoft.com/beta/identity/conditionalAccess/plans
Content-Type: application/json

{
  "displayName": "ZZ - LimitProbe - Sandbox",
  "description": "Temporary sandbox to probe rule limits. All rules disabled. Safe to delete.",
  "tags": ["LimitProbe"],
  "sharedConditions": {
    "clientAppTypes": ["all"],
    "users": { "excludeGroups": ["<break-glass-group-id>"] }
  },
  "rules": [
    {
      "displayName": "Probe-001",
      "state": "disabled",
      "conditions": {
        "users": { "includeUsers": ["<demo-user-id>"] },
        "applications": { "includeApplications": ["All"] }
      },
      "controls": {
        "grantControls": { "operator": "OR", "builtInControls": ["block"] }
      }
    }
  ]
}
ZZ - LimitProbe - Sandbox plan overview with tag LimitProbe, shared exclusion and rules Probe-001 to Probe-007 all Off
The sandbox plan: one tag, one shared exclusion, every rule Off and scoped to a single user.

Every later rule goes to the rules endpoint with the same body and an incrementing name:

POST https://graph.microsoft.com/beta/identity/conditionalAccess/plans/{planId}/rules

One call per rule is slow, so I used Graph's $batch endpoint with 20 POSTs per batch. Two details make it work. Each request carries dependsOn pointing at the one before it, which forces Graph to run them in order (section 05 gotchas explains why that matters). Each also sends Prefer: return=minimal, so Graph answers with a 204 and a Location header instead of echoing the whole rule.

Two requests from a batch
{
  "requests": [
    {
      "id": "1",
      "method": "POST",
      "url": "/identity/conditionalAccess/plans/{planId}/rules",
      "headers": { "Content-Type": "application/json", "Prefer": "return=minimal" },
      "body": { "displayName": "Probe-083", "state": "disabled", "...": "..." }
    },
    {
      "id": "2",
      "dependsOn": ["1"],
      "method": "POST",
      "url": "/identity/conditionalAccess/plans/{planId}/rules",
      "headers": { "Content-Type": "application/json", "Prefer": "return=minimal" },
      "body": { "displayName": "Probe-084", "state": "disabled", "...": "..." }
    }
  ]
}

One batch timed out on the client side partway through. Before retrying I listed the plan's rules and confirmed the count was still 82, so the final number is not inflated by a duplicate batch. Recount before you retry anything that timed out.

Rule 101 is refused with error 2009

Probe-001 through Probe-100 all created cleanly. Probe-101 failed:

{
  "status": 400,
  "body": {
    "error": {
      "code": "BadRequest",
      "message": "The server could not process the request because it is malformed or incorrect.",
      "innerError": {
        "message": "2009: The maximum number of rules per plan has been exceeded. Please remove some rules and try again."
      }
    }
  }
}

Read innerError.message. The top-level code and message are generic, and a "malformed" request reads like a schema problem when the body for rule 101 was identical to the 100 that worked. A script that logs only the top-level error will tell you your JSON is wrong. The request chained after Probe-101 never ran, and Graph returned a 424 FailedDependency for it. The plan now sits at exactly 100 rules.

Sandbox plan rule list ending at Probe-100, all Off
Probe-081 through Probe-100, the last rules that fit.
Policy plans list showing ZZ - LimitProbe - Sandbox with 100 rules and the LimitProbe tag
The plans list with the sandbox at 100 rules, tagged LimitProbe.

Plan 100 is refused with error 2165, and the tenant held 99

For the tenant-wide number I created plans named ZZ - PlanProbe - NNN. Each holds one disabled rule for the demo user and excludes the breakglass group at plan level. The tenant started with 5 plans: my 4 real ones and the rule sandbox. Probes 001 to 094 created cleanly. Probe 095, which would have been the 100th plan, failed:

"innerError": {
  "message": "2165: The number of policy plans has reached the limit of 100."
}

A retry a few seconds later returned the same error. The message says 100, but my tenant held 99. I listed every plan to check: 5 of mine plus 94 probes, no duplicates, no paging, and GET /identity/conditionalAccess/deletedItems/plans came back empty. Either the check is off by one or something Graph does not show counts as a plan. Design for 99.

Policy plans list showing ZZ - PlanProbe - 074 through 094, each with 1 rule and a PlanProbe tag
PlanProbe-074 through PlanProbe-094. Each plan holds one rule and carries the PlanProbe tag.

Plan rules do not count against the 240-policy quota

By the end the tenant held 64 classic policies and 206 plan rules, 270 objects in all. That is past the documented 240 and past the 244 that Ali Tajran observed for classic policies, and nothing complained about the policy count. The two quotas are separate. On paper that allows 240 classic policies plus 99 plans of up to 100 rules each. I did not test whether a tenant-wide rule total exists below that.

Gotchas for anyone scripting against plans

Parallel writes to one plan fail. My first batch sent two rule creations to the same plan with no dependsOn. One succeeded and the other returned a 503:

"code": "ServiceUnavailable",
"message": "Failed to store the policy plan or rule descriptions. Please retry the request."

This is a write collision, not a limit and not throttling. The plan appears to be stored as one object, so concurrent writes fight over it. After I serialized the requests, 98 more creations ran without a single 503.

The rules endpoint accepts no query options. I wanted to count rules without pulling every body back, and each option I tried was refused:

Query optionResponse
$count404, no controller named 'plans' matches the URI
$select=displayName400, 'displayName' cannot be used in $select
$select=id400, 'id' cannot be used in $select
$top=5&$skip=100400, the limit of '0' for Top query has been exceeded
$expand=rules on /plans400, Query option 'Expand' is not allowed

The only way to count rules is a plain GET that returns every rule in full, roughly 2 KB each. At 100 rules that is a large response, and it matters for anyone building inventory, drift detection or backup on top of plans. This differs from the list-rules reference, which lists $count, $filter, $orderby, $select, $skip and $top as supported. In my tenant, the beta endpoint did not honor them.

Plan creation is throttled. Twenty plans in one batch went through. The next batch, sent seconds later, returned a 429 with Retry-After: 1. A retry with 10 plans returned another 429 with Retry-After: 10, and waiting about 75 seconds cleared it. After that, batches of 10 plans with a 30 second gap ran to the limit. I never saw a 429 creating rules, though those batches were spaced further apart.

Check your backup tooling

Admin365 reports that existing Conditional Access backup tools and cmdlets do not see plans yet (Entra Conditional Access Policy Plans Explained). I have not tested this myself. If you move policies into plans, confirm that whatever you rely on for backup and drift detection covers them.

The maximums in short, and why I care

A plan holds 100 rules. A tenant held 99 plans, though the error message says 100. Plan rules sit outside the 240 policy quota for classic policies. None of these are numbers to design toward. I pushed to the edge to find where it is, and a real plan will stop being readable long before it gets there.

Two habits matter more than the numbers. Test in a lab tenant before you trust a preview feature, and keep every test object tagged and disabled so it cannot reach a sign-in. Then name things well from the start. Plans reduce how much there is to name, so each name carries more, and a convention decided before the first plan exists costs far less than one retrofitted later.

I think this matters because of where organizations are heading. Teams getting granular with Zero Trust design principles end up with extensive policy sets. Every new persona, device state, network or agent scenario adds policies, and each one repeats the same exclusions and targeting. Plans give that growth a structure. Shared decisions are made once and scenarios become rules, with an owner on the object. That is how I expect large policy sets to stay understandable as the granularity increases, and it is why I will keep testing them.

Still open

Two Questions I Could Not Settle From Graph

Source: Lab testing, conditionalaccess.tech tenant, 4 November 2026 · What are Conditional Access policy plans? (preview)

The first is why the plan cap bites at 99 when the error message says 100. The second is whether a tenant-wide total of plan rules exists below 99 plans of 100 rules each. Filling every plan to the brim is a lot of writes for a lab, and I stopped short of it.

Learn lists policy plans as preview, so both numbers could change before general availability. I will rerun this test when plans go GA and update the figures here.

Preview disclaimer

Everything in this article is testing of a product in preview. Behavior, limits, error codes and portal wording are subject to change, and my figures reflect one tenant on 4 November 2026. Verify against current Microsoft documentation before you design around them.