On November 3, 2026, Microsoft retired the memberOf rule operator in Microsoft Entra ID. The operator spent years in public preview with a clear “not for production” label, and plenty of tenants built access, licensing and Conditional Access on top of it anyway.

Two days later, the useful question is no longer what Microsoft announced. It is this: what in my tenant is frozen right now, and who has the wrong access because of it?

This post is the audit I would run. Inventory, blast radius, drift, then fix.


What was retired

The operator let you build a dynamic rule from the membership of other groups instead of from user or device attributes:

user.memberof -any (group.objectId -in ['<group-id-1>', '<group-id-2>'])

Three kinds of objects can carry that rule, and all three are affected:

ObjectWhat happens after November 3
Dynamic membership groupsStop processing membership changes
Dynamic administrative unitsStop updating membership and administrative scope
Entitlement management auto-assignment policiesStop updating assignments

Nothing is deleted and nothing throws an error. These objects simply keep whatever membership they had on November 3.


Why this one is dangerous: it fails quietly

A frozen group looks perfectly healthy. The consequences show up somewhere else:

  • Joiners never enter the group, so they miss licenses, app assignments and Teams or SharePoint access.
  • Leavers and movers never leave it, so they keep access they should have lost.
  • Conditional Access policies that include or exclude these groups may stop receiving updated membership. Microsoft calls this out explicitly in its final reminders.
  • SCIM provisioning from a frozen group goes quiet. The WorkOS write-up on this describes no new accounts for joiners and no role changes for movers, while disable and delete events still flow through.

One more detail from Microsoft’s own documentation: memberOf groups never updated automatically when a child group was deleted or when members were removed from a child group. So some of your groups may have been drifting well before November 3.


Why Microsoft pulled it

Microsoft’s stated reason is scale and reliability. A single memberOf rule could slow dynamic membership processing for every dynamic group in the tenant, not just the one using it. The preview also came with hard limits that were never lifted:

  • Maximum of 500 memberOf groups per tenant
  • Could not be combined with any other condition in the rule
  • Required Entra ID P1 or P2
  • Public cloud only

It was never promoted to general availability, so it was retired instead.


Timeline

DateWhat happened
August 5, 2026MC1448379 announces the retirement
October 5, 2026MC1488834 final reminder
October 28, 2026MC1529463 final reminder and migration guidance
November 3, 2026Retirement, worldwide

A note on the Graph memberOf endpoint

Some coverage says the Graph memberOf API call stops working too and breaks scripts. I could not confirm that. The Message Center text I read lists only the three services in the table above, and the same coverage says the migration path for scripts is the Graph API for direct and transitive membership. That only makes sense if memberOf and transitiveMemberOf keep working.

Step 4 below shows you how to check this in your own tenant instead of guessing.


Step 1: Inventory every memberOf rule

This script is read-only. It finds dynamic groups, dynamic administrative units and entitlement assignment policies that reference memberOf, and exports each to CSV.

Connect-MgGraph -Scopes "Group.Read.All","AdministrativeUnit.Read.All","EntitlementManagement.Read.All"

$out  = "C:\Temp\memberOf-audit"
New-Item -ItemType Directory -Path $out -Force | Out-Null
$guid = '[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}'

function Get-GraphPaged([string]$Uri) {
    do {
        $page = Invoke-MgGraphRequest -Method GET -Uri $Uri
        $page.value
        $Uri  = $page.'@odata.nextLink'
    } while ($Uri)
}

# 1. Dynamic groups
$groups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'DynamicMembership')" -All -Property "id,displayName,membershipRule,membershipRuleProcessingState,assignedLicenses"

$affectedGroups = $groups | Where-Object { $_.MembershipRule -match 'memberof' } | ForEach-Object {
    [PSCustomObject]@{
        GroupId         = $_.Id
        DisplayName     = $_.DisplayName
        ProcessingState = $_.MembershipRuleProcessingState
        Rule            = $_.MembershipRule
        SourceGroupIds  = (([regex]::Matches($_.MembershipRule, $guid).Value | Select-Object -Unique) -join ";")
        LicenseSkuIds   = ($_.AssignedLicenses.SkuId -join ";")
    }
}

# 2. Dynamic administrative units
$aus = Get-GraphPaged 'https://graph.microsoft.com/v1.0/administrativeUnits?$select=id,displayName,membershipType,membershipRule,membershipRuleProcessingState'

