Every Entra tenant eventually grows a graveyard of stale groups. Someone created All-Sales-Users two years ago, added forty people by hand, and then half of them changed departments, three left the company, and a dozen new hires never got added. The group still feeds a licence assignment and a Conditional Access policy, so the drift is now a security and a billing problem at once. The fix is not “remember to update the group” — humans never do — it is to stop maintaining membership by hand at all. Microsoft Entra ID dynamic membership groups let you write a rule once (user.department -eq "Sales") and let the directory keep the membership correct forever: as people join, move and leave, Entra adds and removes them automatically.
This article is a cookbook for those rules. We treat a dynamic group as what it really is — a small saved query that Entra re-evaluates whenever a member object changes — and we work through the rule syntax, the operators, the user and device properties you are allowed to filter on, and the gotchas that bite first-timers (the licence requirement, the “rules don’t recompute instantly” surprise, the difference between a dynamic user group and a dynamic device group, and why you can never add a member by hand once a group is dynamic). The centrepiece is a full hands-on lab: you will build a real dynamic group three ways — in the Azure portal, with the az CLI (via the Microsoft Graph API), and as Bicep — validate that the right users land in it, then tear it all down.
By the end you will reach for a dynamic group the moment you catch yourself adding the second user to a list by hand. You will know exactly which properties are filterable, which operators to use, how to read a rule like a sentence, and how to debug the classic “I created the group but it’s empty” panic. These groups quietly power licence assignment, app role assignment and Conditional Access targeting across real tenants — get the rules right and a whole class of “someone forgot to update the group” incidents simply stops happening.
What problem this solves
Manual group membership does not scale and does not stay correct. The moment a group’s membership is a human responsibility, it drifts: joiners are forgotten, leavers linger, movers keep access they should have lost. In a small tenant that is annoying. In a tenant where groups drive licensing (a user in M365-E5-Licensed gets a ~$57/user/month licence), app access (a group assigned to an enterprise app), or Conditional Access (a group exempted from MFA, or required to be compliant), drift is a direct cost and security exposure. A leaver still in the licensed group is paying money; a mover still in an “exempt from MFA” group is a hole.
Dynamic groups move that responsibility from a person to the directory. You declare the intent — “everyone in the Sales department,” “every Windows device that is company-owned,” “all guests from partner.com” — as a membership rule, and Entra evaluates it continuously. When HR updates a user’s department in the source-of-truth that syncs to Entra, the user’s group memberships re-shuffle automatically minutes later. No ticket, no runbook, no monthly audit of who should be in which list.
Who hits this: anyone administering an Entra tenant past a few dozen users. It bites hardest where group membership feeds something automated — group-based licensing, Conditional Access, SCIM/app provisioning, or Privileged Identity Management-style scoping — because there the drift is invisible until an audit or a bill surfaces it. The honest prerequisite, which trips up many first-timers, is that dynamic groups require Microsoft Entra ID P1 (or P2) licensing — they are not in the free tier — and that is the very first thing this guide makes you check.
Learning objectives
By the end of this article you can:
- Explain what a dynamic membership group is, how Entra’s rule engine evaluates it, and how it differs from an assigned (manual) group.
- Decide correctly between a dynamic user group and a dynamic device group — and explain why a single group can never mix both.
- Write membership rules from scratch using the correct operators (
-eq,-contains,-match,-startsWith,-in,-any/-all) and the filterable user and device properties. - Build the same dynamic group three ways — Azure portal,
azCLI against Microsoft Graph, and Bicep — and validate membership end to end. - Use the portal’s “Validate rules” tool and
azmembership queries to confirm a rule does what you intended before it goes live. - Diagnose the classic failures: an empty group, a P1 licence error, a rule that won’t save, slow membership processing, and “I can’t add a user manually.”
- Apply the right design and security practices so dynamic groups safely drive licensing, app assignment and Conditional Access without becoming a new source of risk.
Prerequisites & where this fits
You should already understand the Entra basics: a tenant is your directory; users and groups are objects in it; a security group controls access while a Microsoft 365 group also brings a mailbox and Teams. You should be comfortable in the Azure portal and have run az login at least once. Knowing what Conditional Access and group-based licensing are helps, because they are the main consumers of these groups, but you do not need to be an expert in either.
This sits in the Identity & Governance track. Groups are the unit of assignment that almost everything else in Entra targets, so getting membership to maintain itself is foundational. It connects upward to governance — the same directory data that drives Azure Policy and RBAC scoping — and outward to anything that consumes a group: licensing, enterprise apps, and access reviews. A quick map of who needs what before you start:
| Prerequisite | Why you need it | How to check |
|---|---|---|
| Entra ID P1 or P2 licence | Dynamic membership is a P1+ feature; rules won’t save without it | Entra admin centre → Overview → licence; or check assigned plans |
| Role: Groups Administrator (or higher) | To create groups and edit membership rules | Entra → Roles and administrators → your assignments |
az CLI 2.50+ |
To run the CLI lab against Microsoft Graph | az version |
| A few users with a populated attribute | So a rule has something to match (e.g. department) |
Entra → Users → check the property is set |
| Tenant ID handy | For CLI and Bicep deployments | az account show --query tenantId -o tsv |
If your account is only Global Reader you can look but not create; you need at least Groups Administrator to do the lab.
Core concepts
Five mental models make every rule you ever write obvious.
A dynamic group is a saved query, not a list. An assigned (manual) group stores an explicit list of member object IDs that you maintain. A dynamic group stores a membership rule — a boolean expression over object properties — and stores no member list of its own. Entra evaluates the rule and derives the membership. You never see or edit the member list directly; you edit the rule, and the list follows. This is the single most important shift: you stop thinking “who is in this group?” and start thinking “what is true of the people who should be in this group?”
The engine is event-driven, then it re-derives. Entra re-evaluates a group’s rule when a relevant member object changes (a user’s department is updated, a device’s accountEnabled flips) and periodically across the tenant. When a rule changes or a group is first created, Entra runs a full re-evaluation of every object in the directory against it. This is why a brand-new dynamic group is empty for a few minutes then suddenly populates — and why people panic and assume the rule is broken when it is merely still processing. Processing time scales with directory size; in a large tenant a full recompute can take a while.
User groups and device groups are different species. A dynamic group’s rule targets either users or devices, never both — you choose the membership type at creation: Dynamic User or Dynamic Device. A user rule filters on user.* properties; a device rule filters on device.* properties. You cannot write a rule that mixes user.department and device.deviceOSType, and you cannot convert the membership type after creation in a way that changes the species. Pick the right one up front.
Dynamic membership is exclusive — no manual edits. Once a group’s membership type is dynamic, you can no longer add or remove members by hand. The rule is the sole authority. The portal greys out “Add members”; the Graph API rejects manual member writes on a dynamic group. If you find yourself needing to add “just one extra person” who doesn’t match the rule, that is a signal either to fix the rule or to use a different (assigned) group — not to fight the engine.
One direction only: a property the directory knows. A rule can only filter on properties Entra actually stores and that are supported for dynamic rules — a specific allow-list of user and device attributes, plus the extension attributes (extensionAttribute1–15). If the data you want to filter on isn’t in Entra (it lives only in HR or an app), the rule has nothing to match; you must first get that data into Entra (via sync or an extension attribute) before a rule can use it.
The vocabulary in one table
Pin down every moving part before the deep sections. The glossary at the end repeats these for lookup; this is the mental model side by side:
| Term | One-line definition | Where it lives | Why it matters |
|---|---|---|---|
| Membership type | Assigned, Dynamic User, or Dynamic Device | Group property (set at creation) | Determines whether you maintain the list or a rule does |
| Membership rule | The boolean expression deriving membership | Group property membershipRule |
The whole point — the saved query |
| Processing state | Whether the rule has finished evaluating | Group property membershipRuleProcessingState |
Explains the “empty for a few minutes” gap |
| Filterable property | A user/device attribute usable in a rule | Directory schema (allow-listed) | You can only filter on supported attributes |
| Extension attribute | extensionAttribute1–15, free-text custom fields |
On the user/device object | The escape hatch for data Entra doesn’t model natively |
| Operator | -eq, -contains, -match, -in, -any… |
Inside the rule | How you compare a property to a value |
| Group-based licensing | Assigning licences to a group | A consumer of the group | A common reason dynamic membership matters for cost |
| Conditional Access | Policies that target groups | A consumer of the group | A common reason correctness matters for security |
How the rule engine works
A membership rule is a single boolean expression. Entra reads it left to right, evaluates each clause against a candidate object, and the object is a member if and only if the whole expression is true. The shape is always the same:
(property operator value) [and|or [not] (property operator value)] ...
For example, (user.department -eq "Sales") and (user.accountEnabled -eq true) puts every enabled user in Sales into the group, and removes anyone who is disabled or leaves Sales. You build complexity by combining clauses with -and, -or and -not, grouping with parentheses exactly as in any programming language.
The portal gives you a rule builder (dropdowns for property / operator / value) for simple rules and a rule syntax text box for anything beyond a couple of clauses. The builder is convenient but limited — nested logic, the -match regex operator, and the multi-value -any/-all operators generally need the text box. Learn the text syntax; the builder is training wheels.
Membership type: user vs device
The first and most consequential choice. It cannot be meaningfully changed later, so get it right:
| Membership type | Rule prefix | Filters on | Typical use | Cannot do |
|---|---|---|---|---|
| Assigned | (none) | n/a — manual list | Small, hand-curated groups; break-glass | Auto-maintain |
| Dynamic User | user. |
User attributes (department, jobTitle, country, userType…) | Licensing, Conditional Access, app assignment by role/org | Reference device.* |
| Dynamic Device | device. |
Device attributes (OS, ownership, compliance, model…) | Intune/Conditional Access device targeting | Reference user.* |
A rule must be entirely one species. (user.department -eq "IT") or (device.deviceOSType -eq "Windows") is invalid and the portal rejects it. If you genuinely need “users in IT and their Windows devices,” that is two groups, not one.
Operators: the full set you actually use
These are the operators supported in membership rules. Pick by the shape of the comparison:
| Operator | Meaning | Example | Notes / gotcha |
|---|---|---|---|
-eq / -ne |
Equals / not equals | user.department -eq "Sales" |
String compares are case-insensitive; quote string values |
-startsWith / -notStartsWith |
Prefix match | user.userPrincipalName -startsWith "svc-" |
Great for naming conventions (service accounts) |
-contains / -notContains |
Substring / collection contains | user.proxyAddresses -contains "smtp:sales@" |
On multi-value props it tests membership |
-match / -notMatch |
Regular-expression match | user.displayName -match "(?i)contractor" |
Full regex; the most powerful and most misused |
-in / -notIn |
Value is in a list | user.country -in ["IN","US","UK"] |
List literal in square brackets, comma-separated |
-any / -all |
Quantifier over a multi-value property | user.assignedPlans -any (assignedPlan.service -eq "exchange") |
For collections of complex objects |
-and / -or / -not |
Boolean combinators | (A) -and -not (B) |
Parenthesise freely to control precedence |
Two rules of thumb that prevent most syntax errors. First, string values are always quoted ("Sales", "IN"), but booleans and the null keyword are not (-eq true, -ne null). Second, when in doubt about a multi-value property, reach for -contains or -any rather than -eq — comparing a collection with -eq rarely does what you expect.
Filterable properties: what you can match on
You can only filter on attributes Entra stores and supports for dynamic rules. The commonly used ones:
| Property (user) | Type | Example value | Typical rule use |
|---|---|---|---|
user.department |
string | "Sales" |
Org-based grouping for licensing/CA |
user.jobTitle |
string | "Account Manager" |
Role-based app assignment |
user.country |
string | "IN" |
Region/data-residency targeting |
user.usageLocation |
string | "IN" |
Required before assigning many licences |
user.accountEnabled |
boolean | true |
Exclude disabled accounts |
user.userType |
string | "Guest" / "Member" |
Separate guests for Conditional Access |
user.userPrincipalName |
string | "svc-…@contoso.com" |
Naming-convention matches |
user.companyName |
string | "Contoso" |
Multi-company tenants |
user.extensionAttribute1–15 |
string | "Contractor" |
The escape hatch for custom data |
For device groups, the equivalents are device.deviceOSType, device.deviceOSVersion, device.deviceOwnership ("Company"/"Personal"), device.isCompliant, device.deviceManagementAppId, device.deviceModel, and the device extensionAttribute* set. A device rule that is extremely common in practice: (device.deviceOwnership -eq "Company") and (device.deviceOSType -eq "Windows").
If the attribute you want isn’t in this allow-list, two options: write the value into an extension attribute (free text, syncs from on-prem AD or set via Graph), or reconsider whether the grouping belongs in Entra at all. Never assume an arbitrary HR field is filterable — confirm it first.
Reading and writing a rule like a sentence
The skill is to translate intent into the expression and back. A few worked examples, from simple to layered:
| Intent | Rule |
|---|---|
| All enabled users in Sales | (user.department -eq "Sales") and (user.accountEnabled -eq true) |
| All guests | (user.userType -eq "Guest") |
| Service accounts by naming convention | (user.userPrincipalName -startsWith "svc-") |
| Users in India or the US | (user.country -in ["IN","US"]) |
| Everyone except contractors (extension attr) | (user.objectId -ne null) and -not (user.extensionAttribute1 -eq "Contractor") |
| Company-owned Windows devices | (device.deviceOwnership -eq "Company") and (device.deviceOSType -eq "Windows") |
That last user example also shows the idiom for an “all users” group: (user.objectId -ne null) is true for every user, so you can use it as a base and subtract exceptions with -and -not (...). Microsoft provides a managed “All users” dynamic group concept, but a hand-rolled objectId -ne null rule is the explicit, portable equivalent.
Where dynamic groups get consumed
A dynamic group is only useful because something targets it. Knowing the consumers shapes how careful you must be with the rule — a sloppy rule on a group nobody uses is harmless; the same rule on a Conditional Access group is an outage or a breach.
| Consumer | What it does with the group | Why correctness matters | Failure mode if the rule is wrong |
|---|---|---|---|
| Group-based licensing | Assigns licences to all members | Each member may cost real money/month | Over-licensing (cost) or under-licensing (broken apps) |
| Conditional Access | Includes/excludes the group in a policy | Controls MFA, device compliance, location rules | Lock everyone out, or exempt people who shouldn’t be |
| Enterprise app / SCIM | Grants app access or provisions accounts | Drives who can sign in and what is created downstream | Unauthorised access or orphaned downstream accounts |
| Access reviews / governance | Periodically recertifies membership | Audit and least-privilege | Wrong population reviewed; findings invalid |
| PIM / role-assignable | Members get eligible/active role | Privileged access | Privilege creep if the rule is too broad |
One hard constraint worth knowing before you design: a role-assignable group (one you can assign an Entra directory role to) cannot be a dynamic group. Role-assignable groups must be assigned membership, by design, so a rule can never silently grant a privileged role. If you need privileged access governed, use PIM and assigned groups, not a dynamic rule.
Architecture at a glance
Hold the mental model as a one-way pipeline rather than a picture. On the left sits your source of truth for identity attributes — typically an HR system feeding on-prem Active Directory, which Entra Connect synchronises into Entra ID, or attributes set directly in Entra/Graph for cloud-only objects. Those attributes (department, jobTitle, country, extensionAttribute*) land on each user object in the directory. That is the only data a rule can ever see: if a value isn’t on the object in Entra, no rule can match it.
In the centre sits the dynamic group — a small stored membershipRule plus a membershipRuleProcessingState, but no stored member list. Whenever a user object changes, or on a periodic sweep, the rule-evaluation engine runs the candidate object through the rule; objects that evaluate true are derived as members, and the membership set is recomputed. There is a deliberate lag here: a change to a user attribute does not instantly reshuffle every group; the engine processes it within minutes (longer in very large tenants), which is exactly why a freshly created group looks empty before it fills.
On the right sit the consumers: group-based licensing, Conditional Access, enterprise-app assignment, access reviews. They read the derived membership and act on it. The whole value of the architecture is that the arrow runs strictly left to right and is automatic — fix department once at the source, and minutes later the correct licences, access and policies follow without anyone touching the group. The two things that break this picture are (1) the data never reaching Entra (so the rule can’t see it) and (2) forgetting the processing lag (so you judge the rule before it has run). Everything in the lab below is about making both of those visible.
Real-world scenario
Lumio Retail, a mid-size e-commerce company (about 1,200 employees across India, the UK and the UAE), ran group-based licensing off a hand-maintained security group called Licensed-E5-Users. IT added each new hire manually during onboarding. Over eighteen months the group accumulated drift: 1,247 members against a real headcount of 1,180 — 67 ghost members, most of them leavers whose accounts were disabled but never removed from the group. At roughly $57/user/month for the E5 licence, the company was paying for dozens of licences assigned to disabled accounts, and a quarterly licence true-up flagged the gap as a ~$45,000/year leak.
Worse, the same group was included in a Conditional Access policy that required compliant devices. Three contractors who had left were still members; their (now unmanaged) accounts technically still satisfied a policy include they should have aged out of. Nothing had been exploited, but the auditors marked it a finding: membership of a security-critical group was a manual process with demonstrable drift.
The platform team replaced the manual group with a dynamic user group whose rule was (user.accountEnabled -eq true) and (user.assignedPlans -any (assignedPlan.servicePlanId -ne null)) and (user.userType -eq "Member") — refined down, after testing, to a cleaner department-and-status rule that matched their licensing policy exactly. They built it first in a dev tenant with the portal’s “Validate rules” tool, checking five real user objects (an active employee, a disabled leaver, a guest, a contractor, and a brand-new hire) to confirm each landed on the correct side of the rule. Only after the validation matched expectations did they create the group in production via Bicep, so the rule was code-reviewed and version-controlled rather than typed into a portal box at 5 p.m.
The result: every disabled leaver dropped out of the licensed group automatically within minutes of being disabled, the ghost-member count went to zero and stayed there, and the next licence true-up reclaimed 60+ licences. The Conditional Access finding closed because membership was now provably derived from accountEnabled and org attributes, not human memory. The lesson Lumio took away — and the reason this article makes you validate before going live — is that the rule is only as trustworthy as the data behind it and the testing in front of it. A dynamic group is a force multiplier in both directions: correct, it eliminates a whole class of drift; wrong, it propagates a mistake to every consumer instantly.
Advantages and disadvantages
Dynamic groups are not free of trade-offs. Weigh them deliberately:
| Advantages | Disadvantages |
|---|---|
| Membership maintains itself — zero manual joiner/mover/leaver toil | Requires Entra ID P1/P2 — a per-user licence cost |
| Eliminates drift; membership is always derivable from data | A bad rule propagates to every consumer instantly (blast radius) |
| Rule is auditable and version-controllable (as Bicep/Graph) | You cannot add/remove members by hand (no exceptions) |
| Scales to thousands of objects without effort | Processing lag — not instant; “empty for minutes” surprises people |
| One source of truth: fix the attribute, fix the access | Can’t be role-assignable (no dynamic privileged groups) |
| Consistent across licensing, CA, app assignment | Limited to filterable properties; needs data in Entra first |
When the advantages dominate: any group whose membership is a function of data you already have — org, role, location, device posture, account status. That is the sweet spot, and it covers most “all the X” groups. When the disadvantages dominate: small hand-curated lists, break-glass/exception groups, role-assignable privileged groups, and any grouping based on data that simply isn’t (and won’t be) in Entra. The decision is almost always “is this membership a stable function of directory data?” — if yes, make it dynamic; if it needs human judgement per member, keep it assigned.
Hands-on lab
This is the centrepiece. You will build the same dynamic user group three ways — portal, az CLI (against Microsoft Graph), and Bicep — validate that the right users are members, then tear everything down. Budget 30–40 minutes. Everything here is reversible and uses only directory objects (no compute cost), but it does require Entra ID P1/P2 on the tenant.
Lab prerequisites and expected starting state
| Requirement | How to verify | Expected output |
|---|---|---|
| Entra ID P1 or P2 | az rest --method GET --uri "https://graph.microsoft.com/v1.0/subscribedSkus?\$select=skuPartNumber" --query "value[].skuPartNumber" |
A list including AAD_PREMIUM or AAD_PREMIUM_P2 (or an M365 SKU that bundles it) |
| Groups Administrator role | Entra portal → Roles and administrators | Your account listed under Groups Administrator (or Global Admin) |
az CLI 2.50+ |
az version |
"azure-cli": "2.x" ≥ 2.50 |
| Signed in | az account show --query tenantId -o tsv |
Your tenant GUID prints |
Create two throwaway test users so the rule has something to match (skip if you already have users with a department set). We will tag them with department = LabSales so the rule is unambiguous and we don’t accidentally scoop up real users:
# Set a domain variable to your tenant's verified domain (e.g. contoso.onmicrosoft.com)
DOMAIN="$(az rest --method GET --uri 'https://graph.microsoft.com/v1.0/domains' \
--query "value[?isDefault].id | [0]" -o tsv)"
echo "Using domain: $DOMAIN"
# Create two enabled test users in department 'LabSales'
for U in labuser1 labuser2; do
az rest --method POST --uri "https://graph.microsoft.com/v1.0/users" \
--headers "Content-Type=application/json" \
--body "{
\"accountEnabled\": true,
\"displayName\": \"Lab User ${U}\",
\"mailNickname\": \"${U}\",
\"userPrincipalName\": \"${U}@${DOMAIN}\",
\"department\": \"LabSales\",
\"usageLocation\": \"IN\",
\"passwordProfile\": { \"forceChangePasswordNextSignIn\": true, \"password\": \"Lab!Pass${RANDOM}9z\" }
}"
done
Expected output: two JSON blobs, each with a new id (a GUID) and "department": "LabSales". If you get a 403, your account lacks rights to create users — ask for User Administrator, or just reuse two existing users and set their department instead.
Part A — Build it in the Azure portal
This is the path you’ll use most for one-off groups.
- Sign in to the Microsoft Entra admin centre (
entra.microsoft.com) → Groups → All groups → New group. - Set Group type =
Security. (Microsoft 365 also supports dynamic membership, but Security is the cleanest for access scenarios.) - Group name:
dyn-lab-sales. Add a description so future-you knows it’s rule-driven, e.g. “Dynamic: all enabled users in department LabSales.” - Membership type: change the dropdown from
AssignedtoDynamic User. This is the decisive step — note that Add members/owners disappears once you do this. - Under Dynamic user members, click Add dynamic query. The rule builder opens.
- In the builder, set Property =
department, Operator =Equals, Value =LabSales. The builder writes the rule for you. - Click Edit (the
</>rule-syntax toggle) to see the text form. It should read exactly:(user.department -eq "LabSales") - Click Validate Rules (preview tool), then Add users and pick
Lab User labuser1. Expected result: a green tick — the user matches the rule. Add a user you know is not in LabSales; expected: a red cross — does not match. This is how you test a rule before saving. - Click Save on the rule, then Create the group.
- The group is created but its membership shows empty / “Evaluating” for a few minutes — this is normal, not a bug. Go get coffee.
- After a few minutes, open the group → Members. Expected:
Lab User labuser1andLab User labuser2are present, and you cannot click “Add members” (it’s gone). The membership processing state on the Overview shows Evaluation/Update succeeded.
If after ten minutes the group is still empty, jump to the troubleshooting section — but 90% of “it’s empty” reports are step 10 impatience.
Part B — Build it with the az CLI (Microsoft Graph)
The CLI path is what you script and put in pipelines. There is no first-class az ad group flag for dynamic rules, so we go straight to Microsoft Graph via az rest. The two non-obvious fields are groupTypes: ["DynamicMembership"] and membershipRuleProcessingState: "On".
- Create the dynamic group in one call:
Expected output: a JSON object with a new groupaz rest --method POST --uri "https://graph.microsoft.com/v1.0/groups" \ --headers "Content-Type=application/json" \ --body '{ "displayName": "dyn-lab-sales-cli", "description": "Dynamic: enabled users in department LabSales (CLI)", "mailEnabled": false, "mailNickname": "dyn-lab-sales-cli", "securityEnabled": true, "groupTypes": ["DynamicMembership"], "membershipRule": "(user.department -eq \"LabSales\")", "membershipRuleProcessingState": "On" }'id,"groupTypes": ["DynamicMembership"], and yourmembershipRuleechoed back. Capture the id:GID="$(az rest --method GET \ --uri "https://graph.microsoft.com/v1.0/groups?\$filter=displayName eq 'dyn-lab-sales-cli'&\$select=id" \ --query "value[0].id" -o tsv)" echo "Group id: $GID" - Confirm the rule and processing state landed:
Expected:az rest --method GET \ --uri "https://graph.microsoft.com/v1.0/groups/$GID?\$select=displayName,membershipRule,membershipRuleProcessingState" \ -o json"membershipRuleProcessingState": "On"and the exact rule string. - Wait 2–5 minutes, then query the derived members:
Expected output (after processing): both lab users listed withaz rest --method GET \ --uri "https://graph.microsoft.com/v1.0/groups/$GID/members?\$select=displayName,department,accountEnabled" \ --query "value[].{name:displayName, dept:department, enabled:accountEnabled}" -o tabledept = LabSales. An emptyvaluebefore that is the processing lag, not a failure. - Prove that manual membership is rejected on a dynamic group (the key constraint). Try to add any user by hand:
Expected: an error along the lines of “The dynamic membership of the group does not allow updates to its members”. That error is the lesson, not a problem.# This SHOULD fail with 400/403 — dynamic groups reject manual member writes az rest --method POST --uri "https://graph.microsoft.com/v1.0/groups/$GID/members/\$ref" \ --headers "Content-Type=application/json" \ --body '{"@odata.id":"https://graph.microsoft.com/v1.0/directoryObjects/00000000-0000-0000-0000-000000000000"}' - (Optional) Change the rule in place and watch membership shift. Pause processing, update the rule, resume:
Expected:az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/groups/$GID" \ --headers "Content-Type=application/json" \ --body '{ "membershipRule": "(user.department -eq \"LabSales\") and (user.usageLocation -eq \"IN\")" }'204 No Content. After re-processing, members whoseusageLocationisn’tINdrop out. This demonstrates that editing the rule, not the list, is how you change membership.
Part C — Build it as Bicep (infrastructure as code)
For production, the rule belongs in version control and code review, not a portal box. Entra groups are deployable with the Microsoft.Graph Bicep extension. Save this as dyn-group.bicep:
extension microsoftGraphV1
@description('Department value the rule matches')
param departmentValue string = 'LabSales'
resource dynSalesGroup 'Microsoft.Graph/groups@v1.0' = {
displayName: 'dyn-lab-sales-bicep'
description: 'Dynamic: enabled users in department ${departmentValue} (Bicep)'
uniqueName: 'dyn-lab-sales-bicep'
mailEnabled: false
mailNickname: 'dyn-lab-sales-bicep'
securityEnabled: true
groupTypes: [
'DynamicMembership'
]
membershipRule: '(user.department -eq "${departmentValue}") and (user.accountEnabled -eq true)'
membershipRuleProcessingState: 'On'
}
output groupId string = dynSalesGroup.id
output appliedRule string = dynSalesGroup.membershipRule
- Deploy it at tenant scope (Graph resources deploy to the tenant, not a resource group):
Expected: a successful deployment whoseaz deployment tenant create \ --name dyn-group-lab \ --location centralindia \ --template-file dyn-group.bicep \ --parameters departmentValue=LabSalesoutputs.groupId.valueis the new group’s GUID andoutputs.appliedRule.valueechoes the full rule with theaccountEnabledclause. - Validate via CLI exactly as in Part B step 3, substituting the Bicep group’s id. Expected: both lab users present (they are enabled and in LabSales).
- Note the IaC win: the rule (
membershipRule) is now a reviewed line in source control. Changing membership policy is a pull request, not an ad-hoc portal edit — which is precisely how Lumio avoided typing a rule wrong at 5 p.m.
Validate the whole thing
A quick cross-check that all three groups derived the same membership:
# List all three lab groups and their rules side by side
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/groups?\$filter=startswith(displayName,'dyn-lab-sales')&\$select=displayName,membershipRule,membershipRuleProcessingState" \
--query "value[].{name:displayName, state:membershipRuleProcessingState, rule:membershipRule}" -o table
Expected output: three rows, each state = On, each with a LabSales rule, each (after processing) deriving your two lab users.
Teardown
Delete everything you created so nothing lingers (and no licences ever attach):
# Delete the three lab groups by name
for NAME in dyn-lab-sales dyn-lab-sales-cli dyn-lab-sales-bicep; do
ID="$(az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/groups?\$filter=displayName eq '${NAME}'&\$select=id" \
--query "value[0].id" -o tsv)"
if [ -n "$ID" ] && [ "$ID" != "None" ]; then
az rest --method DELETE --uri "https://graph.microsoft.com/v1.0/groups/${ID}"
echo "Deleted group ${NAME} ($ID)"
fi
done
# Delete the two test users
for U in labuser1 labuser2; do
UID="$(az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/users?\$filter=mailNickname eq '${U}'&\$select=id" \
--query "value[0].id" -o tsv)"
if [ -n "$UID" ] && [ "$UID" != "None" ]; then
az rest --method DELETE --uri "https://graph.microsoft.com/v1.0/users/${UID}"
echo "Deleted user ${U} ($UID)"
fi
done
Deleted groups and users go to the Entra recycle bin for 30 days; you can purge them permanently from Deleted groups / Deleted users if needed. Expected: each delete prints its confirmation; re-running the validate query now returns zero rows.
Common mistakes & troubleshooting
The failures that consume the most time, each with the exact way to confirm and fix it:
| # | Symptom | Root cause | Confirm with | Fix |
|---|---|---|---|---|
| 1 | “I created the group but it’s empty” | Rule still processing (full re-evaluation runs on create) | Group → Overview → membership processing state shows Evaluating | Wait 3–10 min; large tenants longer — it is not broken |
| 2 | Rule won’t save / “requires a premium licence” | Tenant has no Entra ID P1/P2 | subscribedSkus lacks AAD_PREMIUM* |
Assign a P1/P2 (or bundling M365) licence to the tenant |
| 3 | “Add members” is greyed out | Group is dynamic — manual edits are blocked by design | Membership type = Dynamic User/Device | Edit the rule, not the list; or use an assigned group |
| 4 | Rule rejected: invalid syntax | Mixed user./device., unquoted string, or bad operator |
Portal shows a red rule error; az rest returns 400 |
One species only; quote strings; check operator spelling |
| 5 | Expected user not a member | Their attribute isn’t set in Entra (or has a typo/case) | Check the user object’s department etc. via Graph |
Populate/correct the attribute at the source; wait to re-process |
| 6 | Users not updating after an HR change | Attribute changed upstream but hasn’t synced to Entra | Compare on-prem AD value vs Entra value | Wait for Entra Connect sync, then rule re-processing |
| 7 | Group won’t become role-assignable | Dynamic groups cannot be role-assignable | Toggle is disabled when membership type is dynamic | Use an assigned group for role assignment / PIM |
| 8 | Membership “paused”, never updates | membershipRuleProcessingState set to Paused |
GET …?$select=membershipRuleProcessingState returns Paused |
PATCH it back to On |
| 9 | Too many / wrong users scooped in | Rule too broad (e.g. -contains matching unintended substrings) |
Validate Rules against edge-case users | Tighten the rule; add -and clauses; test before saving |
| 10 | Licences/CA applied to wrong people | A wrong rule propagated to a consumer instantly | Audit who the licensing/CA policy now covers | Fix the rule; remember blast radius — test in dev first |
The single highest-value habit, drawn from rows 1 and 9: use Validate Rules (or a Graph membership query) before you trust a group, and give a new group a few minutes before declaring it broken. The second habit, from rows 5 and 6: a rule can only ever be as correct as the data on the object — when a user is missing from a group, suspect the attribute long before you suspect the engine.
Best practices
- Name groups so the rule is self-evident.
dyn-sales-licensed, notGroup 7. Thedyn-prefix and a description stating the rule save every future admin a lookup. - Always validate before you trust. Use the portal’s Validate Rules tool (or a Graph members query) against known edge-case objects — an active user, a leaver, a guest — before any consumer points at the group.
- Keep rules as simple as the intent allows. Prefer one or two clear clauses over a clever regex. Every added clause is a future maintenance and debugging cost.
- Filter on
accountEnabledfor anything that costs or grants. Addingand (user.accountEnabled -eq true)keeps disabled leavers out of licensed and access groups automatically — this alone closes most drift. - Manage production rules as code. Define the group and its
membershipRulein Bicep (or Graph in a pipeline) so changes are reviewed and version-controlled, not typed into a portal at the end of the day. - Use extension attributes for data Entra doesn’t model. When the grouping depends on a custom fact, write it to
extensionAttribute1–15(from on-prem AD or via Graph) and filter on that — don’t invent a property that isn’t filterable. - Don’t try to make a dynamic group role-assignable. Privileged role assignment needs assigned membership and PIM; never govern privilege with a rule.
- Respect the processing lag in runbooks. If onboarding automation depends on a user landing in a dynamic group, build in a wait/retry — the membership isn’t instantaneous.
- One species per group. Decide user vs device up front; never try to express “users and their devices” in a single rule.
- Review broad rules periodically. A rule like
objectId -ne null(all users) is powerful and easy to over-grant — pair such groups with access reviews.
Security notes
- Mind the blast radius. A dynamic group feeding Conditional Access or licensing applies a rule change to everyone it newly matches, instantly. Treat the rule on such a group as security-critical code: review it, test it in a dev tenant, and prefer IaC so it can’t be changed casually.
- Never govern privilege with a rule. Dynamic groups cannot be role-assignable, and that is a feature — keep it that way. Use assigned groups plus PIM for any group that grants an Entra or Azure role, so a directory-attribute change can never silently escalate someone.
- Be deliberate with
-containsand-match. A loose substring or regex can scoop in unintended principals (e.g. a-contains "admin"rule matchingbadmin@…). Anchor regexes, prefer-eq/-startsWithwhere possible, and validate against adversarial examples. - Protect the attribute source. Because the rule trusts the data on the object, whoever can write
departmentor anextensionAttributecan change who is in the group. Lock down who can edit user attributes (and the on-prem AD fields that sync in) with least privilege. - Exclude guests intentionally, not accidentally. Rules that don’t filter
userTypecan sweep in guest accounts you didn’t mean to license or grant. Add(user.userType -eq "Member")where the group should be employees only. - Audit membership changes. Dynamic membership changes appear in the Entra audit logs; route them to a Log Analytics workspace so a rule that suddenly adds 500 people is visible and alertable.
- Pair sensitive groups with access reviews. Even a correct rule benefits from periodic recertification when it gates real access — defence in depth against a rule that drifts out of policy intent.
Cost & sizing
The “size” of a dynamic group has two cost dimensions, and neither is the group object itself (groups are free):
- The licence to use the feature. Dynamic membership requires Microsoft Entra ID P1 at minimum (P2 if you also want PIM/access-reviews/Identity Protection). P1 is roughly $6/user/month (often already bundled in Microsoft 365 E3/E5 or EMS). The licence is per-user for users who benefit from premium features; for a tenant already on E3/E5 this is effectively sunk cost. If you are on the free tier, this is the one real spend the feature requires.
- What the group grants. This is where the money actually moves. A dynamic group feeding group-based licensing assigns a paid licence (e.g. E5 ≈ $57/user/month) to every member — so a rule that is too broad over-licenses and a rule that is too narrow breaks apps. The cost lever is rule precision plus the
accountEnabledfilter, which is exactly how Lumio reclaimed 60+ licences.
Rough figures for sizing a decision:
| Item | Rough cost | Notes |
|---|---|---|
| Group object | Free | No charge for groups, dynamic or assigned |
| Entra ID P1 (enables dynamic membership) | ~$6/user/month (~₹500) | Bundled in M365 E3/E5, EMS E3 |
| Entra ID P2 (adds PIM, access reviews) | ~$9/user/month (~₹750) | Bundled in M365 E5, EMS E5 |
| What a licence-feeding group grants (e.g. E5) | ~$57/user/month (~₹4,700) | The real spend the rule controls |
| Graph API / processing | Free | No per-evaluation charge |
Sizing guidance: there is no instance to right-size, but rule precision is the cost control. Always include accountEnabled -eq true on cost- or access-granting groups; exclude guests with userType -eq "Member" unless you mean to include them; and review broad rules so you are not licensing or granting access to people who shouldn’t have it. The free-tier reality: dynamic membership is not available without P1 — on a free tenant you maintain groups manually or upgrade.
Interview & exam questions
1. What is a dynamic membership group and how does it differ from an assigned group? An assigned group stores an explicit, hand-maintained list of members. A dynamic group stores a membership rule (a boolean expression over object attributes) and stores no list — Entra derives the membership by evaluating the rule, adding and removing members automatically as objects change. You edit the rule, never the list.
2. What licence is required for dynamic groups, and why does this surprise people? Microsoft Entra ID P1 (or P2). It surprises people because group creation itself is free, so they assume dynamic membership is too — but the rule simply won’t save on a free tenant. P1 is commonly bundled in M365 E3/E5 and EMS.
3. Can a single dynamic group contain both users and devices? No. A group’s membership type is either Dynamic User or Dynamic Device, and its rule must reference only user.* or only device.* properties. A rule mixing the two is invalid. “Users and their devices” requires two groups.
4. You created a dynamic group and it’s empty. What’s the most likely explanation? It is still processing — when a rule is created or changed, Entra runs a full re-evaluation of the directory, which takes minutes (longer in large tenants). The group populates once processing completes. Check the membership processing state before assuming the rule is wrong.
5. Why can’t you add a member to a dynamic group by hand? Because the rule is the sole authority for membership — allowing manual edits would let the membership diverge from the rule. The portal greys out “Add members” and Graph rejects manual member writes on a dynamic group. To include someone, fix the rule or use a different group.
6. Write a rule for all enabled users in the Finance department who are not guests. (user.department -eq "Finance") and (user.accountEnabled -eq true) and (user.userType -eq "Member"). The accountEnabled clause excludes disabled leavers; the userType clause excludes guests.
7. Which operator would you use to match a multi-value property like proxyAddresses, and why not -eq? Use -contains (or -any for collections of complex objects). -eq compares the whole collection and rarely matches as intended; -contains tests whether the collection includes the value.
8. Can a dynamic group be made role-assignable for an Entra directory role? No. Role-assignable groups must have assigned membership by design, so a directory-attribute change can never silently grant a privileged role. Use assigned groups plus PIM for privileged access.
9. A user changed departments in HR but is still in the old dynamic group. What do you check, in order? First, whether the new value has synced into Entra (compare on-prem AD vs the Entra user object — Entra Connect runs on a schedule). Then, whether the rule has re-processed since the change (processing lag). The attribute on the Entra object plus the processing state explain almost every “didn’t update” case.
10. What field tells you whether a group is dynamic, and what value enables rule evaluation? groupTypes contains DynamicMembership for a dynamic group, and membershipRuleProcessingState must be On (not Paused) for the rule to be evaluated. Both are visible via a Graph GET on the group.
11. Give a concrete business reason dynamic groups save money. Group-based licensing assigns paid licences (e.g. E5 ≈ $57/user/month) to every member. A dynamic rule that includes accountEnabled -eq true automatically drops disabled leavers, eliminating licences paid for ghost accounts — directly reclaiming spend that manual groups leak through drift.
12. How should you safely roll out a new rule that will feed Conditional Access? Build and Validate Rules in a non-production/dev tenant against edge-case users, define the final rule as Bicep for code review, and only then apply it in production — because a rule on a CA group changes who is affected the instant it processes (blast radius).
These map most directly to SC-300 (Identity and Access Administrator) — implement and manage groups, group-based licensing, and dynamic membership — and touch AZ-104 (Administrator) for the groups-and-governance fundamentals. The licensing-cost angle is relevant background for MS-102 (Microsoft 365 Administrator). A compact mapping:
| Question theme | Primary cert | Objective area |
|---|---|---|
| Dynamic vs assigned, membership rules | SC-300 | Plan and implement groups |
| Group-based licensing & cost | SC-300 / MS-102 | Manage licences via groups |
| Role-assignable groups & PIM boundary | SC-300 | Privileged access governance |
| Groups in Conditional Access | SC-300 | Implement Conditional Access |
| Groups fundamentals & RBAC scoping | AZ-104 | Manage Entra users and groups |
Quick check
- True or false: you can add one extra person by hand to a dynamic group when they don’t match the rule.
- You write
(user.department -eq "IT") or (device.deviceOSType -eq "Windows")and the portal rejects it. Why? - A brand-new dynamic group is empty thirty seconds after you create it. Is it broken? What do you do?
- Which single clause should you add to a licence-feeding rule to stop paying for disabled leavers?
- Name the
groupTypesvalue and themembershipRuleProcessingStatevalue that together mean “this is a dynamic group whose rule is actively evaluated.”
Answers
- False. A dynamic group’s membership is derived solely from its rule; manual add/remove is blocked (the portal greys it out, Graph rejects it). To include someone, change the rule or use an assigned group.
- Because a rule must target one species — either users or devices. Mixing
user.*anddevice.*in the same rule is invalid; you’d need two separate groups. - Not broken. A new or changed rule triggers a full re-evaluation that takes a few minutes (longer in large tenants). Wait and check the membership processing state; it will populate.
and (user.accountEnabled -eq true)— disabled accounts then fall out of the group automatically, so you stop licensing leavers.groupTypescontainsDynamicMembership, andmembershipRuleProcessingStateisOn(notPaused).
Glossary
- Dynamic membership group — a group whose members are derived from a saved membership rule rather than a hand-maintained list; Entra adds/removes members automatically as objects change.
- Assigned group — a group with an explicit, manually maintained member list (the default, non-dynamic type).
- Membership type — the group property choosing Assigned, Dynamic User, or Dynamic Device; set at creation and effectively fixed.
- Membership rule (
membershipRule) — the boolean expression overuser.*ordevice.*properties that derives membership. membershipRuleProcessingState—On(rule evaluated) orPaused(evaluation stopped); must beOnfor a dynamic group to update.groupTypes— the Graph property whoseDynamicMembershipvalue marks a group as dynamic (andUnifiedmarks a Microsoft 365 group).- Filterable property — a user or device attribute that membership rules are allowed to reference (an allow-listed subset of the directory schema).
- Extension attribute —
extensionAttribute1–15, free-text custom fields on user/device objects; the escape hatch for data Entra doesn’t model natively. - Operator — a comparison keyword in a rule:
-eq,-ne,-contains,-startsWith,-match,-in,-any,-all, combined with-and/-or/-not. - Processing lag — the few-minute delay between a relevant change (or rule creation) and the membership being recomputed; the reason a new group is briefly empty.
- Group-based licensing — assigning Microsoft licences to all members of a group; a primary consumer of dynamic groups and the main cost lever.
- Conditional Access — Entra policies that include/exclude groups to enforce MFA, device compliance, location and risk controls; a primary consumer where rule correctness is security-critical.
- Role-assignable group — a group that can be granted an Entra directory role; cannot be dynamic — it must have assigned membership.
- Entra ID P1/P2 — the premium licence tiers; P1 is the minimum required for dynamic membership, P2 adds PIM, access reviews and Identity Protection.
- Microsoft Graph — the API behind
az restcalls here; where the group, its rule and its derived members are read and written.
Next steps
You can now build self-maintaining groups and validate them before they go live. Build outward:
- Next: Azure Key Vault: Secrets, Keys and Certificates Done Right — pair dynamic groups with managed identities and least-privilege secret access.
- Related: Management Groups 101: Designing a Hierarchy That Scopes Policy and RBAC — where groups plug into the broader RBAC and policy scope.
- Related: Azure Policy Governance at Scale — govern the resources your grouped identities can touch.
- Related: Azure Resource Hierarchy Explained: Subscriptions, Resource Groups and Resources — the scoping model everything in Entra ultimately targets.
- Related: Deploy Your First Bicep File From Scratch — go deeper on the IaC that made the production group in this lab reviewable.