🎁 New User? Get 20% off your first purchase with code NEWUSER20 Register Now →
Menu

Categories

Bulk Microsoft 365 User Provisioning with Graph API and CSV (2026)

Bulk Microsoft 365 User Provisioning with Graph API and CSV (2026)
Bulk M365 user provisioning with Graph API guide

Onboarding 200 new hires by hand in the M365 admin center is no one's idea of a good time. With the Microsoft Graph PowerShell SDK, the same job is a 50-line script that reads a CSV, creates each user, assigns the right license, drops them into the correct security and distribution groups, and emails the temporary password to the manager — in under a minute.

This guide is the production version of that script. We will design the CSV, build the user-creation loop, handle license assignment via SKUs, manage group membership, deal with the inevitable failures, and — critically — handle the temporary password without writing it to disk. Free PDF cheat sheet at the bottom.

Prerequisites

  • PowerShell 7.x
  • Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.Groups, Microsoft.Graph.Identity.DirectoryManagement
  • App registration (or interactive admin) with delegated/application scopes: User.ReadWrite.All, Group.ReadWrite.All, Organization.Read.All (for license SKUs)
  • A CSV with the new hires

CSV design

Keep the CSV simple but complete. Every column maps directly to a user property or a downstream action:

FirstName,LastName,UPN,Department,JobTitle,Office,ManagerUPN,License,Groups
Alice,Smith,alice.smith@contoso.com,Engineering,Senior Dev,Berlin,bob@contoso.com,SPE_E3,sg-eng;dl-eng-news
Carol,Jones,carol.jones@contoso.com,Sales,Account Exec,London,dave@contoso.com,SPE_E5,sg-sales;dl-sales-news

Notes on the columns:

  • UPN = User Principal Name. Must use a verified domain in your tenant.
  • License = the SKU part number. SPE_E3 = Microsoft 365 E3, SPE_E5 = E5, EXCHANGESTANDARD = Exchange Online Plan 1, etc. Discover them with Get-MgSubscribedSku | Format-Table SkuPartNumber, ConsumedUnits, PrepaidUnits.
  • Groups = semicolon-separated list of group display names (we will look up the IDs).

Authenticate with the right scopes

Connect-MgGraph -Scopes @(
    "User.ReadWrite.All",
    "Group.ReadWrite.All",
    "Organization.Read.All"
)
Get-MgContext  # confirm

Create the user

function New-RandomTempPassword {
    param([int]$Length = 16)
    -join ((33..126) | Where-Object { $_ -notin 34,39,96 } |
        Get-Random -Count $Length | ForEach-Object { [char]$_ })
}

$user = New-MgUser `
    -AccountEnabled `
    -DisplayName "$($row.FirstName) $($row.LastName)" `
    -UserPrincipalName $row.UPN `
    -MailNickname ($row.UPN -split '@')[0] `
    -GivenName $row.FirstName -Surname $row.LastName `
    -Department $row.Department -JobTitle $row.JobTitle `
    -OfficeLocation $row.Office `
    -PasswordProfile @{
        Password = New-RandomTempPassword
        ForceChangePasswordNextSignIn = $true
    }

ForceChangePasswordNextSignIn = $true guarantees the user resets the temp password on first login. Never store the temp password — pass it directly to the secure delivery mechanism (manager email through your mail flow, Slack via webhook, password manager link).

License assignment

# Look up the SKU once at script start
$skus = Get-MgSubscribedSku
$skuId = ($skus | Where-Object SkuPartNumber -eq $row.License).SkuId

if (-not $skuId) {
    throw "Unknown license SKU: $($row.License)"
}

Set-MgUserLicense -UserId $user.Id -AddLicenses @(@{ SkuId = $skuId }) -RemoveLicenses @()

If your tenant has limits on certain plans, watch ConsumedUnits vs PrepaidUnits on the SKU — assigning beyond the prepaid count fails.

Group membership

# Build a one-time lookup of all groups the CSV mentions
$groupCache = @{}
$row.Groups -split ';' | ForEach-Object {
    $name = $_.Trim()
    if (-not $groupCache.ContainsKey($name)) {
        $g = Get-MgGroup -Filter "displayName eq '$name'"
        if ($g) { $groupCache[$name] = $g.Id }
    }
}