$affectedAUs = $aus | Where-Object { $_.membershipRule -match 'memberof' } | ForEach-Object {
    [PSCustomObject]@{
        AdminUnitId     = $_.id
        DisplayName     = $_.displayName
        ProcessingState = $_.membershipRuleProcessingState
        Rule            = $_.membershipRule
    }
}

# 3. Entitlement management assignment policies (auto-assignment rules live here)
$policies = Get-GraphPaged 'https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentPolicies'

$affectedPolicies = $policies | Where-Object { ($_ | ConvertTo-Json -Depth 10) -match 'memberof' } | ForEach-Object {
    [PSCustomObject]@{
        PolicyId        = $_.id
        DisplayName     = $_.displayName
        AccessPackageId = $_.accessPackageId
    }
}

$affectedGroups   | Export-Csv "$out\dynamic-groups.csv"    -NoTypeInformation -Encoding UTF8
$affectedAUs      | Export-Csv "$out\dynamic-admin-units.csv" -NoTypeInformation -Encoding UTF8
$affectedPolicies | Export-Csv "$out\entitlement-policies.csv" -NoTypeInformation -Encoding UTF8

Write-Host "Dynamic groups using memberOf:   $(@($affectedGroups).Count)" -ForegroundColor Yellow
Write-Host "Dynamic admin units:             $(@($affectedAUs).Count)" -ForegroundColor Yellow
Write-Host "Entitlement policies:            $(@($affectedPolicies).Count)" -ForegroundColor Yellow
Write-Host "Reports saved to $out" -ForegroundColor Green

The SourceGroupIds column holds the groups each rule pointed to. You need it for Step 3.


Step 2: Map the blast radius

A list of frozen groups is not useful until you know what each one controls. This script reads the CSV from Step 1 and, for every affected group, lists the Conditional Access policies that reference it, the enterprise apps it is assigned to, and the licenses it carries.

Connect-MgGraph -Scopes "Group.Read.All","Policy.Read.All","Directory.Read.All"

$out            = "C:\Temp\memberOf-audit"
$affectedGroups = Import-Csv "$out\dynamic-groups.csv"
$caPolicies     = Get-MgIdentityConditionalAccessPolicy -All

$impact = foreach ($g in $affectedGroups) {
    $ca = $caPolicies | Where-Object {
        $_.Conditions.Users.IncludeGroups -contains $g.GroupId -or
        $_.Conditions.Users.ExcludeGroups -contains $g.GroupId
    }
    $apps = Get-MgGroupAppRoleAssignment -GroupId $g.GroupId -All

    [PSCustomObject]@{
        DisplayName        = $g.DisplayName
        GroupId            = $g.GroupId
        ConditionalAccess  = ($ca.DisplayName -join "; ")
        AssignedApps       = (($apps.ResourceDisplayName | Select-Object -Unique) -join "; ")
        GroupBasedLicenses = $g.LicenseSkuIds
    }
}

$impact | Export-Csv "$out\blast-radius.csv" -NoTypeInformation -Encoding UTF8
$impact | Format-Table DisplayName, ConditionalAccess, AssignedApps -AutoSize

License SKU IDs are GUIDs. Translate them with Get-MgSubscribedSku | Select-Object SkuPartNumber, SkuId.


Step 3: Measure the drift

The memberOf operator built a group from the direct members of its source groups. That means you can calculate what the membership should be today and compare it with what the frozen group actually contains:

  • Missing means the person should be in the group but is not. These are joiners who never got access.
  • Stale means the person is in the group but should not be. These are leavers and movers who kept access.
Connect-MgGraph -Scopes "Group.Read.All"

$out            = "C:\Temp\memberOf-audit"
$affectedGroups = Import-Csv "$out\dynamic-groups.csv"

function Get-DirectMemberIds([string]$GroupId) {
    Get-MgGroupMember -GroupId $GroupId -All |
        Where-Object { $_.AdditionalProperties['@odata.type'] -ne '#microsoft.graph.group' } |
        Select-Object -ExpandProperty Id
}

$summary = @()
$details = @()

