~/2026/04/08/intune-autopilot-provisioning-failures-and-how-to-actually-debug-them.md
Intune: Autopilot Provisioning Failures and How to Actually Debug Them
--- author: Tom Lasswell date: read: 8 min in: [engineering] tags: [intune, windows] ---
$ grep -n '^#' post.md
Autopilot is one of the better ideas Microsoft has shipped for endpoint management, and it's also one of the easiest things to make look unreliable by debugging it wrong. When a machine fails partway through the enrollment status page, the instinct on most help desks is to reimage it and try again. Sometimes that works, which is exactly the problem: it teaches everyone that Autopilot is flaky instead of teaching anyone what actually went wrong. I've found that treating a stuck Autopilot run as a diagnosable event, not a coin flip, is the difference between a five-minute fix and a recurring mystery.
Read the error code before you touch the machine
Most Autopilot failures surface a specific code, and almost nobody reads it before they wipe the device. When there is one, record it before you reset anything. The earlier version of this post got two of the most common ones wrong, so here they are as Microsoft documents them:
| Code | Where you see it | What Microsoft says it means |
|---|---|---|
0x800705B4 | Self-deploying or pre-provisioning, usually at Securing your hardware | A general timeout. The common cause in self-deploying mode is a device that isn't TPM 2.0 capable, such as a virtual machine. Those can't use self-deploying mode at all. |
0x80180014 | Re-running Autopilot on a device that was deployed before | Either the device was previously deployed with self-deploying or pre-provisioning mode and its Intune record has to be deleted (or the device unblocked under Windows Autopilot > Devices > Unblock device), or Windows (MDM) enrollment is set to Block in a device platform restriction that applies to it. |
0x801c03ea | Self-deploying | TPM attestation failed, so the Microsoft Entra join with a device token failed. |
0x81039001 | Self-deploying or pre-provisioning technician flow | E_AUTOPILOT_CLIENT_TPM_MAX_ATTESTATION_RETRY_EXCEEDED, intermittent; a retry may succeed. |
0xc1036501 | Self-deploying | Automatic MDM enrollment can't pick an MDM because there are multiple MDM configurations in Entra ID. |
0x80070774 | Hybrid join, during ESP | Domain mismatch between where the Intune Connector for Active Directory runs and where devices are targeted. |
0x801C03F3 | Pre-provisioning, in the User Device Registration admin log | Entra ID can't find the device object, usually because someone deleted it. |
80180018 | "Something went wrong" page | An Intune enrollment problem, typically a missing license or too many devices enrolled for the user. |
My original text said 0x80180014 meant a conflicting Entra ID object from an old enrollment, and that 0x800705B4 almost always meant waiting on an app scoped to the wrong group. Neither matches Microsoft's documentation. With 0x80180014 in particular, the ETW trace shows Enrollment blocked for AP device by SDM One Time Limit Check, which points you straight at the device record, not at Entra ID. None of these get fixed by reimaging alone. They get fixed by looking at the code and correcting the cause; a reset or redeployment may still be needed afterward, but only once the record or configuration is right.
The Autopilot event log (Applications and Services Logs > Microsoft > Windows > ModernDeployment-Diagnostics-Provider > Autopilot) adds the profile side of the story. The event IDs I look for first:
- 807
ZtdDeviceIsNotRegistered: the hardware hash isn't uploaded or isn't assigned to a profile. - 809 and 815
ZtdDeviceHasNoAssignedProfile: the assigned profile was deleted, or nothing is assigned and there's no default. - 908
SerialNumberMismatch/ProductKeyIdMismatch: the registration doesn't match the hardware; re-register. - 171 and 172: TPM identity confirmation failed during self-deploying mode.
- 153 with
ProfileState_Available: a profile was downloaded, so the problem is later in the flow. - 163: a profile is already on the device and won't be downloaded again until it's reset.
Collect the evidence properly
Intune already does part of this for you. When a Windows Autopilot deployment fails, the device can automatically capture and upload a diagnostics package, one set per day per device, kept for 28 days. It's on by default under Tenant administration > Device diagnostics and shows up on the device's Diagnostics tab. On Windows 11 user-driven deployments, turning on Turn on log collection and diagnostics page for end users in the ESP profile also gives the technician the Autopilot diagnostics page with Ctrl+Shift+D.
On the device itself, Shift+F10 at OOBE opens a command prompt, and this is the collection command Microsoft gives in the Autopilot known issues:
mdmdiagnosticstool.exe -area "Autopilot;TPM" -cab C:\autopilot.cab
For enrollment and provisioning problems more broadly, use the areas from the MDM log collection doc; the zip contains MDMDiagHtmlReport.html, MDMDiagReport.xml, a registry dump and the admin event logs:
mdmdiagnosticstool.exe -area "DeviceEnrollment;DeviceProvisioning;Autopilot" -zip C:\Users\Public\Documents\MDMDiagReport.zip
To turn that into something readable, Michael Niehaus's Get-AutopilotDiagnostics script on the PowerShell Gallery parses the live device or a captured archive and lists the profile, ESP tracking, apps, policies and certificates with their status. The gallery page notes it doesn't work on ARM64:
Install-Script -Name Get-AutopilotDiagnostics -Force
Get-AutopilotDiagnostics.ps1 -CABFile C:\autopilot.cab
Get-AutopilotDiagnostics.ps1 -Online
-Online looks up additional app and policy details from your tenant, which makes the output far easier to read on a real deployment. The Intune Management Extension logs, which cover Win32 apps and scripts, are in %ProgramData%\Microsoft\IntuneManagementExtension\Logs.
When I'm at the device and want a one-screen answer before digging through archives, I run this from the Shift+F10 prompt (powershell.exe -ExecutionPolicy Bypass -File D:\Get-AutopilotTriage.ps1) or from an elevated session afterwards. It reads the Autopilot profile values Windows stored, pulls the known Autopilot event IDs plus recent errors from the MDM and device registration logs, checks the TPM, prints the device clock, and saves the diagnostics archive:
<#
.SYNOPSIS
One-screen Autopilot triage on the affected device.
.DESCRIPTION
Reads the Autopilot profile values from
HKLM:\SOFTWARE\Microsoft\Provisioning\Diagnostics\Autopilot, lists the
documented Autopilot event IDs from the ModernDeployment-Diagnostics-Provider
log, the latest errors from the DeviceManagement-Enterprise-Diagnostics-Provider
and User Device Registration admin logs, TPM readiness and the device clock,
then runs mdmdiagnosticstool to capture an archive for deeper analysis.
.PARAMETER OutputFolder
Folder for the diagnostics archive. Defaults to C:\AutopilotTriage.
.PARAMETER MaxEvents
Number of recent errors to show per log. Defaults to 5.
.EXAMPLE
.\Get-AutopilotTriage.ps1
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2026-04-08)
Requires: Elevated PowerShell on the affected device.
#>
[CmdletBinding()]
param (
[Parameter()]
[string]$OutputFolder = "C:\AutopilotTriage",
[Parameter()]
[int]$MaxEvents = 5
)
New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
# Profile values Windows received from the Autopilot deployment service.
Write-Host "== Autopilot profile" -ForegroundColor Cyan
$profileKey = "HKLM:\SOFTWARE\Microsoft\Provisioning\Diagnostics\Autopilot"
$apProfile = Get-ItemProperty -Path $profileKey -ErrorAction SilentlyContinue
if ($apProfile) {
$apProfile | Select-Object -Property CloudAssignedTenantDomain, CloudAssignedTenantId, IsAutopilotDisabled, TenantMatched, CloudAssignedOobeConfig | Format-List
} else {
Write-Host "No Autopilot profile values found. The device may not be registered, or the profile download failed."
}
# Documented Autopilot event IDs and what they mean.
$known = @{
100 = "Profile not found yet (usually transient)"
153 = "Profile state changed (ProfileState_Available = profile downloaded)"
163 = "Already provisioned; reset the device to download a new profile"
171 = "TPM identity confirmation failed (self-deploying)"
172 = "Could not mark profile available (see 171)"
807 = "ZtdDeviceIsNotRegistered: hash not uploaded or not assigned"
809 = "Assigned profile was deleted"
815 = "No profile assigned and no default profile"
908 = "Serial number or product key mismatch; re-register"
}
Write-Host "== Autopilot events" -ForegroundColor Cyan
$apEvents = Get-WinEvent -LogName "Microsoft-Windows-ModernDeployment-Diagnostics-Provider/Autopilot" -MaxEvents 200 -ErrorAction SilentlyContinue |
Where-Object { $known.ContainsKey($_.Id) }
$apEvents | Select-Object -First 15 -Property TimeCreated, Id, @{ Name = "Meaning"; Expression = { $known[$_.Id] } } | Format-Table -AutoSize
# Latest errors from the enrollment and registration logs.
$logs = @(
"Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin",
"Microsoft-Windows-User Device Registration/Admin"
)
foreach ($log in $logs) {
Write-Host "== Errors: $log" -ForegroundColor Cyan
$errors = Get-WinEvent -FilterHashtable @{ LogName = $log; Level = 2 } -MaxEvents $MaxEvents -ErrorAction SilentlyContinue
if ($errors) {
$errors | Select-Object -Property TimeCreated, Id, @{ Name = "Message"; Expression = { ($_.Message -split "`r?`n")[0] } } | Format-Table -AutoSize -Wrap
} else {
Write-Host "No errors (or log not present)."
}
}
# TPM readiness matters for self-deploying and pre-provisioning.
Write-Host "== TPM" -ForegroundColor Cyan
Get-Tpm | Select-Object -Property TpmPresent, TpmReady, TpmEnabled, TpmActivated, TpmOwned | Format-List
# A large clock offset breaks TPM attestation and can cause ESP timeouts.
Write-Host "== Clock" -ForegroundColor Cyan
Write-Host "Device UTC time: $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')). Compare with a known-good clock."
# Archive for Get-AutopilotDiagnostics or a support case.
$cab = Join-Path -Path $OutputFolder -ChildPath "autopilot.cab"
& mdmdiagnosticstool.exe -area "Autopilot;TPM" -cab $cab | Out-Null
Write-Host "Diagnostics archive: $cab"
Trimmed output from a device that failed at Securing your hardware:
== Autopilot profile
CloudAssignedTenantDomain : contoso.onmicrosoft.com
IsAutopilotDisabled : 0
TenantMatched : 1
== Autopilot events
TimeCreated Id Meaning
----------- -- -------
4/8/2026 9:14:02 AM 171 TPM identity confirmation failed (self-deploying)
4/8/2026 9:11:40 AM 153 Profile state changed (ProfileState_Available = profile downloaded)
== TPM
TpmPresent : True
TpmReady : False
If the device clock is off by more than a few minutes, Microsoft's documented fix is to go back to the start of OOBE, connect to the network, and run w32tm /resync /force before trying again.
The enrollment status page tells you less than you think
The ESP tracks three phases: Device preparation (securing the hardware with TPM attestation, joining Entra ID, enrolling in MDM), Device setup (device-targeted items), and Account setup (user-targeted items). The phase it's visually stuck on is not always where the real problem is, partly because of what it doesn't track. Per Microsoft's ESP documentation:
- Security policies such as device restrictions aren't tracked at all; they install in the background. The only policies ESP tracks are Microsoft Edge, Assigned Access and Kiosk Browser.
- Certificates are tracked only for SCEP profiles, and network connections only for VPN and Wi-Fi profiles.
- Device setup tracks device-context apps: per-machine LOB MSI, device-context LOB store apps, Win32 and WinGet apps. Account setup tracks user-assigned apps.
- Scripts that run in the user context may not execute during ESP. Microsoft's workaround is to run them as system.
So a device "stuck on apps" is often waiting on exactly one Win32 install, and the diagnostics archive shows which. Two of the most common causes are both documented: mixing LOB (MSI) and Win32 apps during ESP (both use TrustedInstaller, so one fails with Another installation is in progress), and Microsoft 365 Apps deployed with the built-in app type installing while another tracked Win32 app is running. Microsoft recommends packaging Microsoft 365 Apps as Win32 for ESP.
Timeouts are a policy design problem, not a network problem
A recurring pattern: a machine fails ESP on a slow connection (a home Wi-Fi network, a hotel, a site with a saturated WAN link) and the assumption is "the network is bad." Sometimes it is. More often the ESP timeout is too aggressive for the payload being pushed during provisioning, and a machine on a fast connection would have hit the same wall a few minutes later. The numbers to design around:
- Show an error when installation takes longer than specified number of minutes defaults to 60.
- Hybrid-joined Autopilot adds 40 minutes on top of whatever you set, to give the connector time to create the device object.
- Install Windows quality updates is on by default for new ESP profiles and, per Microsoft, adds 20 to 40 minutes to provisioning with possible restarts.
- Block device use until these required apps are installed accepts up to 100 selected apps. With Selected, apps not on that list aren't tracked, and in user-driven mode Win32 apps not on the list install after ESP completes.
The fix isn't blaming the network, it's right-sizing what has to finish before the ESP releases the user to the desktop versus what can land afterwards as a background required app. I move anything that isn't strictly needed for day-one login off the blocking list and let it install after ESP.
Stale registrations are the silent repeat offender
When the same device fails more than once, the first thing I check is its registration history: a reseller's pre-registration, a previous deployment, a motherboard swap. Microsoft is explicit that large hardware changes such as a motherboard replacement need a new hash, and that a device with a hardware change can show Fix pending or Attention required in the Autopilot devices list. This read-only Graph script pulls everything Intune knows about a serial number in one go: the Autopilot identity (with its enrollment state, group tag and linked IDs) and any managed device records carrying the same serial:
<#
.SYNOPSIS
Shows the Autopilot and Intune records for one or more serial numbers.
.DESCRIPTION
Read-only. Lists windowsAutopilotDeviceIdentities and managedDevices from
Microsoft Graph v1.0 and matches them by serial number, so duplicate or
stale records for the same hardware are visible side by side.
.PARAMETER SerialNumber
One or more device serial numbers.
.EXAMPLE
.\Get-AutopilotRecord.ps1 -SerialNumber "<serial-number>"
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2026-04-08)
Requires: Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement;
DeviceManagementServiceConfig.Read.All,
DeviceManagementManagedDevices.Read.All.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string[]]$SerialNumber
)
Connect-MgGraph -Scopes "DeviceManagementServiceConfig.Read.All", "DeviceManagementManagedDevices.Read.All" -NoWelcome
# Autopilot identities (v1.0), following paging.
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/windowsAutopilotDeviceIdentities"
$identities = @()
while ($uri) {
$page = Invoke-MgGraphRequest -Method GET -Uri $uri
$identities += $page.value
$uri = $page.'@odata.nextLink'
}
$managed = Get-MgDeviceManagementManagedDevice -All -Property "id,deviceName,serialNumber,enrolledDateTime,lastSyncDateTime,azureADDeviceId"
foreach ($serial in $SerialNumber) {
Write-Host "== $serial" -ForegroundColor Cyan
$identities | Where-Object { $_.serialNumber -eq $serial } | ForEach-Object {
[PSCustomObject]@{
Record = "Autopilot"
Id = $_.id
GroupTag = $_.groupTag
EnrollmentState = $_.enrollmentState
LastContacted = $_.lastContactedDateTime
ManagedDeviceId = $_.managedDeviceId
EntraDeviceId = $_.azureActiveDirectoryDeviceId
}
} | Format-List
$managed | Where-Object { $_.SerialNumber -eq $serial } | ForEach-Object {
[PSCustomObject]@{
Record = "Intune managed device"
Id = $_.Id
DeviceName = $_.DeviceName
Enrolled = $_.EnrolledDateTime
LastSync = $_.LastSyncDateTime
EntraDeviceId = $_.AzureAdDeviceId
}
} | Format-List
}
Two managed device records for one serial, or an Autopilot identity whose managedDeviceId points at a record that no longer matches, is the "works for other devices, fails for this one" pattern that looks like a hardware fault.
The cleanup order matters, and my earlier advice to just delete the stale Autopilot record was too casual. Microsoft's deregistration procedure is: delete the device from Intune first, then deregister it from Windows Autopilot (unassigning the user if that option is available), then sync. For Entra joined devices, don't manually delete the Microsoft Entra device object. The object created at registration is Autopilot's anchor for group membership and profile targeting, and deleting it can cause the join failures above (0x801C03F3 among them). For hybrid-joined devices, delete the computer object from on-premises AD instead so it doesn't sync back. After that, re-register and redeploy.
It's a five-minute check against an afternoon of chasing ghosts, and it's the same lesson as the rest of this post: the evidence is on the device and in Graph, and it's faster to read it than to reimage and hope.
References
- Windows Autopilot troubleshooting FAQ
- Windows Autopilot known issues
- Set up the Enrollment Status Page
- Device action: Collect diagnostics
- Collect MDM logs
- Windows Autopilot registration overview (deregister a device)
- List windowsAutopilotDeviceIdentities, Graph v1.0
- Get-AutopilotDiagnostics (PowerShell Gallery)