📋 Guide Info

12 min read

Updated February 17, 2026

1,200 reads

Microsoft 365PowerShellAutomationGraph APICSVOnboardingOffboarding

Automate Microsoft 365 User Onboarding & Offboarding with PowerShell

Liladhar Sapkota - Author
Liladhar SapkotaFebruary 17, 2026

📌 What You'll Learn in This Guide

If you're still creating users one by one in the Microsoft 365 admin center, you're wasting hours every month. This guide shows you how to automate Microsoft 365 user onboarding using PowerShell and Microsoft Graph.

✅ Onboarding Automation
  • Bulk create users from CSV - 50+ users in 2 minutes
  • Automate license assignment - Business Premium, E3, etc.
  • Add users to security groups automatically
  • Set manager relationship via UPN
  • Complete user profile (title, department, location)
✅ Offboarding Automation
  • Block sign-in immediately
  • Revoke all sessions - kills active logins
  • Remove all licenses to free up seats
  • Set mailbox forwarding to manager/HR
  • Mark user as "Ex-Staff" for reporting
Who this is for: IT support, sysadmins, and anyone who manages M365 users. You don't need to be a PowerShell expert - just copy, paste, and run.

⚡ Why Automate Microsoft 365 User Management?

Manual user creation is error-prone and slow. Here's what automating M365 onboarding and offboarding gives you:

⏱️ Save 5+ hours per week

Stop clicking through the admin center. A CSV file and one script does all the work.

✅ No missed steps

Every user gets licenses, groups, and manager assigned. No "I forgot to add them to the distribution list."

🔒 Secure offboarding

Block, revoke, remove licenses, and set forwarding - all in one script. No former employees left with access.

📊 Audit trail

CSV files show exactly who was onboarded/offboarded and when. Great for compliance.

Pro tip: These scripts use Microsoft Graph PowerShell (not the deprecated MSOnline module). Graph is the future - all new Microsoft 365 automation should use it.

1 Install Required PowerShell Modules

Run these commands once on your admin machine. Open PowerShell as Administrator.

# Microsoft Graph module - for users, groups, licenses (replaces MSOL)
Install-Module Microsoft.Graph -Scope AllUsers -Force

# Exchange Online module - for mailbox forwarding during offboarding
Install-Module ExchangeOnlineManagement -Scope AllUsers -Force

If you get a policy error, run: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

Why Microsoft Graph? The old MSOnline and AzureAD modules are deprecated. Microsoft Graph is the only supported way to automate M365 now.

2 Connect to Microsoft Graph

Run this command. Sign in with a Global Admin or User Admin account when prompted.

