Azure Identity

Entra ID Dynamic Groups: A Membership-Rule Cookbook That Actually Scales

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:

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 (extensionAttribute115). 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 extensionAttribute115, 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.extensionAttribute115 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.

  1. Sign in to the Microsoft Entra admin centre (entra.microsoft.com) → GroupsAll groupsNew group.
  2. Set Group type = Security. (Microsoft 365 also supports dynamic membership, but Security is the cleanest for access scenarios.)
  3. 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.”
  4. Membership type: change the dropdown from Assigned to Dynamic User. This is the decisive step — note that Add members/owners disappears once you do this.
  5. Under Dynamic user members, click Add dynamic query. The rule builder opens.
  6. In the builder, set Property = department, Operator = Equals, Value = LabSales. The builder writes the rule for you.
  7. Click Edit (the </> rule-syntax toggle) to see the text form. It should read exactly:
    (user.department -eq "LabSales")
    
  8. 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.
  9. Click Save on the rule, then Create the group.
  10. The group is created but its membership shows empty / “Evaluating” for a few minutes — this is normal, not a bug. Go get coffee.
  11. After a few minutes, open the group → Members. Expected: Lab User labuser1 and Lab User labuser2 are 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".

  1. Create the dynamic group in one call:
    az 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"
      }'
    
    Expected output: a JSON object with a new group id, "groupTypes": ["DynamicMembership"], and your membershipRule echoed 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"
    
  2. Confirm the rule and processing state landed:
    az rest --method GET \
      --uri "https://graph.microsoft.com/v1.0/groups/$GID?\$select=displayName,membershipRule,membershipRuleProcessingState" \
      -o json
    
    Expected: "membershipRuleProcessingState": "On" and the exact rule string.
  3. Wait 2–5 minutes, then query the derived members:
    az 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 table
    
    Expected output (after processing): both lab users listed with dept = LabSales. An empty value before that is the processing lag, not a failure.
  4. Prove that manual membership is rejected on a dynamic group (the key constraint). Try to add any user by hand:
    # 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"}'
    
    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.
  5. (Optional) Change the rule in place and watch membership shift. Pause processing, update the rule, resume:
    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\")"
      }'
    
    Expected: 204 No Content. After re-processing, members whose usageLocation isn’t IN drop 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
  1. Deploy it at tenant scope (Graph resources deploy to the tenant, not a resource group):
    az deployment tenant create \
      --name dyn-group-lab \
      --location centralindia \
      --template-file dyn-group.bicep \
      --parameters departmentValue=LabSales
    
    Expected: a successful deployment whose outputs.groupId.value is the new group’s GUID and outputs.appliedRule.value echoes the full rule with the accountEnabled clause.
  2. 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).
  3. 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

Security notes

Cost & sizing

The “size” of a dynamic group has two cost dimensions, and neither is the group object itself (groups are free):

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

  1. True or false: you can add one extra person by hand to a dynamic group when they don’t match the rule.
  2. You write (user.department -eq "IT") or (device.deviceOSType -eq "Windows") and the portal rejects it. Why?
  3. A brand-new dynamic group is empty thirty seconds after you create it. Is it broken? What do you do?
  4. Which single clause should you add to a licence-feeding rule to stop paying for disabled leavers?
  5. Name the groupTypes value and the membershipRuleProcessingState value that together mean “this is a dynamic group whose rule is actively evaluated.”

Answers

  1. 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.
  2. Because a rule must target one species — either users or devices. Mixing user.* and device.* in the same rule is invalid; you’d need two separate groups.
  3. 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.
  4. and (user.accountEnabled -eq true) — disabled accounts then fall out of the group automatically, so you stop licensing leavers.
  5. groupTypes contains DynamicMembership, and membershipRuleProcessingState is On (not Paused).

Glossary

Next steps

You can now build self-maintaining groups and validate them before they go live. Build outward:

Entra IDDynamic GroupsIdentityRBACConditional AccessMicrosoft GraphBicepSC-300
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading