Group vs direct licence assignment

31 Aug 2026

Group-based vs direct licence assignment (and the mess in between)

Nobody decides to run both. It happens: the tenant was built by hand, someone later assigned an E3 to a security group to stop doing it by hand, and now a share of your users are licensed twice — once directly, once through membership. Nothing breaks, which is the problem. The seat count looks right, the users work, and the only artefact recording why a person has a licence is a click somebody made in 2023.

Then a leaver gets removed from the group and keeps their E3. Or a joiner is added to the group and gets nothing, because their usage location was never set. This is the playbook for getting to one assignment model on purpose, in the order that doesn't leave anyone unlicensed halfway.

The one property that tells them apart

The user object's assignedLicenses tells you what a user has. It doesn't tell you how they got it, so it is the wrong place to start. The right one is licenseAssignmentStates, whose whole purpose is that distinction. Per entry: skuId, the state (Active, ActiveWithError, Disabled, Error), any error, and the one that matters here —

The property

assignedByGroup is null when the licence was assigned directly, and contains the group's object id when it was inherited through membership. That single field is your entire migration report — and a user with two entries for the same skuId, one of them null, is a user in the mess.

Nothing at tenant level will show you this. The subscribedSku resource gives you consumedUnits — "the number of licenses that have been assigned" — with no breakdown by how they were assigned. 412 of 450 reads identically whether every seat is group-managed or every seat is a manual click, which is also why it is the number that hides licence waste so well.

ONE SKU, ONE USER: THE FOUR STATES AND THE ONLY SAFE ROUTE No group grants this SKU A licensed group grants it Directly assigned assignedByGroup: null No direct assignment Direct only where most tenants start the reason lives in someone's memory, not in the directory Both at once two entries, same skuId correct to pass through, expensive to live in Unlicensed mail and files stop until processing catches up Group only membership is the record the target state 1. add 2. then remove the direct one removing it first Blue is transient by design; the dashed step is the one that pages you on a Friday.

The migration, in the order that matters

1
Sort every licensed user into three buckets.

Read licenseAssignmentStates for every user and bucket by SKU: direct only, group only, both. Do this before you touch anything — the size of the "both" bucket is what decides whether this is an afternoon or a project. Done when the three buckets add up to consumedUnits for each SKU. If they don't, you have users with an error and you'll meet them in step 5 anyway.

2
One group per SKU, and no nesting.

Group-based licensing doesn't support nested groups: assign a licence to a group that contains groups and only the first-level members get it. A tidy "All Staff → Sales, Engineering" hierarchy silently licenses nobody. Done when each SKU maps to exactly one group whose members are all users.

3
Set usage location before you grant, not after.

Under group-based licensing, users without a location inherit the location of the tenant. That is not a failure you get told about — it is a successful assignment of the wrong service set to someone in another country, and it is why Microsoft recommends setting location as part of user creation. Done when every member of the group has an explicit usage location.

4
Grant through the group — and treat it as asynchronous.

The two APIs return different things for a reason. POST /users/{id}/assignLicense answers 200 OK with the updated user. POST /groups/{id}/assignLicense answers 202 Accepted: the request was taken, the work happens afterwards. Done when the user's licenseAssignmentStates shows an entry carrying the group's id with state: Active — not when the call returned.

5
Only then remove the direct assignment.

Microsoft's own guidance for moving users between licensed groups is add first, confirm, remove second — because doing it the other way leaves the user unlicensed until processing finishes, and "processing time varies based on tenant size and current load". The direct-to-group move has exactly the same shape. Done when no user has a null assignedByGroup entry for that SKU and consumedUnits has not moved.

The five ways it fails, in the payload

Group assignment can fail per user, long after the API said 202. When it does, the failure is a value in the error property of that user's assignment state — five named ones plus Other. They are worth knowing by name, because the admin center's Errors & issues tab is where they surface and the name is the fix.

error What it means in practice
CountViolation The group is bigger than the number of seats you own. Membership is now the thing that buys licences — the group grew and the subscription didn't.
MutuallyExclusiveViolation Conflicting service plans — typically a user who already holds a SKU containing the same plan, often the direct one you were about to remove.
DependencyViolation A plan needs another plan that this assignment disables. Common when the group's assignment turns services off that the direct one left on.
ProhibitedInUsageLocationViolation Step 3 didn't happen. The service isn't available in the location the user ended up with — possibly the tenant's, inherited.
UniquenessViolation A duplicate proxy address blocks provisioning. Nothing to do with groups; the group assignment is just what finally tripped over it.

Note the second one. MutuallyExclusiveViolation and DependencyViolation are the two you are most likely to create during the migration, in the window where a user legitimately holds both assignments. That window is fine — it just needs to be short and deliberate rather than the permanent state of a third of your tenant.

Why it's worth the afternoon

Direct assignment is not wrong. It is undocumented. The seat exists because someone clicked, and the reason left with them — which is the same mechanism behind licensed accounts nobody signs into. Group-based assignment replaces the click with a membership rule you can read, review and revoke, and it makes deprovisioning a single removal instead of a checklist.

The catch is that neither the admin center nor consumedUnits will ever show you the split, so "are we clean?" is a question you can only answer by walking licenseAssignmentStates for every user. GraphPaaS reads that per tenant on a schedule and keeps it, so the "both" bucket is a number you watch go to zero rather than a script someone runs once and never again.

Sources
  • licenseAssignmentState resource type — that assignedByGroup is null for direct assignments and holds the group id otherwise, plus the full state and error enumerations.
  • Assign or unassign licenses to a group — nested groups only licensing first-level members, location inherited from the tenant, and the add-confirm-remove order with its unlicensed-gap warning.
  • group: assignLicense — licences on a group flow to all its users, and the call returns 202 Accepted rather than a finished result.
  • user: assignLicense — direct licensing as the stated alternative to group-based, returning 200 OK with the updated user.
  • subscribedSku resource type — consumedUnits as a flat count of assigned licences, with no property distinguishing how they were assigned.
GraphPaaS

Every licence in your tenant, with the one thing the seat count leaves out: how it got there.

See your licences →

Newsletter

Get the next article by email

Practical M365 & Azure cost pieces like this one. No spam, unsubscribe anytime.