Connect-MgGraph -Scopes `
  "User.ReadWrite.All",
  "Directory.Read.All",
  "Group.ReadWrite.All",
  "Directory.ReadWrite.All"

You only need to consent once. After that, just Connect-MgGraph works.

Scopes explained: These permissions let you create users, read directory info, and manage groups. You're granting the script permission to do its job.

3 Create Your CSV Template for Bulk User Creation

Save this as C:\Scripts\onboard.csv. Open in Excel, fill in your users. This is your bulk user import file.

displayName,userPrincipalName,passwordProfile,givenName,surname,jobTitle,department,managerUPN,usageLocation,streetAddress,city,state,country,postalCode,officeLocation,mobilePhone
"Raju Pariyar","raju.pariyar@yourtenant.com","TempP@ssw0rd!","Raju","Pariyar","Support Engineer","IT","manager@yourtenant.com","AU","123 George St","Sydney","NSW","AU","2000","Sydney Office","0400000000"
"Anita Sharma","anita.sharma@yourtenant.com","TempP@ssw0rd!","Anita","Sharma","Accountant","Finance","financemgr@yourtenant.com","AU","456 Collins St","Melbourne","VIC","AU","3000","Melbourne Office","0411111111"
Column Description Example
displayNameFull nameRaju Pariyar
userPrincipalNameLogin email (must be unique)raju.pariyar@yourtenant.com
passwordProfileTemporary passwordTempP@ssw0rd!
givenNameFirst nameRaju
surnameLast namePariyar
jobTitleJob titleSupport Engineer
departmentDepartmentIT
managerUPNManager's email (must exist)manager@yourtenant.com
usageLocationCountry code (ISO)AU, US, GB
⚠️ Important: ManagerUPN must be an existing user in your tenant. The script will fail if the manager doesn't exist.

4 PowerShell Onboarding Script (Bulk Create Users)

This script reads your CSV and creates all users. It handles:

  • Bulk user creation from CSV - creates accounts with all profile details
  • Automated license assignment - finds and assigns the right license SKU
  • Add users to security groups - you can customize the group list
  • Set manager relationship - links new user to their manager

Save this as C:\Scripts\onboard.ps1

📋 Click to see the full onboarding script
<#
.SYNOPSIS
    Microsoft 365 User Onboarding Script - Bulk create users from CSV
.DESCRIPTION
    Reads user data from CSV and creates users in Microsoft 365 using Microsoft Graph.
    Handles: user creation, license assignment, group membership, manager assignment.
.PARAMETER CsvPath
    Path to the CSV file containing user data
.EXAMPLE
    .onboard.ps1 -CsvPath "C:Scriptsonboard.csv"
#>

param(
    [Parameter(Mandatory=$true)]
    [string]$CsvPath,

    [string[]]$DefaultGroups = @("All Staff", "M365 Licensed Users")  # Change these to your groups
)

# Helper function to clean CSV values
function Trim-OrNull {
    param([object]$Value)
    if ($null -eq $Value) { return $null }
    $s = "$Value".Trim()
    if ([string]::IsNullOrWhiteSpace($s)) { return $null }
    return $s
}

# Normalize country codes
function Normalise-UsageLocation {
    param([string]$Location)
    if (-not $Location) { return $null }
    $loc = $Location.Trim().ToUpper()
    if ($loc.Length -eq 2) { return $loc }
    
    # Handle common full names
    switch ($loc) {
        "AUSTRALIA" { return "AU" }
        "UNITED STATES" { return "US" }
        "USA" { return "US" }
        "UNITED KINGDOM" { return "GB" }
        "UK" { return "GB" }
        default { return $loc }
    }
}

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "M365 BULK USER ONBOARDING SCRIPT" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# Check if already connected to Graph
try {
    Get-MgContext | Out-Null
    Write-Host "✅ Already connected to Microsoft Graph" -ForegroundColor Green
} catch {
    Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
    Connect-MgGraph -Scopes "User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All" | Out-Null
    Write-Host "✅ Connected" -ForegroundColor Green
}

# Get all available licenses
Write-Host "`n📋 Fetching available licenses..." -ForegroundColor Yellow
$AllSkus = Get-MgSubscribedSku

# Find Business Premium (most common)
$BusinessSku = $AllSkus | Where-Object { 
    $_.SkuPartNumber -match "BUSINESS_PREMIUM|O365_BUSINESS_PREMIUM|STANDARDPACK" 
} | Select-Object -First 1

if (-not $BusinessSku) {
    # Try E3 if Business Premium not found
    $BusinessSku = $AllSkus | Where-Object { 
        $_.SkuPartNumber -match "ENTERPRISEPACK" 
    } | Select-Object -First 1
}

if ($BusinessSku) {
    $BusinessSkuId = $BusinessSku.SkuId
    Write-Host "✅ Found license: $($BusinessSku.SkuPartNumber)" -ForegroundColor Green
    Write-Host "   SKU ID: $BusinessSkuId" -ForegroundColor Gray
} else {
    Write-Host "❌ Could not find any license. Check your tenant." -ForegroundColor Red
    $BusinessSkuId = $null
}

# Resolve group IDs from display names
Write-Host "`n👥 Resolving group IDs..." -ForegroundColor Yellow
$GroupIdMap = @{}

