Detect Failed Autopilot Enrollment with PowerShell · CloudEngineerHub
Detection & Remediation9 min read4 views
Detecting Failed Autopilot Enrollment with PowerShell
A PowerShell detection script that pulls Autopilot error events and Enrollment Status Page signals from devices, so you can see exactly where provisioning failed.
C
CloudEngineerHub Team
Published: September 8, 2026· Last Updated: September 28, 2026·9 min read
By the time a failed Autopilot enrollment reaches your queue, someone has usually rebooted the device a couple of times and the OOBE screens are long gone. The best remaining evidence lives on the machine itself, in the event log of the component that ran the deployment: Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Admin. That log records the Autopilot provider's view of the world, from profile processing to enrollment steps, and it is the single most reliable local source when a device stalls partway through provisioning. This article walks through a read-only PowerShell detection script that harvests that log, and deliberately refuses to hard-code interpretations of event IDs. It pulls every error event inside a lookback window, logs the message text for classification, adds a coarse keyword tally as a pattern hint, and then gathers the Enrollment Status Page context signals that explain stuck deployments: pending reboots, ESP tracking state, and Intune Management Extension activity during setup. Deployed through Intune, it turns "the device failed somehow" into a specific, timestamped narrative.
Visual Walkthrough
C
CloudEngineerHub Team
Author
The CloudEngineerHub editorial team — Microsoft endpoint management and cloud engineers writing hands-on guides on Intune, Azure, Entra ID, Autopilot, Defender and PowerShell. Every article is reviewed against current Microsoft documentation.
Exit 1 flips the device into the remediation queue for this package.
remediate
3
Remediation executes
Fix script runs silently as SYSTEM with a 5-minute budget.
verify
4
Re-run detection
The same detection script must now exit 0 — proof, not hope.
telemetry
5
Results report
Per-device outcome lands in Reports → Remediations for trend analysis.
6
Repeat on schedule
The loop continues — drift gets caught the day it appears.
Continuous cycle
Proactive Remediations — The Detect → Fix → Verify Loop. 1. Detection script runs: Scheduled (daily/hourly): exit 0 healthy, exit 1 unhealthy — nothing more. — 2. Verdict: unhealthy: Exit 1 flips the device into the remediation queue for this package. — 3. Remediation executes: Fix script runs silently as SYSTEM with a 5-minute budget. — 4. Re-run detection: The same detection script must now exit 0 — proof, not hope. — 5. Results report: Per-device outcome lands in Reports → Remediations for trend analysis. — 6. Repeat on schedule: The loop continues — drift gets caught the day it appears.
Symptoms
Failed Autopilot enrollments produce a recognizable set of complaints from the field:
The device hangs on "Preparing your device for mobile management" or another ESP phase until the technician gives up.
ESP progress reaches the apps or policies stage and stays there until timeout, then shows a failure page with retry options.
The device reboots during OOBE and comes back to the start of setup, repeatedly.
The device completes OOBE but enrollment never finishes, so it never appears in the Intune admin center.
The admin center shows the device with an Autopilot deployment failed or timed-out status for its most recent attempt.
Possible Causes
Profile download or processing failed
The device reached the Autopilot deployment service but could not download or apply the profile, commonly because of network interception during OOBE or a transient service error. The provider log records the failure with message text you can read directly.
Enrollment failed after the profile applied
TPM attestation problems, Entra join errors, or MDM enrollment failures happen after profile processing. The device has the profile but never completes the management handshake, which the provider log also records.
A pending reboot blocked the ESP
If servicing leaves a reboot pending while the device is trying to complete ESP, installation phases cannot finish. The device appears to hang, and the underlying cause is a servicing operation, not Autopilot itself.
App or policy installation stalled inside ESP
ESP waits for assigned apps and policies. A Win32 app that installs forever, or a policy the device cannot process, holds the page open until timeout. The IME's own logs tell that story in parallel with the provider log.
The device was not actually Autopilot-assigned
Sometimes nothing failed: the device simply was never in scope, so there are no provider events, no ESP state, and no provisioning artifacts in the lookback window. Detecting that absence is just as useful as detecting errors.
Detection Script
The script is read-only and completes in seconds. It runs in 64-bit SYSTEM context with a 7-day default lookback window, first harvesting error events from the Autopilot diagnostics provider, then collecting ESP context: provider activity of any level, ESP tracking registry state, IME presence and recent log activity, provisioning artifacts, and the three standard pending-reboot markers.
PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
# Lookback window (days) for Autopilot provider events.
[ValidateRange(1, 90)]
[int] $ErrorWindowDays = 7
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Detection script: finds evidence of failed or stuck Windows Autopilot
enrollment and Enrollment Status Page (ESP) activity.
.DESCRIPTION
Read-only. The authoritative local source for Autopilot failures is the
Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Admin event log.
This script deliberately does NOT interpret specific event IDs: it pulls
all Level 2 (error) events in the lookback window and reports the message
text so you can classify failures yourself. It also gathers ESP context
signals: pending reboot markers, ESP tracking state in the registry, and
Intune Management Extension activity during setup.
Exit 0 = no Autopilot errors in the window and no stuck-state signals.
Exit 1 = Autopilot error events found, or a pending reboot combined with
recent Autopilot/ESP or IME activity.
Exit 2 = no local Autopilot activity evidence at all in the window.
Exit 3 = unexpected error.
#>$logDir = 'C:\ProgramData\Company\Logs'$logFile = Join-Path $logDir'AutopilotFailureDetection.log'$autopilotEventLog = 'Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Admin'$espTrackingKeyPath = 'HKLM:\SOFTWARE\Microsoft\WindowsAutopilot\EnrollmentStatusTracking'$provisioningKeyPath = 'HKLM:\SOFTWARE\Microsoft\Provisioning'$imeLogFolder = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs'$imeServiceName = 'IntuneManagementExtension'function Write-DetectionLog {
param([Parameter(Mandatory)][string] $Level, [Parameter(Mandatory)][string] $Message)
if (-not (Test-Path $logDir)) { New-Item -Path$logDir -ItemType Directory -Force | Out-Null }
"$([DateTime]::UtcNow.ToString('o')) [$Level] $Message" | Out-File -FilePath$logFile -Append -Encoding utf8
}
try {
Write-DetectionLog 'INFO'"Autopilot failure detection started. ErrorWindowDays=$ErrorWindowDays"$cutoff = (Get-Date).AddDays(-1 * $ErrorWindowDays)
# ---- Check 1: error events from the Autopilot diagnostics provider ----$errorEvents = @(Get-WinEvent -FilterHashtable @{ LogName = $autopilotEventLog; Level = 2; StartTime = $cutoff } -ErrorAction SilentlyContinue)
Write-DetectionLog 'INFO'"Found $($errorEvents.Count) error event(s) in '$autopilotEventLog' within the last $ErrorWindowDays day(s)."if ($errorEvents.Count -gt0) {
$errorEvents | Select-Object -First10 | ForEach-Object {
$oneLine = (($_.Message -replace'\s+', ' ').Trim())
if ($oneLine.Length -gt300) { $oneLine = $oneLine.Substring(0, 300) }
Write-DetectionLog 'ERROR' ("Autopilot error event: Id={0} Time={1:u} Message={2}" -f$_.Id, $_.TimeCreated, $oneLine)
}
# Keyword tally over message text - a pattern hint, not an error classification.foreach ($wordin @('profile', 'enroll', 'provision', 'join', 'certificate')) {
$hits = @($errorEvents | Where-Object { $_.Message -and ($_.Message -match$word) }).Count
if ($hits -gt0) {
Write-DetectionLog 'INFO'"Message keyword '$word' appears in $hits error message(s) - pattern hint only, not a classification."
}
}
Write-DetectionLog 'ERROR''Autopilot errors present - review the messages above and correlate with profile assignment in the Autopilot deployment service.'
exit 1
}
# ---- Check 2: ESP context signals ----$anyEvents = @(Get-WinEvent -FilterHashtable @{ LogName = $autopilotEventLog; StartTime = $cutoff } -ErrorAction SilentlyContinue)
Write-DetectionLog 'INFO'"Provider activity (any level) in window: $($anyEvents.Count) event(s)."$espTrackingPresent = Test-Path $espTrackingKeyPathif ($espTrackingPresent) {
$phaseNames = @(Get-ChildItem -Path$espTrackingKeyPath -ErrorAction SilentlyContinue | ForEach-Object { $_.PSChildName })
Write-DetectionLog 'INFO'"ESP tracking state present at $espTrackingKeyPath . Subkeys: $($phaseNames -join ', ')"
}
else {
Write-DetectionLog 'INFO'"No ESP tracking state at $espTrackingKeyPath - normal once setup completes or when ESP never ran."
}
$imeService = Get-Service -Name$imeServiceName -ErrorAction SilentlyContinue
$imeServicePresent = ($null -ne$imeService)
$imeRecentActivity = $falseif (Test-Path $imeLogFolder) {
$recentImeLogs = @(Get-ChildItem -Path$imeLogFolder -Filter'*.log' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt$cutoff })
$imeRecentActivity = ($recentImeLogs.Count -gt0)
}
Write-DetectionLog 'INFO'"IME present=$imeServicePresent RecentImeLogActivity=$imeRecentActivity"$artifactCount = 0if (Test-Path $provisioningKeyPath) {
$artifactCount = @(Get-ChildItem -Path$provisioningKeyPath -ErrorAction SilentlyContinue).Count
}
Write-DetectionLog 'INFO'"Provisioning artifact subkeys: $artifactCount" # ---- Check 3: pending reboot markers ----$cbsRebootPending = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'$wuRebootRequired = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'$pfroProp = Get-ItemProperty -Path'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name'PendingFileRenameOperations' -ErrorAction SilentlyContinue
$fileRenamePending = ($null -ne$pfroProp -and @($pfroProp.PendingFileRenameOperations).Count -gt0)
$pendingReboot = ($cbsRebootPending -or$wuRebootRequired -or$fileRenamePending)
Write-DetectionLog 'INFO'"Pending reboot: Cbs=$cbsRebootPending WindowsUpdate=$wuRebootRequired FileRename=$fileRenamePending"if ($pendingReboot -and ($espTrackingPresent -or$imeRecentActivity -or$anyEvents.Count -gt0)) {
Write-DetectionLog 'ERROR''A reboot is pending while there is recent Autopilot/ESP or IME activity - ESP cannot complete in this state.'
exit 1
}
# ---- Check 4: any local Autopilot evidence at all ----if ($anyEvents.Count -eq0 -and -not$espTrackingPresent -and$artifactCount -eq0) {
Write-DetectionLog 'WARN'"No Autopilot provider events, ESP tracking state, or provisioning artifacts within the last $ErrorWindowDays day(s)."
Write-DetectionLog 'WARN''Either Autopilot never ran locally in the window or the device is not Autopilot-assigned - verify assignment before deeper debugging.'
exit 2
}
Write-DetectionLog 'INFO''No Autopilot errors and no stuck-state signals detected in the lookback window.'
exit 0
}
catch {
Write-DetectionLog 'ERROR'"Unexpected failure: $($_.Exception.Message)"
exit 3
}
finally {
Write-DetectionLog 'INFO''Autopilot failure detection finished.'
}
Detection Logic
Error events, no ID interpretation. The script queries Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Admin with Level = 2, which is the standard eventing severity for errors, inside the lookback window. For each of the first ten events it logs the ID, timestamp, and a whitespace-collapsed message excerpt. Event message content varies across Windows builds, which is exactly why the script reports text instead of asserting that a given ID means a given thing. A keyword tally over the messages (profile, enroll, provision, join, certificate) provides a quick pattern hint without pretending to be a classification.
Provider activity of any level. Event volume in the window tells you whether Autopilot processing happened at all. A device with zero events, zero ESP tracking state, and zero provisioning artifacts was never in the flow, and that is a different investigation than a device with errors.
ESP tracking state. The script checks HKLM:\SOFTWARE\Microsoft\WindowsAutopilot\EnrollmentStatusTracking and logs the subkey names it finds, because phase layout varies by build. Presence indicates the ESP ran on this device; the script treats it as context, not as proof of health or failure.
IME signals during setup. ESP installs the Intune Management Extension during the device preparation phase. The script reports whether the IME service exists and whether any IME log file under C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\ was written inside the window, which distinguishes "ESP reached the app phase" from "ESP died before app installation".
Pending reboot markers. Three standard markers are checked: the Component Based Servicing RebootPending key, the Windows Update RebootRequired key, and a non-empty PendingFileRenameOperations value in HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager. A pending reboot combined with recent provider, ESP, or IME activity exits 1, because ESP cannot complete in that state.
Absence handling. No errors anywhere but no evidence of Autopilot activity either exits 2, pointing you at profile assignment rather than failure analysis.
Remediation Script
Detection-only, by design. There is no safe local repair for a failed Autopilot enrollment: retries happen through OOBE or the ESP's own retry option, profile assignment lives in the Autopilot deployment service, and enrollment repair is an admin-side procedure. What this script changes is the quality of your diagnosis. A device exiting 1 with error messages gives your support team a starting point; a device exiting 2 tells you to check assignment and group membership before anyone burns hours on the device itself.
Deploying Through Intune
Open the Microsoft Intune admin center and go to Devices → Scripts and remediations, then + Add on the Windows 10/11 platform tab.
Name it Detect - Failed Autopilot enrollment and describe it as a read-only event log harvester.
On the Detection script tab, upload detect.ps1. Set Run script in 64-bit PowerShell: Yes and Run this script using signed-on credentials: No so it runs as SYSTEM.
Leave Enforce script signature check: No unless your organization signs internal scripts.
On the Remediation script tab, select No remediation script.
Set Frequency to Daily while a deployment wave is running. Weekly is enough for steady-state monitoring.
Assign to the device groups participating in Autopilot waves. There is little value in assigning it to devices that never go through OOBE.
Review and create.
Post-specific notes:
Keep the 7-day default window during waves; widen it only when you are investigating a specific device after the fact.
Assign to groups by deployment ring so results map onto your rollout schedule.
The script never reboots or changes the device, so it is safe to run during active provisioning.
Correlate exit 1 devices with the deployment error view in the admin center before touching the hardware hash.
Visual Walkthrough
A repeatable triage path for the Enrollment Status Page before you reimage.
Decision TreeESP Stuck? Debug It Like an Engineer5/5
Decision gate
Where is ESP blocked?
Phase determines the log — never guess, MDMDiagnostics knows.
Device ESP blocked
1
Collect diagnostics
Shift+F10 → MDMDiagnosticsTool -area ESP; CAB holds the truth.
2
Read the blocking app
ESP log lists the exact blocking app or policy with status.
3
Check app failure
IME/AgentExecutor logs reveal exit codes and detection misses.
4
Fix and re-enroll
Correct the app/policy, then retry enrollment — device unchanged.
User ESP blocked
1
Reproduce with user
Sign-in context matters: user ESP runs after credential entry.
2
Inspect sidecar logs
DeviceManagement-Enterprise-Diagnostics-Admin and IntuneManagementExtension.
3
Find policy deadlock
A required user app failing detection is the classic hang.
4
Relax blocking set
Trim blocking apps to a viable minimum, redeploy, verify desktop.
ESP Stuck? Debug It Like an Engineer. Gate: Where is ESP blocked? — Device ESP blocked: Collect diagnostics — Device ESP blocked: Read the blocking app — Device ESP blocked: Check app failure — Device ESP blocked: Fix and re-enroll — User ESP blocked: Reproduce with user — User ESP blocked: Inspect sidecar logs — User ESP blocked: Find policy deadlock — User ESP blocked: Relax blocking set
Exit Codes
Any non-zero exit marks the device as needing remediation in the Intune reporting view. The codes follow the series convention:
Code
Meaning
0
No Autopilot error events and no stuck-state signals in the lookback window
1
Autopilot error events found, or a pending reboot combined with recent Autopilot/ESP or IME activity
2
No local Autopilot activity evidence in the window; verify profile assignment first
3
Unexpected script error - inspect the log
Logging
Everything is written to C:\ProgramData\Company\Logs\AutopilotFailureDetection.log, one line per entry with a UTC timestamp, level, and message. Error events appear with their ID, timestamp, and a 300-character message excerpt, and the keyword tally lines show how often each hint word appears. Expect a handful of lines per run on healthy devices and up to about twenty on failing ones; the file grows slowly, so monthly rotation is plenty. For the full text of any event, open Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Admin in Event Viewer, and cross-reference C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log when the failure involves app installation during ESP.
Troubleshooting the Script
Every device exits 3. A context or module problem: confirm 64-bit PowerShell and SYSTEM execution, and confirm the script file transferred intact.
Exit 2 on a device you know went through Autopilot. The lookback window may be shorter than the provisioning history; raise ErrorWindowDays or check whether the device was re-imaged since.
Error messages are unreadable or empty. Some provider events log message templates that are empty on certain builds; rely on the ID and timestamp, then pull the full record in Event Viewer.
Pending-reboot exit 1 on devices that are not provisioning. The reboot flag only exits 1 alongside recent provider, ESP, or IME activity; if that combination is common in your estate, narrow the assignment to wave groups.
Keyword tally looks misleading. The tally is a hint over raw message text, not a diagnosis. Always read the excerpts.
Nothing logs at all. Check the assignment and the device's last sync, then run the script manually in an elevated 64-bit console.
How to Validate the Fix
On a healthy Autopilot device, run the script in an elevated 64-bit PowerShell session. Expect exit code 0 and an INFO line stating provider activity counts.
Find a device with a known failed attempt, or reproduce one in a test VM by blocking enrollment endpoints during OOBE. Expect exit code 1 with error events in the log.
Read the logged message excerpts and confirm they match what Event Viewer shows for the same timestamps.
Reprovision the test device with correct assignment, then re-run detection. Expect exit code 0 on the new attempt.
Check the package's Device status in the admin center and confirm the split between 0, 1, and 2 devices matches your wave reports.
Production Considerations
Context. Runs as SYSTEM in 64-bit Windows PowerShell; the #Requires directive enforces local admin rights for manual runs.
Idempotency. The script writes nothing except its log: all checks are registry, filesystem, and event log reads.
Side effects. None and none by construction: no reboot triggers, no service restarts, no policy changes. It is safe during active provisioning.
Security. No credentials, no network calls, no sensitive data beyond device and event metadata in the log. Before broad production rollout, validate the script in a test ring with your PowerShell automation SME and security team, in line with your internal script review policy.
Event volume. The query is bounded by the lookback window and a first-ten cap per category, so runtime stays predictable even on chatty devices.
Why does the script refuse to interpret specific event IDs?
Provider message templates change across Windows builds, and hard-coding meanings turns into false classification after the next feature update. Reporting message text with IDs and timestamps keeps the diagnosis accurate, and the keyword tally gives you fast pattern scanning without pretending to be an error taxonomy.
Will this find failures during the account setup phase of ESP?
Partly. Provider events and pending-reboot signals are machine-scoped and visible as SYSTEM. The account setup phase runs in the user's session, so some user-side detail lives in user-context logs the script does not read. Treat a clean result with a user-reported account-phase failure as a prompt to collect ESP diagnostics from the user session.
How do I correlate this with the Autopilot deployment service side?
Use the timestamps in the log. The device-side events tell you what the device tried and when; the admin center deployment error view and the service-side reports tell you what the service saw. Matching timestamps across both sides usually narrows a failure to one hop within minutes.
Comments
0No comments yet — be the first to share your thoughts.