Skip to content

Automation with Microsoft Graph

Everything in the Intune admin center is built on Microsoft Graph. Anything you can click, you can script: reports, bulk changes, scheduled checks, configuration backups. This page covers how to do it safely.

Graph basics for Intune

  • Endpoint: https://graph.microsoft.com/beta/deviceManagement/... for most Intune resources. Some, like managed devices, are also in v1.0. The beta endpoint is supported for Intune but changes more often, so pin the properties you rely on with $select.
  • PowerShell: the Microsoft Graph PowerShell SDK. For Intune, the Microsoft.Graph.Authentication module plus Invoke-MgGraphRequest is often all you need, and it works on Windows, macOS and Linux with PowerShell 7.
  • Licensing: the Graph API for Intune needs an active Intune license in the tenant.
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All' -NoWelcome

Authentication options

Scenario Use Notes
Admin running a script interactively Delegated permissions with Connect-MgGraph -Scopes ... The script can only do what both the scopes and the admin's own roles allow
Scheduled job in Azure Managed identity (Azure Automation, Functions) No credentials to store or rotate. Grant Graph application permissions to the managed identity's service principal.
Scheduled job elsewhere, or CI/CD App registration with a certificate, or workload identity federation (for example GitHub Actions OIDC) Avoid client secrets in scripts. If you must use one, keep it in a vault and set a short expiry.

Least-privilege permissions

Request only what each script needs. Read-only scripts never need ReadWrite.

Task Permission
Read managed devices and their compliance state DeviceManagementManagedDevices.Read.All
Run remote actions (sync, restart, wipe, retire) DeviceManagementManagedDevices.PrivilegedOperations.All
Read configuration and compliance policies, scripts, reports DeviceManagementConfiguration.Read.All
Change configuration and compliance policies DeviceManagementConfiguration.ReadWrite.All
Read or manage apps DeviceManagementApps.Read.All / DeviceManagementApps.ReadWrite.All
Read enrollment settings, Autopilot devices, Apple tokens DeviceManagementServiceConfig.Read.All
Read Intune roles and assignments DeviceManagementRBAC.Read.All
Read BitLocker key metadata (not the keys) BitlockerKey.ReadBasic.All
Resolve users and groups User.Read.All, GroupMember.Read.All

Patterns every script needs

  • $select only the properties you use. Smaller responses are faster and less likely to be throttled.
  • Filter on the server with $filter where the endpoint supports it.
  • Follow @odata.nextLink until it's empty. Large collections are paged.
  • Handle throttling. On HTTP 429, wait for the Retry-After interval. The Graph PowerShell SDK retries automatically, but tight loops can still hit limits.
  • Batch independent GET requests with $batch, up to 20 per batch.
  • Guard date parsing. Fields like lastSyncDateTime can be empty. Check before you parse.
  • Make destructive scripts safe: support -WhatIf, confirm by default, and log what you changed.

Examples

Find devices that haven't synced recently

Get-StaleIntuneDevices.ps1
#Requires -Modules Microsoft.Graph.Authentication
[CmdletBinding()]
param(
    [Parameter(HelpMessage = 'Days since the last sync')]
    [ValidateRange(1, 365)]
    [int]$DaysInactive = 30,

    [Parameter(HelpMessage = 'Where to write the CSV report')]
    [string]$OutputPath = '.\StaleDevices.csv'
)

Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All' -NoWelcome

try {
    $cutoff = (Get-Date).ToUniversalTime().AddDays(-$DaysInactive)
    $uri = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices' +
           '?$select=id,deviceName,operatingSystem,osVersion,userPrincipalName,lastSyncDateTime,complianceState'

    $devices = [System.Collections.Generic.List[object]]::new()
    do {
        $page = Invoke-MgGraphRequest -Method GET -Uri $uri
        foreach ($item in $page.value) { $devices.Add($item) }
        $uri = $page.'@odata.nextLink'
    } while ($uri)

    $stale = foreach ($device in $devices) {
        # lastSyncDateTime can be empty for devices that never completed a sync.
        $lastSync = if ($device.lastSyncDateTime) { [datetime]$device.lastSyncDateTime } else { $null }
        if ($null -eq $lastSync -or $lastSync -lt $cutoff) {
            [pscustomobject]@{
                DeviceName = $device.deviceName
                OS         = "$($device.operatingSystem) $($device.osVersion)"
                User       = $device.userPrincipalName
                LastSync   = $lastSync
                Compliance = $device.complianceState
            }
        }
    }

    $stale | Sort-Object LastSync | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Information "$(@($stale).Count) of $($devices.Count) devices haven't synced in $DaysInactive days." -InformationAction Continue
}
finally {
    $null = Disconnect-MgGraph -ErrorAction SilentlyContinue
}

