Remove Copilot Licenses in Bulk With PowerShell: Direct and Group Assignments
🔍 WiseChecker

Remove Copilot Licenses in Bulk With PowerShell: Direct and Group Assignments

Official documentation checked October 14, 2026.

Bulk license removal should target an approved user list and one verified SKU. A product-catalog identifier is not a Microsoft Graph SKU GUID, and removing a direct assignment does not remove an inherited group assignment. This workflow separates inventory, preview, execution, and verification so that a typo does not remove unrelated services.

ADVERTISEMENT

1. Verify the tenant, SKU, and authorization

Use the current Microsoft Graph PowerShell modules in an approved administrative session. For inventory, request the appropriate read permissions, such as User.Read.All and LicenseAssignment.Read.All, and verify the account and tenant with Get-MgContext. For the actual assignment change, Microsoft documents LicenseAssignment.ReadWrite.All plus a supported role such as License Administrator. Broad User.ReadWrite.All is not inherently required. See assignLicense permissions.

Get-MgSubscribedSku -All | Select-Object SkuPartNumber, SkuId
Get-MgContext | Select-Object Account, TenantId, Scopes

Match the intended commercial product to its returned SkuPartNumber and SkuId. Do not choose every result containing “Copilot” or paste a store product code such as CFQ7TTC0LG0Z into a GUID field. Microsoft’s subscription inventory reference links the product-name mapping.

2. Build a direct-assignment-only plan

Create an approved users.csv with the header UserPrincipalName, then one exact work-account UPN per row. Keep it in a restricted working folder. Set the SKU to the verified GUID from your tenant; the placeholder below intentionally cannot run unchanged.

$copilotSkuId = [guid]'REPLACE_WITH_VERIFIED_SKU_GUID'
$rows = @(Import-Csv -LiteralPath '.\users.csv')
if (-not $rows.Count -or
    'UserPrincipalName' -notin $rows[0].PSObject.Properties.Name) {
    throw 'CSV must contain a UserPrincipalName column and users.'
}
$upns = @($rows.UserPrincipalName | ForEach-Object {
    if ([string]::IsNullOrWhiteSpace($_)) { throw 'Blank UPN.' }
    $_.Trim()
} | Sort-Object -Unique)
$plan = foreach ($upn in $upns) {
    $u = Get-MgUser -UserId $upn -Property Id,UserPrincipalName,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
    $states = @($u.LicenseAssignmentStates | Where-Object {
        $_.SkuId -eq $copilotSkuId
    })
    $directOnly = $states.Count -eq 1 -and
        [string]::IsNullOrEmpty($states[0].AssignedByGroup) -and
        $states[0].State -eq 'Active'
    [pscustomobject]@{
        UserId = $u.Id
        UserPrincipalName = $u.UserPrincipalName
        SkuId = $copilotSkuId
        DirectOnly = $directOnly
        BeforeLicenses = $u.AssignedLicenses
        AssignmentStates = $u.LicenseAssignmentStates
    }
}
$plan | Select-Object UserId,UserPrincipalName,SkuId,DirectOnly
$plan | ConvertTo-Json -Depth 8 |
    Out-File -LiteralPath '.\copilot-removal-before.json' -Encoding utf8 -NoClobber -ErrorAction Stop

Review every excluded row. The assignedByGroup field is null for a direct assignment and contains a group ID for an inherited one. This conservative plan excludes mixed, group-based, missing, and error-state assignments. See licenseAssignmentState.

ADVERTISEMENT

3. Preview first, then execute an approved small batch

Confirm the saved baseline and intended user count before using the following preview. It rereads assignment state and stops if the user’s eligibility has changed:

foreach ($row in @($plan | Where-Object DirectOnly)) {
    $u = Get-MgUser -UserId $row.UserId -Property LicenseAssignmentStates -ErrorAction Stop
    $states = @($u.LicenseAssignmentStates | Where-Object {
        $_.SkuId -eq $copilotSkuId
    })
    if ($states.Count -ne 1 -or
        -not [string]::IsNullOrEmpty($states[0].AssignedByGroup) -or
        $states[0].State -ne 'Active') {
        throw "Assignment changed for $($row.UserId). Review before continuing."
    }
    Set-MgUserLicense -UserId $row.UserId -AddLicenses @() -RemoveLicenses @($copilotSkuId) -WhatIf -ErrorAction Stop
}

-WhatIf is supported by the current Set-MgUserLicense cmdlet. It is a no-write preview, not proof that a future server request will succeed. Only after approval, required write authorization, and a pilot test should an administrator remove -WhatIf for the reviewed batch. Capture each operation’s outcome; stop and investigate errors instead of printing unconditional success.

4. Handle inherited licenses and verify all results

For group-derived access, identify every assigning group and review its licensing design. Removing a user from a group may also remove file, app, or other license access. Removing a SKU from the group affects its eligible members, not just the CSV. Do not automate either action from this direct-assignment script. Microsoft’s group-licensing examples explain the separate assignment path.

Re-read every changed user’s assignments and compare with the baseline: the intended SKU should be absent as expected, and unrelated licenses and disabled-plan settings should remain unchanged. Track processing delays and failures separately. License unassignment does not itself cancel a purchased subscription or establish a refund. Do not schedule recurring removals until the eligibility rule, approval process, audit record, and recovery procedure are independently established.

ADVERTISEMENT