foreach ($g in $affectedGroups) {

    # -notAny rules invert the logic: review those by hand
    if ($g.Rule -match '-notany') {
        $summary += [PSCustomObject]@{ DisplayName = $g.DisplayName; GroupId = $g.GroupId; Expected = ""; Actual = ""; Missing = "MANUAL REVIEW"; Stale = "" }
        continue
    }

    $expected = @($g.SourceGroupIds -split ";" | Where-Object { $_ } | ForEach-Object { Get-DirectMemberIds $_ } | Select-Object -Unique)
    $actual   = @(Get-DirectMemberIds $g.GroupId)
    $missing  = @($expected | Where-Object { $_ -notin $actual })
    $stale    = @($actual   | Where-Object { $_ -notin $expected })

    $summary += [PSCustomObject]@{
        DisplayName = $g.DisplayName
        GroupId     = $g.GroupId
        Expected    = $expected.Count
        Actual      = $actual.Count
        Missing     = $missing.Count
        Stale       = $stale.Count
    }

    foreach ($id in $missing) { $details += [PSCustomObject]@{ GroupId = $g.GroupId; ObjectId = $id; Issue = "Should be a member (not added)" } }
    foreach ($id in $stale)   { $details += [PSCustomObject]@{ GroupId = $g.GroupId; ObjectId = $id; Issue = "Should not be a member (still in group)" } }
}

$summary | Export-Csv "$out\drift-summary.csv" -NoTypeInformation -Encoding UTF8
$details | Export-Csv "$out\drift-details.csv" -NoTypeInformation -Encoding UTF8
$summary | Sort-Object Stale, Missing -Descending | Format-Table -AutoSize

Treat the numbers as a starting point, not a verdict. A group with 0 missing and 0 stale after two days may simply have had no joiners or leavers yet. It is still frozen.


Step 4: Check who is calling the memberOf endpoint

If you send Microsoft Graph activity logs to a Log Analytics workspace, you can see which applications call the memberOf endpoint, and whether those calls started failing after November 3:

MicrosoftGraphActivityLogs
| where TimeGenerated > ago(30d)
| where RequestUri has "memberOf"
| summarize Calls = count(),
            Failures = countif(ResponseStatusCode >= 400),
            LastCall = max(TimeGenerated)
    by AppId, RequestMethod
| order by Calls desc

A spike in Failures after the cutoff would settle the question from the note above. If the failure count stays flat, those integrations are fine. has matches whole terms, so transitiveMemberOf calls are not included.


Step 5: Fix what you found

Work through the blast-radius CSV and decide per group. There are three options.

Option A: Replace the rule with supported attributes

This is the cleanest fix when the source groups were themselves driven by attributes. Move that logic into the group’s own rule. This is an illustrative example:

# Before
user.memberof -any (group.objectId -in ['<Sales-NL-id>', '<Sales-BE-id>'])

# After: the logic the source groups used, written directly
(user.department -eq "Sales") -and ((user.country -eq "NL") -or (user.country -eq "BE"))

Microsoft’s own commentary and the community write-ups agree there is no one-to-one replacement for nested group logic. If the source groups were manually curated, this option does not apply.

Option B: Convert to assigned membership

In the Entra admin center: Groups > All groups > open the group > change Membership type from Dynamic to Assigned > Save.

Converting does not fix drift. Whatever is wrong when you convert stays wrong until someone edits the group by hand. Use drift-details.csv to add the missing people and remove the stale ones as part of the conversion.

Option C: Delete what nobody uses

If the blast-radius report shows no Conditional Access policies, no app assignments and no licenses, and the owner confirms the group is dead, remove it instead of migrating it.

Dynamic administrative units and entitlement policies follow the same three options. Record the owner, the decision and the reason in the ticket for each one.


Where to start

PriorityThe group controlsWhy it comes first
1Conditional Access include/excludeStale membership is a policy gap
1App assignment and SCIM provisioningLeavers keep access, joiners get nothing
2Dynamic administrative unitsDelegated admin scope is out of date
2Group-based licensingJoiners unlicensed, leavers still consuming licenses
3Teams, SharePoint, mail groupsStale audiences, lower risk

After the fix

Re-run Step 1 until it returns zero rows in all three reports. For every group you converted or rewrote, re-run Step 3 against the new state and confirm the stale and missing counts are zero.


Key takeaways

  • The retirement does not delete or break anything visibly. It freezes membership, and the damage is stale access.
  • Leavers keeping access is the real security exposure. Joiners missing access is the operational one.
  • Always check what a group controls before deciding how to fix it. Conditional Access and app assignments come first.
  • Converting to assigned membership locks in the current state, so reconcile the drift first.
  • Confirm Graph endpoint behavior in your own logs instead of relying on secondary coverage.

References