Tenant-Wide License Migration Using Graph PowerShell

Bulk Upgrade All ENTERPRISEPACK Users to ENTERPRISEPREMIUM

License upgrades are common during: i) Security posture improvements, ii) Feature enablement initiatives, iii) Tenant-wide modernization and iv) Microsoft 365 E5 adoption programs.

Manually upgrading users from Office 365 E3 (ENTERPRISEPACK) to Office 365 E5 (ENTERPRISEPREMIUM) through the admin center is inefficient and error-prone — especially in larger tenants.

This guide demonstrates how to: i) Identify all users assigned E3, ii) Validate E5 availability, iii) Remove E3 license, iv) Assign E5 license, v) Skip users already on E5 and vi) Export a structured migration report.

This script upgrades license from:

  • E3 (ENTERPRISEPACK)
    • SkuId: 6fd2c87f-b296-42f0-b197-1e91e994b900
  • E5 (ENTERPRISEPREMIUM)
    • SkuId: 06ebc4ee-1bb5-47dd-8120-11324bc54e06

🚀 Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.

I) The Script

⚠ Ensure the correct SkuPartNumber values are set for your tenant before execution.


# Connect to Microsoft Graph
Connect-MgGraph -Scopes User.ReadWrite.All, Organization.Read.All

# =========================
# CONFIG (EDIT THESE)
# =========================
# Common examples:
# Office 365 E3 = ENTERPRISEPACK
# Office 365 E5 = ENTERPRISEPREMIUM
# Microsoft 365 E3 = SPE_E3 (often)
# Microsoft 365 E5 = SPE_E5 (often)
$SourceSkuPartNumber = "ENTERPRISEPACK"       # E3
$TargetSkuPartNumber = "ENTERPRISEPREMIUM"    # E5

# Report path
$ReportPath = "C:\Path\E3_to_E5_MigrationReport.csv"

# =========================
# Resolve SKU IDs
# =========================
$SubscribedSkus = Get-MgSubscribedSku

$SourceSku = $SubscribedSkus | Where-Object { $_.SkuPartNumber -eq $SourceSkuPartNumber }
$TargetSku = $SubscribedSkus | Where-Object { $_.SkuPartNumber -eq $TargetSkuPartNumber }

if (-not $SourceSku) {
    Write-Host "Source SKU not found: $SourceSkuPartNumber" -ForegroundColor Red
    break
}
if (-not $TargetSku) {
    Write-Host "Target SKU not found: $TargetSkuPartNumber" -ForegroundColor Red
    break
}

$SourceSkuId = $SourceSku.SkuId
$TargetSkuId = $TargetSku.SkuId

Write-Host "Source: $SourceSkuPartNumber ($SourceSkuId)" -ForegroundColor Cyan
Write-Host "Target: $TargetSkuPartNumber ($TargetSkuId)" -ForegroundColor Cyan

# =========================
# Check target availability
# =========================
$AvailableTarget = $TargetSku.PrepaidUnits.Enabled - $TargetSku.ConsumedUnits
Write-Host "Available target licenses ($TargetSkuPartNumber): $AvailableTarget" -ForegroundColor Cyan

if ($AvailableTarget -le 0) {
    Write-Host "No available $TargetSkuPartNumber licenses. Exiting." -ForegroundColor Red
    break
}

# =========================
# Fetch all users & filter to E3 users
# =========================
Write-Host "Fetching users (this may take time in large tenants)..." -ForegroundColor Cyan
$Users = Get-MgUser -All -Property Id,UserPrincipalName,DisplayName,AssignedLicenses,AccountEnabled

$E3Users = $Users | Where-Object { $_.AssignedLicenses.SkuId -contains $SourceSkuId }

if (-not $E3Users) {
    Write-Host "No users found with $SourceSkuPartNumber assigned." -ForegroundColor Yellow
    break
}

