If you manage identity in any reasonably mature Entra ID tenant, you've probably wanted a dynamic group that captures "everyone" for baseline Conditional Access policies, licensing, onboarding automation while cleanly leaving out your admin accounts. It sounds like a two-minute rule. It isn't. Here's what actually works, and why the obvious approaches don't.
The problem
A dynamic "all users" group is simple enough on its own:
(user.objectId -ne null) and (user.userType -eq "Member") and (user.accountEnabled -eq true)The trouble starts the moment you try to add "and exclude anyone assigned an admin role." Directory role assignments and PIM eligibility are not attributes the dynamic membership rule engine can evaluate. There's no user.assignedRoles property, and role membership isn't exposed to the rule builder at all.
Dead end #1: memberOf
Entra does have a preview rule operator that lets a dynamic group pull in members of another group:
user.memberof -any (group.objectId -in ['<admin-group-id>'])This works but only in isolation. memberOf cannot be combined with any other condition in the same rule. You cannot write:
(user.accountEnabled -eq true) and not(user.memberof -any (group.objectId -in ['<id>']))Entra's validator rejects it outright, often with an unhelpful error like Invalid object type '...' specified on property 'memberof'. It's also worth knowing this operator is in preview and being sunset Microsoft has flagged it for retirement, so building anything long-term on top of it is a bad bet regardless of the combination limitation.
There's a second wrinkle if your admin structure is a "group of groups": memberOf only reads direct members of whatever group ID you reference. Users sitting inside a nested group underneath it won't be picked up.
Dead end #2: Custom security attributes
The next instinct is Entra ID Governance's custom security attributes feature cloud-native, flexible, seems perfect. It isn't usable here either: custom security attributes are explicitly not supported as a property in dynamic group membership rules. They exist for access reviews and RBAC conditions, not for this.
What actually works: directory extension attributes
The feature that is supported in dynamic rules, and is cloud-writable regardless of on-prem sync status, is a Microsoft Entra directory extension attribute (also called a custom extension property). It's registered against an app registration, and once created it behaves like any other attribute:
(user.objectId -ne null) and (user.userType -eq "Member") and (user.accountEnabled -eq true) and (user.extension_<appid>_isPrivilegedUser -ne "Admin")This combines freely with other conditions because it's a normal property, not a relationship like memberOf.
Registering it
There's no portal UI for this step, it's Graph or PowerShell only:
$app = New-MgApplication -DisplayName "Directory Extensions Owner App"
New-MgApplicationExtensionProperty -ApplicationId $app.Id -BodyParameter @{
Name = "isPrivilegedUser"
DataType = "String"
TargetObjects = @("User")
}One gotcha that cost real debugging time: an app registration alone isn't enough. Writing the extension property onto a user fails with The following extension properties are not available until the app also has a service principal in the tenant:
New-MgServicePrincipal -AppId $app.AppIdOnce that exists, Update-MgUser -AdditionalProperties @{ "extension_<appid>_isPrivilegedUser" = "Admin" } works as expected.
Bridging the gap: a tagging script
Since the dynamic rule can't read group or role membership directly, something has to translate "is this person an admin" into "does this attribute say Admin," and keep it current. That's a scheduled script's job:
- Recursively walk the admin group(s). If your admin structure nests groups inside groups, a flat membership read misses anyone underneath the nested layer so this needs to recurse until it hits actual users.
- Account for PIM. If your admin groups are PIM-for-Groups enabled,
Get-MgGroupMemberonly returns active members. Anyone with an eligible-but-not-activated assignment won't show up in that call at all, which quietly defeats the purpose of the exclusion. Eligible members need a separate queryGet-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleunioned in with the active set. - Tag, then untag. Every run tags newly-found admins and clears the tag from anyone no longer under any admin group, so people who lose admin status fall back into the "all users" group automatically on the next cycle rather than staying excluded indefinitely.
Running it unattended
This lands naturally in an Azure Automation runbook:
- Create the Automation Account
- Enable a system-assigned managed identity
- Import the required Graph modules into the Automation Account
- Create the runbook and paste in the script
- Test the runbook before scheduling it
- Create and link a schedule
- Check job history periodically
Create Dynamic Group
Now you can see that New custom extension appear for dynamic rules and able select it.
