5 PowerShell Patterns Every M365 Admin Should Know
1. Always Paginate Graph API Responses
The Microsoft Graph API paginates large result sets. If you don't handle @odata.nextLink, you'll silently miss records and not know it.
function Get-AllGraphResults {
param([string]$Uri)
$results = @()
do {
$response = Invoke-MgGraphRequest -Uri $Uri -Method GET
$results += $response.value
$Uri = $response.'@odata.nextLink'
} while ($Uri)
return $results
}
Pass in your initial endpoint, get back everything. Works for users, groups, devices — any collection endpoint.
2. Use -WhatIf on Destructive Operations
PowerShell's -WhatIf switch is built into every cmdlet that modifies state. Use it when writing and testing bulk operations.
Get-MgUser -Filter "department eq 'Contractors'" |
Remove-MgUser -WhatIf
Run it without -WhatIf only when you're sure. Your future self will thank you.
3. Structured Error Logging
Don't just Write-Error. Write structured error records you can query later.
$errors = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($user in $users) {
try {
Set-MgUser -UserId $user.Id -Department "IT"
} catch {
$errors.Add([PSCustomObject]@{
UserId = $user.Id
Error = $_.Exception.Message
Timestamp = Get-Date -Format o
})
}
}
$errors | Export-Csv ./errors.csv -NoTypeInformation
At the end of a bulk operation, you have a clean CSV of failures to review.
4. Throttle-Aware Retry Logic
The Graph API will throttle you if you hit it too fast. Build retry logic into your scripts from the start.
function Invoke-WithRetry {
param($ScriptBlock, [int]$MaxRetries = 3)
$attempt = 0
while ($attempt -lt $MaxRetries) {
try { return & $ScriptBlock }
catch {
if ($_.Exception.Response.StatusCode -eq 429) {
$wait = [int]($_.Exception.Response.Headers['Retry-After'] ?? 10)
Start-Sleep -Seconds $wait
$attempt++
} else { throw }
}
}
}
5. Module Version Pinning
Microsoft updates the Graph PowerShell SDK constantly. Pin your module versions in production scripts.
#Requires -Modules @{ ModuleName = 'Microsoft.Graph'; ModuleVersion = '2.15.0' }
Add this at the top of every production script. If the wrong module version is loaded, the script stops before doing anything — which is exactly what you want.
These patterns are in every automation script I write. They're not clever — they're just solid. Start with these and build from there.