Blocking sign-in or disabling an account in Microsoft Entra ID stops the person getting in. It does nothing to the licenses on the account, so a disabled leaver keeps holding an E3 or E5 until someone removes it. This guide finds those accounts with Microsoft Graph PowerShell, turns license IDs into product names, removes the direct assignments, deals with licenses that come from groups, and then covers the step most clean-ups skip: getting the saving onto the invoice.
Sentsai’s free, read-only scan finds paid licenses still assigned to disabled accounts and disabled guests, plus purchased licenses nobody holds. The free scan shows the savings total; the itemized report is paid.
Needs a one-time approval of read-only access from an administrator who can approve apps. Not an admin? Email this to yours.
Use the Microsoft Graph PowerShell SDK. The MSOnline module was retired in May 2025 and the AzureAD module is deprecated, so scripts built on Get-MsolUser or Set-MsolUserLicense need rewriting, not reviving. Do the discovery with read-only permissions, review the output, and only then connect with write access for the removal.
A disabled account has accountEnabled set to false, and assignedLicenses lists every license it holds, whether assigned directly or inherited from a group. Counting a collection inside a filter is an advanced query in Microsoft Graph, so the command needs -ConsistencyLevel eventual and a count variable. The backtick before $count stops PowerShell reading it as a variable of its own.
Connect-MgGraph -Scopes "User.Read.All", "LicenseAssignment.Read.All"
$disabled = Get-MgUser -Filter "accountEnabled eq false and assignedLicenses/`$count ne 0" -ConsistencyLevel eventual -CountVariable disabledCount -All -Property Id, DisplayName, UserPrincipalName, UserType, AssignedLicenses, LicenseAssignmentStates
$disabledCount now holds the number found. Licensed guests that have been disabled appear too, with a UserType of Guest, and they are usually the easiest removals of all.
assignedLicenses holds SKU IDs, which are GUIDs. Get-MgSubscribedSku returns each SKU in the tenant with its skuPartNumber, such as SPE_E3 for Microsoft 365 E3, so build a lookup table once and export a readable list to review.
$sku = @{}; Get-MgSubscribedSku -All | ForEach-Object { $sku[$_.SkuId] = $_.SkuPartNumber }
$disabled | Select-Object DisplayName, UserPrincipalName, @{n="Licenses"; e={ ($_.AssignedLicenses.SkuId | ForEach-Object { $sku[$_] }) -join ", " }} | Export-Csv .\disabled-licensed.csv -NoTypeInformation
Read the file before changing anything. Some former staff are kept licensed on purpose: a mailbox under litigation hold, a manager who still needs access to the mailbox, an account retained for a legal or HR reason. Those are decisions to confirm with their owners, not waste.
With group-based licensing, some of these licenses were never assigned to the user. They come from membership of a licensed group, and a license inherited from a group cannot be removed from the user directly; Graph rejects the request. The licenseAssignmentStates property shows which is which: each entry has a skuId and an assignedByGroup value that is empty for a direct assignment and holds the group’s object ID for an inherited one.
$disabled | ForEach-Object { $u = $_; $u.LicenseAssignmentStates | Where-Object { $_.AssignedByGroup } | Select-Object @{n="User"; e={$u.UserPrincipalName}}, @{n="License"; e={$sku[$_.SkuId]}}, AssignedByGroup }
To remove an inherited license, take the user out of the group, or, for a dynamic group, change its membership rule so disabled accounts drop out. If a user holds the same license both directly and through a group, it counts as one license, and removing only the direct copy changes nothing until the group membership goes too.
Connect again with write access. Set-MgUserLicense takes the SKU IDs to remove and an empty -AddLicenses value, which Microsoft’s own examples pass as @{}. Try one account first, then run the loop, which removes only the directly assigned licenses on each disabled account.
Connect-MgGraph -Scopes "User.Read.All", "LicenseAssignment.ReadWrite.All"
$e3 = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -eq "SPE_E3"; Set-MgUserLicense -UserId "leaver@contoso.com" -RemoveLicenses @($e3.SkuId) -AddLicenses @{}
foreach ($u in $disabled) { $direct = @($u.LicenseAssignmentStates | Where-Object { -not $_.AssignedByGroup } | Select-Object -ExpandProperty SkuId -Unique); if ($direct.Count -gt 0) { Set-MgUserLicense -UserId $u.Id -RemoveLicenses $direct -AddLicenses @{} | Out-Null } }
Removing all of a user’s direct licenses in one call avoids a dependency error, which Graph raises when you remove a license that another assigned license still depends on. Run Step 1 again afterwards: anything left is either inherited from a group or failed for a reason worth reading.
This is where most of the saving gets lost. An unassigned license goes back into the pool and you keep paying for it, because you pay for the subscription quantity, not for assignments. Microsoft’s own instructions for reducing a subscription make it two steps: unassign licenses from users, then remove licenses from the subscription. When you can take the second step depends on how you buy:
Until renewal, the freed licenses still have value: assign them to new starters instead of buying more. Then carry the final count into the renewal so the reduction actually happens.
Sentsai finds these accounts for Microsoft 365, read-only, in minutes: paid licenses on disabled accounts and disabled guests, purchased licenses nobody holds, and group license assignments that Microsoft reports in an error state. It never removes a license itself; it shows you what to remove and what it costs. The free scan shows the total annual savings it identified, how many users it checked and how many compliance gaps it found. The itemized list, with every named account and license, costs 5% of the identified annual savings, with a $99 minimum.
Read-only, for Microsoft 365, in minutes. The free scan shows the total annual savings and the headline counts; the itemized, named list costs 5% of identified annual savings, $99 minimum.
Needs a one-time approval of read-only access from an administrator who can approve apps. Not an admin? Email this to yours.
Questions? support@sentsai.com