foreach ($groupName in $DefaultGroups) {
    $group = Get-MgGroup -Filter "displayName eq '$groupName'" -ErrorAction SilentlyContinue
    if (-not $group) {
        # Try search if filter fails
        $group = Get-MgGroup -Search "`"displayName:$groupName`"" -ConsistencyLevel eventual -ErrorAction SilentlyContinue
    }
    
    if ($group) {
        $GroupIdMap[$groupName] = $group[0].Id
        Write-Host "✅ Found group: $groupName" -ForegroundColor Green
    } else {
        Write-Host "⚠️ Group not found: $groupName - will skip" -ForegroundColor Yellow
    }
}

# Load CSV
if (-not (Test-Path $CsvPath)) {
    Write-Host "❌ CSV not found: $CsvPath" -ForegroundColor Red
    exit
}

$Users = Import-Csv $CsvPath
Write-Host "`n📊 Loaded $($Users.Count) user(s) from CSV" -ForegroundColor Green

$successCount = 0
$failCount = 0

# Process each user
foreach ($row in $Users) {
    Write-Host "`n----------------------------------------" -ForegroundColor DarkCyan
    Write-Host "Processing: $($row.userPrincipalName)" -ForegroundColor Cyan
    
    # Get and clean all fields
    $upn = Trim-OrNull $row.userPrincipalName
    $password = Trim-OrNull $row.passwordProfile
    $displayName = Trim-OrNull $row.displayName
    $givenName = Trim-OrNull $row.givenName
    $surname = Trim-OrNull $row.surname
    $jobTitle = Trim-OrNull $row.jobTitle
    $department = Trim-OrNull $row.department
    $companyName = Trim-OrNull $row.companyName
    $managerUPN = Trim-OrNull $row.managerUPN
    $usageLoc = Normalise-UsageLocation (Trim-OrNull $row.usageLocation)
    
    # Address fields
    $street = Trim-OrNull $row.streetAddress
    $city = Trim-OrNull $row.city
    $state = Trim-OrNull $row.state
    $country = Trim-OrNull $row.country
    $postalCode = Trim-OrNull $row.postalCode
    $officeLoc = Trim-OrNull $row.officeLocation
    $mobile = Trim-OrNull $row.mobilePhone
    $phone = Trim-OrNull $row.telephoneNumber

    # Validate required fields
    if (-not $upn -or -not $password) {
        Write-Host "❌ Skipping - missing UPN or password" -ForegroundColor Red
        $failCount++
        continue
    }

    if (-not $displayName) {
        $displayName = "$givenName $surname".Trim()
    }

    # Build user object
    $userBody = @{
        accountEnabled = $true
        displayName = $displayName
        mailNickname = ($upn.Split("@")[0])
        userPrincipalName = $upn
        givenName = $givenName
        surname = $surname
        jobTitle = $jobTitle
        department = $department
        companyName = $companyName
        streetAddress = $street
        city = $city
        state = $state
        country = $country
        postalCode = $postalCode
        officeLocation = $officeLoc
        usageLocation = $usageLoc
        passwordProfile = @{
            password = $password
            forceChangePasswordNextSignIn = $true
        }
    }

    # Add optional fields
    if ($mobile) { $userBody["mobilePhone"] = $mobile }
    if ($phone) { $userBody["businessPhones"] = @($phone) }

    # Remove null values
    $cleanBody = @{}
    foreach ($key in $userBody.Keys) {
        if ($null -ne $userBody[$key]) {
            $cleanBody[$key] = $userBody[$key]
        }
    }

    # Create user
    try {
        $newUser = Invoke-MgGraphRequest -Method POST `
            -Uri "https://graph.microsoft.com/v1.0/users" `
            -Body ($cleanBody | ConvertTo-Json -Depth 5) `
            -ContentType "application/json" `
            -ErrorAction Stop
        
        $newUserId = $newUser.id
        Write-Host "✅ User created successfully" -ForegroundColor Green

        # Assign license
        if ($BusinessSkuId -and $usageLoc) {
            try {
                Set-MgUserLicense -UserId $newUserId -AddLicenses @(@{SkuId = $BusinessSkuId}) -RemoveLicenses @() -ErrorAction Stop
                Write-Host "✅ License assigned" -ForegroundColor Green
            } catch {
                Write-Host "⚠️ License assignment failed: $_" -ForegroundColor Yellow
            }
        } else {
            if (-not $usageLoc) {
                Write-Host "⚠️ Usage location missing - license not assigned" -ForegroundColor Yellow
            }
        }

        # Add to groups
        foreach ($groupName in $GroupIdMap.Keys) {
            $groupId = $GroupIdMap[$groupName]
            $body = @{
                "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$newUserId"
            } | ConvertTo-Json

            $uri = "https://graph.microsoft.com/v1.0/groups/$groupId/members/`$ref"
            
            try {
                Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ContentType "application/json" -ErrorAction Stop
                Write-Host "✅ Added to group: $groupName" -ForegroundColor Green
            } catch {
                Write-Host "⚠️ Failed to add to $groupName : $_" -ForegroundColor Yellow
            }
        }

        # Set manager
        if ($managerUPN) {
            try {
                $manager = Get-MgUser -UserId $managerUPN -ErrorAction Stop
                $managerBody = @{
                    "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($manager.Id)"
                }
                Set-MgUserManagerByRef -UserId $newUserId -BodyParameter $managerBody -ErrorAction Stop
                Write-Host "✅ Manager set: $managerUPN" -ForegroundColor Green
            } catch {
                Write-Host "⚠️ Failed to set manager: $_" -ForegroundColor Yellow
            }
        }

        $successCount++
        
    } catch {
        Write-Host "❌ Failed to create user: $_" -ForegroundColor Red
        $failCount++
    }
}

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "ONBOARDING COMPLETE" -ForegroundColor Cyan
Write-Host "✅ Successful: $successCount" -ForegroundColor Green
Write-Host "❌ Failed: $failCount" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan

CSV format: Run the script: .\onboard.ps1 -CsvPath "C:\Scripts\onboard.csv"

Pro tip: Test with one user first. Once it works, add all users to your CSV and run it again. The script is idempotent - it won't duplicate users if UPNs are unique.

💰 Microsoft 365 License SKU Reference

When automating license assignment, you need the correct SKU IDs. Here are the most common ones:

License Name SKU Part Number SKU ID (example)
Microsoft 365 Business PremiumM365_BUSINESS_PREMIUMf245ecc8-75af-4f8e-b61f-27d8114de5f3
Microsoft 365 Business BasicO365_BUSINESS_ESSENTIALS3b555118-da6a-4418-894f-7df1e2096870
Microsoft 365 E3ENTERPRISEPACK6fd2c87f-b296-42f0-b197-1e91e994b900
Microsoft 365 E5ENTERPRISEPREMIUMc7df2760-2c81-4ef7-b578-5b5392b571df
Office 365 E1STANDARDPACK18181a46-0d4e-45cd-891e-60aabd171b4e
Power Automate FreeFLOW_FREEf30db892-98e9-47e5-835c-8fdb9cbd1b6a

Your actual SKU IDs will be different. Run Get-MgSubscribedSku | Format-Table SkuPartNumber, SkuId to see yours.

5 PowerShell Offboarding Script (Automated Exit)

When someone leaves, you need to do all this consistently:

🚫 Block sign-in
🔄 Revoke sessions
💰 Remove licenses
📧 Set forwarding
🏷️ Mark as ex-staff

CSV format: Save as C:\Scripts\offboard.csv

userPrincipalName,ForwardTo
raju.pariyar@yourtenant.com,manager@yourtenant.com
anita.sharma@yourtenant.com,hr@yourtenant.com
📋 Click to see offboarding script
<#
.SYNOPSIS
    Microsoft 365 User Offboarding Script
.DESCRIPTION
    Automates the entire offboarding process: block sign-in, revoke sessions, remove licenses, set forwarding.
.PARAMETER CsvPath
    Path to CSV with userPrincipalName and ForwardTo columns
.EXAMPLE
    .offboard.ps1 -CsvPath "C:Scriptsoffboard.csv"
#>

param(
    [Parameter(Mandatory=$true)]
    [string]$CsvPath
)

function Trim-OrNull {
    param([object]$Value)
    if ($null -eq $Value) { return $null }
    $s = "$Value".Trim()
    if ([string]::IsNullOrWhiteSpace($s)) { return $null }
    return $s
}

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "M365 BULK OFFBOARDING SCRIPT" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# Check CSV
if (-not (Test-Path $CsvPath)) {
    Write-Host "❌ CSV not found: $CsvPath" -ForegroundColor Red
    exit
}

$Users = Import-Csv $CsvPath
Write-Host "📊 Loaded $($Users.Count) user(s) from CSV" -ForegroundColor Green

# Connect to Graph
Write-Host "`n🔌 Connecting to Microsoft Graph..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All" | Out-Null
Write-Host "✅ Connected" -ForegroundColor Green

# Try Exchange Online connection
try {
    Write-Host "`n📧 Connecting to Exchange Online..." -ForegroundColor Yellow
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
    $ExchangeConnected = $true
    Write-Host "✅ Connected to Exchange Online" -ForegroundColor Green
} catch {
    $ExchangeConnected = $false
    Write-Host "⚠️ Exchange connection failed - mailbox forwarding will be skipped" -ForegroundColor Yellow
}

$successCount = 0
$failCount = 0

foreach ($row in $Users) {
    $userUpn = Trim-OrNull $row.userPrincipalName
    $forwardTo = Trim-OrNull $row.ForwardTo

    if (-not $userUpn) {
        Write-Host "`n⚠️ Skipping row - missing UPN" -ForegroundColor Yellow
        continue
    }

    Write-Host "`n----------------------------------------" -ForegroundColor DarkCyan
    Write-Host "Offboarding: $userUpn" -ForegroundColor Cyan

    # Find the user
    try {
        $user = Get-MgUser -UserId $userUpn -ErrorAction Stop
        Write-Host "✅ User found: $($user.DisplayName)" -ForegroundColor Green
    } catch {
        Write-Host "❌ User not found in Entra ID" -ForegroundColor Red
        $failCount++
        continue
    }

    # 1. Block sign-in
    try {
        Update-MgUser -UserId $user.Id -AccountEnabled:$false -ErrorAction Stop
        Write-Host "✅ Sign-in blocked" -ForegroundColor Green
    } catch {
        Write-Host "❌ Failed to block sign-in: $_" -ForegroundColor Red
    }

    # 2. Revoke all sessions
    try {
        Invoke-MgInvalidateUserRefreshToken -UserId $user.Id -ErrorAction Stop
        Write-Host "✅ All sessions revoked" -ForegroundColor Green
    } catch {
        Write-Host "❌ Failed to revoke sessions: $_" -ForegroundColor Red
    }

    # 3. Remove all licenses
    try {
        $licenses = Get-MgUserLicenseDetail -UserId $user.Id -ErrorAction Stop
        if ($licenses -and $licenses.Count -gt 0) {
            $skuIds = $licenses.SkuId
            Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses $skuIds -ErrorAction Stop
            Write-Host "✅ All licenses removed ($($skuIds.Count) licenses)" -ForegroundColor Green
        } else {
            Write-Host "ℹ️ No licenses to remove" -ForegroundColor Gray
        }
    } catch {
        Write-Host "❌ Failed to remove licenses: $_" -ForegroundColor Red
    }

    # 4. Set mailbox forwarding (if requested and Exchange connected)
    if ($forwardTo -and $ExchangeConnected) {
        try {
            Set-Mailbox -Identity $userUpn -ForwardingSMTPAddress $forwardTo -DeliverToMailboxAndForward:$false -ErrorAction Stop
            Write-Host "✅ Mailbox forwarding set to: $forwardTo" -ForegroundColor Green
        } catch {
            Write-Host "❌ Failed to set forwarding: $_" -ForegroundColor Red
        }
    } elseif ($forwardTo -and -not $ExchangeConnected) {
        Write-Host "⚠️ Forwarding requested but Exchange not connected - skipping" -ForegroundColor Yellow
    } else {
        Write-Host "ℹ️ No forwarding address provided" -ForegroundColor Gray
    }

    # 5. Mark as ex-staff
    try {
        Update-MgUser -UserId $user.Id -JobTitle "Ex-Staff" -Department "Ex-Staff" -ErrorAction Stop
        Write-Host "✅ User tagged as Ex-Staff" -ForegroundColor Green
    } catch {
        Write-Host "❌ Failed to update job title: $_" -ForegroundColor Red
    }

    $successCount++
    Write-Host "✅ Offboarding completed for $userUpn" -ForegroundColor Green
}

Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "OFFBOARDING COMPLETE" -ForegroundColor Cyan
Write-Host "✅ Successful: $successCount" -ForegroundColor Green
Write-Host "❌ Failed: $failCount" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan

Run it:

.\offboard.ps1 -CsvPath "C:\Scripts\offboard.csv"
⚠️ Critical: Offboarding is irreversible. Test on a test user first to understand exactly what it does.

❓ Frequently Asked Questions

How to automate Microsoft 365 user onboarding with PowerShell?
How to bulk create users in Office 365 from CSV?
What PowerShell module do I need for Microsoft 365 automation?
How to automate license assignment in Microsoft 365?

📚 Related Guides

✅ You now have production-ready automation scripts!
  • Onboard 50+ users in 2 minutes
  • Offboard users completely in 1 minute
  • No missed steps - consistent every time
  • Audit trail via CSV files

📬 Get New PowerShell Scripts in Your Inbox

Real IT automation scripts. No spam. Unsubscribe anytime.

Join 2,500+ IT pros

Liladhar Sapkota - IT Professional
About the Author

Liladhar Sapkota is an IT professional with expertise in Microsoft 365, Intune, and automation. Writing documentation based on real production experience.