There’s a category of apps that only make sense during the first hour of a device’s life. Bootstrap scripts. One-shot OEM configuration tools. Certificates that have to land before anything else talks to the network. Baseline installers that prepare a machine for the real app stack to arrive on top. You want them to run inside the Enrollment Status Page, you want them to not chase users around six months later when they reset a password, and you absolutely want them to skip devices that were already enrolled before the app existed.
Intune does not have a setting for this. The assignment model has Required, Available, and Uninstall. The timing model has Available date and Deadline. Neither of those answers “only during provisioning.” So the pattern everyone ends up building is the same three-part structure, and most of the failure modes come from getting one of the three parts slightly wrong.
Why the obvious answers are wrong
Before the good answer, a quick tour of the bad ones, because every one of these shows up in threads on r/Intune at least once a week.
Assign to the Autopilot device group, unassign later. Works exactly once. Scales to zero. Requires a human to remember, which means it’s already broken.
Deadline = now, Available = now, hope the device is fast. The deadline controls when install must happen by. It does nothing to stop the install from retrying on day 47 when the Intune Management Extension decides to reconcile. Once the app is assigned, it’s assigned.
Use an assignment filter. Filters operate on device properties — OS version, manufacturer, enrollment type. There is no “device is currently in ESP” property to filter on. You can get close with device age filters in some scenarios, but you can’t gate on the enrollment phase itself.
Just use a detection script that lies. You could write a detection script that returns “installed” when ESP is over, so the app never runs post-enrollment. This works but it’s fragile — any detection-rule failure causes a retry, and the IME logs fill with false-install evidence that makes troubleshooting miserable later. Do the gating in the requirement rule, not the detection rule. That’s what requirement rules are for.
The pattern that works
A Win32 app with three pieces configured correctly:
- A requirement rule (PowerShell script) that only returns success during Autopilot / first-provisioning
- An install command that does its work and writes a permanent marker to the registry
- A detection rule that looks for that marker — so once installed, the app is installed forever from Intune’s perspective
The requirement rule stops new installs outside the enrollment window. The marker stops retry installs on a device that has already run the app. Together they give you “install exactly once, only during enrollment.”
The requirement rule
Intune Win32 apps support a script-based requirement rule where the script writes a value to stdout and Intune evaluates it against an expected string. Exit 0 always; communicate via stdout.
Here’s a requirement script that returns InESP if the device is still in Autopilot / first provisioning, and NotInESP otherwise:
# Requirement rule: only install during Autopilot / first provisioning
$inProvisioning = $false
# Signal 1: ESP tracking keys exist and provisioning is not yet marked complete.
# Path and value names have shifted across Windows builds — treat missing
# values as "not in ESP" rather than crashing.
$espDevicePath = "HKLM:\SOFTWARE\Microsoft\Windows\Autopilot\EnrollmentStatusTracking\Device\Setup"
if (Test-Path $espDevicePath) {
$completed = (Get-ItemProperty -Path $espDevicePath -Name HasProvisioningCompleted -ErrorAction SilentlyContinue).HasProvisioningCompleted
if ($null -ne $completed -and $completed -ne 1) {
$inProvisioning = $true
}
}
# Signal 2: OS install age. Fresh Autopilot devices have an install date
# within the last few hours. Tune this window to match how long your ESP
# typically takes, plus a safety margin.
try {
$installDate = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).InstallDate
$ageHours = (New-TimeSpan -Start $installDate -End (Get-Date)).TotalHours
if ($ageHours -lt 4) {
$inProvisioning = $true
}
} catch {
# If we can't read it, don't guess — fall through.
}
if ($inProvisioning) { Write-Output "InESP" } else { Write-Output "NotInESP" }
exit 0
In the Win32 app’s Requirements → Add requirement rule → Script, configure:
- Run script as 32-bit – No
- Run this script using the logged on credentials – No (run as system)
- Output data type – String
- Operator – Equals
- Value –
InESP
That’s the gate. Devices that return NotInESP fail the requirement and Intune does not attempt the install.
A note on the OS-age fallback: the ESP registry keys are the authoritative signal, but they’ve moved and been renamed between Windows 10 and Windows 11 builds enough times that scripts depending on a single exact path tend to rot. Treating install age as a secondary signal catches the edge cases where the ESP key isn’t where you expect. Four hours is a reasonable default; tune it to your environment. A device that takes six hours to finish ESP is a device with problems bigger than this script.
The install command and marker
Whatever your actual install does, append a marker write at the end. The marker is what the detection rule will look for. Use a path under HKLM:\SOFTWARE\ with a name you’ll recognize in six months:
# install.ps1 — wraps your real install
try {
# ... your real install work here ...
# Example: & msiexec /i "$PSScriptRoot\YourThing.msi" /qn /norestart
# Write marker only if install succeeded
$markerPath = "HKLM:\SOFTWARE\Contoso\AutopilotBootstrap"
New-Item -Path $markerPath -Force | Out-Null
New-ItemProperty -Path $markerPath -Name "Installed" -Value 1 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $markerPath -Name "InstalledAt" -Value (Get-Date -Format o) -PropertyType String -Force | Out-Null
exit 0
} catch {
Write-Error $_
exit 1
}
Package this with the Microsoft Win32 Content Prep Tool (IntuneWinAppUtil.exe) as usual. The install command becomes:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1
The detection rule
Use the app’s detection configuration to look for the marker registry key. Registry rule:
- Key path –
HKEY_LOCAL_MACHINE\SOFTWARE\Contoso\AutopilotBootstrap - Value name –
Installed - Detection method – Value exists (or equals 1)
Once the marker is written, Intune considers the app installed on that device, permanently. Even if the requirement rule would now return NotInESP, the detection rule short-circuits before the requirement is ever re-evaluated for reinstall. The device is done.
Why all three pieces matter
- Requirement rule only → the app tries to install during enrollment. Great. But if the install fails and retries, and ESP has ended, the requirement will now say
NotInESPand Intune gets stuck reporting failures forever. The marker prevents that. - Detection rule only → the app installs on every device assigned, including ones that enrolled three years ago and just picked up the assignment today.
- Install command without marker → detection rule has nothing to key off. Back to installing on every assigned device.
Gotchas that will eat your week
Re-enrollments reset the clock. If a user wipes and re-enrolls a device, the OS install date resets to “now” and the requirement script returns InESP again. The marker is also gone (it was in HKLM, which got wiped). The app will reinstall. This is usually what you want — a fresh provisioning should get the same bootstrap — but be aware if your install has side effects elsewhere.
Hybrid join is different. Hybrid Azure AD Joined devices go through a different enrollment flow that doesn’t always populate the Autopilot ESP registry keys the same way. If you deploy to a hybrid-joined fleet, rely more heavily on the OS-install-age signal and less on the ESP registry check, or test carefully on a real hybrid device before rollout.
User ESP vs Device ESP. The ESP runs in two phases: Device Preparation/Setup (system context) and Account Setup (user context, after first login). Required Win32 apps assigned to devices typically install during Device ESP. If your app is assigned to users, it installs during Account ESP, which is a narrower window and sometimes much later than the Autopilot window you’re trying to scope to. Assign to devices if at all possible.
System context detection of defaultuser0. Some community scripts try to detect “we’re in ESP” by checking if the current user is defaultuser0. This does not work in Win32 app requirement/detection scripts because those run as SYSTEM, not as the logged-on user. $env:USERNAME will be the computer name or SYSTEM, not defaultuser0. Don’t use this pattern.
Don’t forget to test the negative path. Grab a device that’s been enrolled for a month and deploy the policy to it. The requirement rule should block the install and the IME log should show Requirement rule evaluation: Not applicable. If you see the install actually run on that device, your requirement is letting something through — usually the install-age window is too wide.
Verifying it works
Three places to look:
- IntuneManagementExtension.log (
C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\). Search for your app’s display name. For devices that should skip, the line you want isApplicabilityEvaluator: Requirement rule evaluation returned: Not applicable. For devices that should install, you wantRequirement rule evaluation: Applicablefollowed by the install attempt. - The marker registry key. After a successful Autopilot run on a test device,
Get-ItemProperty HKLM:\SOFTWARE\Contoso\AutopilotBootstrapshould returnInstalled : 1. - The Intune portal’s install report. A correctly scoped app will show “Installed” on enrollment-era devices and “Not applicable” on everything else — not “Failed” and not “Pending.” Not applicable is the success state here.
When this isn’t the right pattern
If your “one-time during enrollment” app is really a one-time-per-user thing (like a personal certificate install, or a mailbox configuration tool), the marker needs to be in HKCU, the app needs user assignment, and the whole timing story shifts to Account ESP. That’s a different post.
If you need to run something that just needs to happen once per device but doesn’t actually care whether it happens during Autopilot or on day 2, skip the requirement rule entirely and just use the detection-rule-with-marker pattern. Simpler, fewer moving parts, and you don’t have to reason about ESP registry keys changing between Windows builds.
The Autopilot-only window is specifically for things where running late is worse than not running at all. Bootstrap scripts that assume a clean machine. Network profiles that have to land before anything else. Things where you’d rather the device skip the app than run it three months into its life. If that’s your scenario, the three-piece pattern above is the pattern. If it isn’t, you probably don’t need to be this clever.