Count noncompliant devices by platform

Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All' -NoWelcome

$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices" +
       "?`$filter=complianceState eq 'noncompliant'&`$select=deviceName,operatingSystem,userPrincipalName"

$noncompliant = do {
    $page = Invoke-MgGraphRequest -Method GET -Uri $uri
    $page.value
    $uri = $page.'@odata.nextLink'
} while ($uri)

$noncompliant | Group-Object { $_.operatingSystem } |
    Sort-Object Count -Descending |
    Select-Object Name, Count

Export a report

The exportJobs API returns the same data as the reports in the admin center, as a zipped CSV. Creating an export job needs a ReadWrite permission even though it only reads data: Graph accepts DeviceManagementConfiguration.ReadWrite.All, DeviceManagementApps.ReadWrite.All or DeviceManagementManagedDevices.ReadWrite.All, depending on the report.

Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.ReadWrite.All' -NoWelcome

$body = @{
    reportName = 'Devices'
    format     = 'csv'
    select     = @('DeviceName', 'UPN', 'OS', 'OSVersion', 'complianceState', 'LastContact', 'ownerType')
} | ConvertTo-Json

$job = Invoke-MgGraphRequest -Method POST -Body $body -ContentType 'application/json' `
    -Uri 'https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs'

do {
    Start-Sleep -Seconds 5
    $job = Invoke-MgGraphRequest -Method GET `
        -Uri "https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs('$($job.id)')"
} while ($job.status -in @('notStarted', 'inProgress'))

if ($job.status -eq 'completed') {
    Invoke-WebRequest -Uri $job.url -OutFile '.\DevicesReport.zip'
}
else {
    Write-Error "Export failed with status '$($job.status)'"
}

List Autopilot registrations

Connect-MgGraph -Scopes 'DeviceManagementServiceConfig.Read.All' -NoWelcome

$uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities' +
       '?$select=serialNumber,manufacturer,model,groupTag,enrollmentState,lastContactedDateTime'

$autopilot = do {
    $page = Invoke-MgGraphRequest -Method GET -Uri $uri
    $page.value
    $uri = $page.'@odata.nextLink'
} while ($uri)

$autopilot | Export-Csv -Path '.\AutopilotDevices.csv' -NoTypeInformation -Encoding UTF8

Ready-made scripts

Before you write a script, check whether someone already has. The open-source IntuneAutomation library has around 70 documented, runbook-ready scripts, including:

Script What it does
get-device-checkin-health Finds devices whose check-in pattern is degrading before they go stale
get-noncompliant-devices-with-reasons Lists noncompliant devices and the settings that failed
check-apple-token-validity Reports Apple push certificate and ADE token expiry
check-bitlocker-keys Confirms BitLocker recovery keys are stored in Entra ID

Read and test any script before you run it against production.

Multi Admin Approval and automation

If a resource is protected by Multi Admin Approval, Graph calls that change it are intercepted too, including calls from app-only service principals.

  1. Send the request with an x-msft-approval-justification header containing a Base64-encoded justification. Without it, the request fails.
  2. Graph returns HTTP 412 with an x-msft-approval-code header. That's expected: the request is now waiting for approval.
  3. A human approver in the approver group approves it. Apps can't approve requests.
  4. Your automation resubmits the same request (same method, URL and body) with the approval code to complete it.

Design automation that changes protected resources to save the approval code and resume later, rather than failing.

Back up your configuration as code

Graph also lets you export your whole Intune configuration as JSON for backup, change tracking and comparison:

  • Microsoft365DSC: Microsoft-backed open-source PowerShell DSC module that can export, compare and deploy Intune (and wider Microsoft 365) configuration.
  • IntuneManagement: a popular community tool for exporting, importing and comparing Intune objects through a GUI or the command line.
  • Your own scripts: GET each policy type with its settings and assignments and commit the JSON to Git.

See Change management for how to use backups in practice.

Securing automation

  • One app registration or managed identity per automation, with only the permissions it needs.
  • Prefer managed identities and certificates over client secrets. Track credential expiry dates in the admin calendar.
  • Monitor service principal sign-ins in Entra ID, and alert on unexpected use.
  • Protect high-impact operations with Multi Admin Approval, and give scripts a -WhatIf mode.

Learn more