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 inv1.0. Thebetaendpoint 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.Authenticationmodule plusInvoke-MgGraphRequestis 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¶
$selectonly the properties you use. Smaller responses are faster and less likely to be throttled.- Filter on the server with
$filterwhere the endpoint supports it. - Follow
@odata.nextLinkuntil it's empty. Large collections are paged. - Handle throttling. On HTTP 429, wait for the
Retry-Afterinterval. 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
lastSyncDateTimecan 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¶
#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.
- Send the request with an
x-msft-approval-justificationheader containing a Base64-encoded justification. Without it, the request fails. - Graph returns HTTP 412 with an
x-msft-approval-codeheader. That's expected: the request is now waiting for approval. - A human approver in the approver group approves it. Apps can't approve requests.
- 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
-WhatIfmode.
Learn more¶
- Intune APIs in Microsoft Graph
- Use Microsoft Entra ID to access the Intune APIs
- Microsoft Graph throttling guidance
- Use Multi Admin Approval with the Graph API