Write-Host "Users with $SourceSkuPartNumber: $($E3Users.Count)" -ForegroundColor Cyan

# =========================
# Migrate
# =========================
$Results = @()

foreach ($User in $E3Users) {

    try {
        # Skip if already has target
        if ($User.AssignedLicenses.SkuId -contains $TargetSkuId) {

            $Results += [PSCustomObject]@{
                UserPrincipalName = $User.UserPrincipalName
                Status            = "Skipped - Already Has Target"
                SourceRemoved     = $SourceSkuPartNumber
                TargetAdded       = $TargetSkuPartNumber
                Timestamp         = (Get-Date)
            }
            continue
        }

        # Re-check availability during loop
        if ($AvailableTarget -le 0) {
            $Results += [PSCustomObject]@{
                UserPrincipalName = $User.UserPrincipalName
                Status            = "Stopped - No Target Licenses Left"
                SourceRemoved     = $SourceSkuPartNumber
                TargetAdded       = $TargetSkuPartNumber
                Timestamp         = (Get-Date)
            }
            break
        }

        # Switch: remove source, add target
        Set-MgUserLicense -UserId $User.Id `
            -AddLicenses @(
                @{
                    SkuId = $TargetSkuId
                }
            ) `
            -RemoveLicenses @($SourceSkuId)

        $AvailableTarget--

        $Results += [PSCustomObject]@{
            UserPrincipalName = $User.UserPrincipalName
            Status            = "Success"
            SourceRemoved     = $SourceSkuPartNumber
            TargetAdded       = $TargetSkuPartNumber
            Timestamp         = (Get-Date)
        }
    }
    catch {
        $Results += [PSCustomObject]@{
            UserPrincipalName = $User.UserPrincipalName
            Status            = "Failed"
            ErrorMessage      = $_.Exception.Message
            SourceRemoved     = $SourceSkuPartNumber
            TargetAdded       = $TargetSkuPartNumber
            Timestamp         = (Get-Date)
        }
    }
}

# =========================
# Export report
# =========================
$Results | Export-Csv $ReportPath -NoTypeInformation
Write-Host "Migration report exported to $ReportPath" -ForegroundColor Green
                            

II) How the Script Works

  1. Dynamically Resolves SKU IDs
  2. Instead of hardcoding GUIDs, the script resolves:

    Get-MgSubscribedSku

    This ensures portability across tenants.

  3. Validates License Availability
  4. Before migration:

    $AvailableTarget = Enabled - Consumed

    Prevents LicenseLimitExceeded errors.

  5. Identifies All E3 Users
  6. Filters users whose AssignedLicenses contain the E3 SKU.

  7. Performs Safe Migration
  8. For each user:

    • Remove E3
    • Add E5
    • Skip if already migrated
    • Stop if target licenses run out

III) Further Enhancements

This migration engine can be extended to:

  • Preserve service plan customizations
  • Add WhatIf (dry-run) mode
  • Migrate by department
  • Migrate via CSV
  • Generate pre-migration impact report
  • Calculate license cost difference

IV) Possible Errors & Solutions

Error Cause Solution
Source SKU Not Found Entered SKU Id might be wrong. Check and correct the same. Verify:
Get-MgSubscribedSku | Select SkuPartNumber
No Available Target Licenses All Target licenses available in your tenant have been consumed. Purchase additional E5 licenses.
Insufficient Privileges Grap API Permissions to query license info not available. Reconnect with:
Connect-MgGraph -Scopes User.ReadWrite.All, Organization.Read.All
Ensure proper admin role and also ensure super admin has consented to these API permissions.

V) Conclusion

Tenant-wide license migration requires: Planning → Validation → Controlled Execution → Reporting This script provides a structured, automation-driven approach for large-scale E3 → E5 upgrades. It eliminates manual errors and ensures governance compliance.

Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                            


                            


                            

© Created and Maintained by LEARNIT WELL SOLUTIONS. All Rights Reserved.