foreach ($g in ($row.Groups -split ';')) {
    $name = $g.Trim()
    if ($groupCache[$name]) {
        New-MgGroupMember -GroupId $groupCache[$name] -DirectoryObjectId $user.Id
    } else {
        Write-Warning "Group not found: $name"
    }
}

Set the manager attribute

if ($row.ManagerUPN) {
    $manager = Get-MgUser -UserId $row.ManagerUPN -ErrorAction SilentlyContinue
    if ($manager) {
        Set-MgUserManagerByRef -UserId $user.Id -BodyParameter @{
            "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($manager.Id)"
        }
    }
}

The full script

[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string]$CsvPath,
    [string]$LogPath = ".\provisioning.log"
)
$ErrorActionPreference = 'Stop'

Connect-MgGraph -Scopes @("User.ReadWrite.All","Group.ReadWrite.All","Organization.Read.All")
$skus = Get-MgSubscribedSku
$rows = Import-Csv $CsvPath

$results = foreach ($r in $rows) {
    try {
        # ... (combine all the steps above) ...
        [PSCustomObject]@{ UPN = $r.UPN; Status = 'created'; Error = '' }
    } catch {
        Write-Warning "FAILED $($r.UPN): $($_.Exception.Message)"
        [PSCustomObject]@{ UPN = $r.UPN; Status = 'failed'; Error = $_.Exception.Message }
    }
}

$results | Export-Csv $LogPath -NoTypeInformation
$results | Group-Object Status | Format-Table -AutoSize
Disconnect-MgGraph

Error handling and idempotency

Two things bite every first run:

  1. UPN already exists. Your script must decide: skip, fail, or update. Add a check at the top of the loop:
$existing = Get-MgUser -Filter "userPrincipalName eq '$($r.UPN)'" -ErrorAction SilentlyContinue
if ($existing) {
    Write-Warning "Skipping $($r.UPN) - already exists"
    continue
}
  1. License capacity exceeded. Add a pre-check before the loop:
$needed = ($rows | Group-Object License | ForEach-Object { @{ Sku = $_.Name; Count = $_.Count } })
foreach ($n in $needed) {
    $sku = $skus | Where-Object SkuPartNumber -eq $n.Sku
    $available = $sku.PrepaidUnits.Enabled - $sku.ConsumedUnits
    if ($n.Count -gt $available) {
        throw "Not enough $($n.Sku) licenses: need $($n.Count), have $available"
    }
}

Cheat sheet

The whole script + license SKU table + scopes on one PDF: M365 Bulk Provisioning Cheat Sheet.

FAQ

How do I find the right SkuPartNumber for my license?

Get-MgSubscribedSku | Format-Table SkuPartNumber, SkuId, ConsumedUnits, @{n='Available';e={$_.PrepaidUnits.Enabled - $_.ConsumedUnits}} lists every SKU your tenant owns, what each is called, and how many seats are free.

Can I assign Group-Based Licensing instead of direct license assignment?

Yes — and it is the recommended pattern at scale. Set the license on a security group via the Entra portal once; this script then only needs to add the user to the group. The license assignment happens automatically.

How do I deliver the temporary password securely?

Either email the manager via Mail.Send (delegated) using a templated message, or write to a one-time-link service. Never write the password to a CSV or log file.

Does the user need a license to be created?

No — a user can exist without a license. They cannot sign in to mailboxes / Teams / OneDrive without one, but the directory object is fine.

What if my tenant uses dynamic groups?

Skip the explicit New-MgGroupMember step for those — Entra ID adds the user automatically once their attributes match the rule. Set the matching attributes (Department, JobTitle, etc.) on the user instead.

Can I use this from Azure Automation?

Yes — switch from interactive Connect-MgGraph to certificate-based application auth, store the cert in the Automation account, and run as a runbook. The script body itself does not change.

What about Exchange-only attributes (alias, mailbox region)?

Most are settable via Graph (mailNickname, preferredDataLocation). A few legacy ones still need the Exchange Online module — combine the two in your script.

Related reading

Share this article:
Dargslan Editorial Team (Dargslan)
About the Author

Dargslan Editorial Team (Dargslan)

Collective of Software Developers, System Administrators, DevOps Engineers, and IT Authors

Dargslan is an independent technology publishing collective formed by experienced software developers, system administrators, and IT specialists.

The Dargslan editorial team works collaboratively to create practical, hands-on technology books focused on real-world use cases. Each publication is developed, reviewed, and...

Programming Languages Linux Administration Web Development Cybersecurity Networking

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.