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:
Try the M365Corner Microsoft 365 Reporting Tool â your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.
â 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
Instead of hardcoding GUIDs, the script resolves:
Get-MgSubscribedSku
This ensures portability across tenants.
Before migration:
$AvailableTarget = Enabled - Consumed
Prevents LicenseLimitExceeded errors.
Filters users whose AssignedLicenses contain the E3 SKU.
For each user:
This migration engine can be extended to:
| 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. |
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.
© Created and Maintained by LEARNIT WELL SOLUTIONS. All Rights Reserved.