~/2025/10/22/powershell-intune-force-a-compliance-policy-re-evaluation-fleet-wide.md
PowerShell: Intune – Force Compliance Re-Evaluation Fleet-Wide
--- author: Tom Lasswell date: read: 3 min in: [ps, scripts] tags: [powershell, intune] ---
$ grep -n '^#' post.md
After pushing a new or corrected compliance policy, the last thing I want is to wait out each device's normal check-in, which Microsoft estimates at about every 8 hours. Clicking Sync one device at a time in the Intune console doesn't scale past a handful of machines, and the console's bulk device actions handle up to 100 devices at a time. The same action is exposed through Microsoft Graph as syncDevice on a managed device (POST /deviceManagement/managedDevices/{id}/syncDevice, v1.0), and the Graph PowerShell SDK wraps it as Sync-MgDeviceManagementManagedDevice. This script pulls every managed device matching an OS and name filter, optionally only the ones not currently compliant, sends a sync to each with a delay between calls, and writes a CSV of what happened.
Requirements
- PowerShell 7.x recommended (Windows PowerShell 5.1 works too).
Microsoft.Graph.AuthenticationandMicrosoft.Graph.DeviceManagementmodules (Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement -Scope CurrentUser).- Two delegated Graph permissions (the script signs in interactively with
Connect-MgGraph -Scopes; running it app-only would need a certificate or managed-identity sign-in and the same two as application permissions):DeviceManagementManagedDevices.PrivilegedOperations.All: the only permissionsyncDeviceaccepts.DeviceManagementManagedDevices.Read.All: needed to list devices withGet-MgDeviceManagementManagedDevice. The privileged-operations permission doesn't include read access, and the first version of this script only requested that one, so the device query failed.
- An Intune role for the signed-in identity that allows the remote sync task (Intune Administrator, or a custom role with that remote task).
- An active Intune license on the tenant (a stated requirement of the Intune Graph API).
- Devices need to be online and reachable through their platform's push service to act on the request immediately. An offline device picks it up the next time it checks in.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
-OperatingSystem | String | No | Managed device operatingSystem value to target. Defaults to Windows. |
-DeviceNamePattern | String | No | Wildcard pattern matched against the device name. Defaults to * (every device). |
-OnlyNonCompliant | Switch | No | Skips devices whose complianceState is already compliant. |
-ThrottleMs | Int | No | Delay in milliseconds between sync calls. Defaults to 500. |
-ReportPath | String | No | Path to the CSV report written after the run. Defaults to a timestamped file in the current directory. |
Usage
Preview which devices would be synced without calling the action:
.\Invoke-FleetComplianceResync.ps1 -WhatIf
After fixing a compliance policy, resync only the laptops that are currently failing it:
.\Invoke-FleetComplianceResync.ps1 -DeviceNamePattern "LAPTOP-*" -OnlyNonCompliant -ThrottleMs 750
Sample output:
Fetching managed devices (OS: Windows, name pattern: LAPTOP-*)...
214 device(s) matched, 37 after the compliance filter.
Sync requested for 37 device(s), 1 failed. Report: .\fleet-sync-report-20251022-090512.csv
Look at failures afterwards:
Import-Csv .\fleet-sync-report-20251022-090512.csv | Where-Object { $_.Status -eq "Failed" } | Format-Table DeviceName, Error
Confirm the devices actually checked in. LastSyncDateTime in the report is the value from before the sync, so compare it with a fresh read an hour later:
$before = Import-Csv .\fleet-sync-report-20251022-090512.csv | Where-Object { $_.Status -eq "Synced" }
foreach ($row in $before) {
$now = Get-MgDeviceManagementManagedDevice -ManagedDeviceId $row.DeviceId -Property "deviceName,lastSyncDateTime,complianceState"
[PSCustomObject]@{
DeviceName = $now.DeviceName
SyncedBefore = $row.LastSyncDateTime
SyncedNow = $now.LastSyncDateTime
ComplianceState = $now.ComplianceState
}
}
Script
<#
.SYNOPSIS
Sends the Intune syncDevice action to every matching managed device so
compliance and configuration policy are re-evaluated immediately.
.DESCRIPTION
Connects to Microsoft Graph, lists managed devices that match the operating
system and device name filters (optionally only those not compliant), and
calls Sync-MgDeviceManagementManagedDevice (the v1.0 syncDevice action) on
each one. The sync makes the device check in with Intune now instead of at
its next scheduled check-in. Calls are spaced by -ThrottleMs, a throttled
call (HTTP 429) is retried once after a back-off, and the result of each
call is written to a CSV report. Supports -WhatIf.
.PARAMETER OperatingSystem
Managed device operatingSystem value to target. Defaults to Windows.
.PARAMETER DeviceNamePattern
Wildcard pattern matched against the device name. Defaults to * (every device).
.PARAMETER OnlyNonCompliant
Skips devices whose complianceState is already compliant.
.PARAMETER ThrottleMs
Delay in milliseconds between sync calls. Defaults to 500.
.PARAMETER ReportPath
Path to the CSV report written after the run.
.EXAMPLE
.\Invoke-FleetComplianceResync.ps1 -WhatIf
.EXAMPLE
.\Invoke-FleetComplianceResync.ps1 -DeviceNamePattern "LAPTOP-*" -OnlyNonCompliant -ThrottleMs 750
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-10-22)
Requires: Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement;
DeviceManagementManagedDevices.Read.All and
DeviceManagementManagedDevices.PrivilegedOperations.All.
.LINK
https://learn.microsoft.com/en-us/graph/api/intune-devices-manageddevice-syncdevice?view=graph-rest-1.0
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter()]
[string]$OperatingSystem = "Windows",
[Parameter()]
[string]$DeviceNamePattern = "*",
[Parameter()]
[switch]$OnlyNonCompliant,
[Parameter()]
[int]$ThrottleMs = 500,
[Parameter()]
[string]$ReportPath = ".\fleet-sync-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
)
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.DeviceManagement
Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All", "DeviceManagementManagedDevices.PrivilegedOperations.All" -NoWelcome
Write-Host "Fetching managed devices (OS: $OperatingSystem, name pattern: $DeviceNamePattern)..."
$escapedOs = $OperatingSystem.Replace("'", "''")
$devices = @(Get-MgDeviceManagementManagedDevice -All -Filter "operatingSystem eq '$escapedOs'" -Property "id,deviceName,complianceState,lastSyncDateTime,operatingSystem" |
Where-Object { $_.DeviceName -like $DeviceNamePattern })
$matchedCount = $devices.Count
if ($OnlyNonCompliant) {
$devices = @($devices | Where-Object { $_.ComplianceState -ne "compliant" })
}
Write-Host "$matchedCount device(s) matched, $($devices.Count) after the compliance filter."
$results = foreach ($device in $devices) {
$status = "Skipped"
$errorMessage = $null
if ($PSCmdlet.ShouldProcess($device.DeviceName, "Sync-MgDeviceManagementManagedDevice")) {
$attempt = 0
while ($status -ne "Synced" -and $attempt -lt 2) {
$attempt++
try {
Sync-MgDeviceManagementManagedDevice -ManagedDeviceId $device.Id -ErrorAction Stop
$status = "Synced"
$errorMessage = $null
} catch {
$status = "Failed"
$errorMessage = $_.Exception.Message
if ($errorMessage -match "TooManyRequests|429" -and $attempt -lt 2) {
# Back off once when Graph throttles the call, then retry.
Start-Sleep -Seconds 30
} else {
break
}
}
}
Start-Sleep -Milliseconds $ThrottleMs
}
[PSCustomObject]@{
DeviceName = $device.DeviceName
DeviceId = $device.Id
ComplianceState = $device.ComplianceState
LastSyncDateTime = $device.LastSyncDateTime
Status = $status
Error = $errorMessage
}
}
$results | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
$failed = @($results | Where-Object { $_.Status -eq "Failed" })
Write-Host "Sync requested for $(@($results).Count) device(s), $($failed.Count) failed. Report: $ReportPath"
Notes
- A sync makes the device check in now, and compliance is evaluated at check-in. It doesn't change a device's compliance state by itself: a device that genuinely fails a policy still shows non-compliant after the sync, just sooner than it otherwise would.
- A successful call only means Intune accepted the request (
syncDevicereturns204 No Content). Whether the device checked in shows up later inlastSyncDateTime, which is why the Usage section compares before and after. - Throttling:
syncDeviceis a write against the Intune devices service, which Microsoft documents at 200 POST/PUT/PATCH/DELETE requests per 20 seconds per app per tenant and 400 per tenant across all apps. The default 500 ms spacing works out to 40 calls per 20 seconds, well under that, and leaves room for other tools calling Graph at the same time. If the report shows throttling errors, raise-ThrottleMsbefore assuming anything is wrong with those devices. The script's own single 429 retry is only a backstop: the Graph PowerShell SDK already retries throttled requests itself, honoringRetry-After(Microsoft: the SDKs "already implement handlers that rely on theRetry-Afterheader or default to an exponential backoff retry policy"), up toMaxRetrytimes, 3 by default, whichSet-MgRequestContextcan change. A 429 only reaches the script once those retries are used up. -Propertykeeps the device query small.Get-MgDeviceManagementManagedDeviceotherwise returns every property of every device, which gets slow on a large tenant.- This is for enrolled devices that still communicate. A device that hasn't checked in for weeks needs a different conversation than a resync: look at its last check-in date and your device cleanup rules first.