Blog Clark Grassano — Portfolio
← Back to Blog

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.