Win32 app delivery in Intune fails in boring, repeatable ways: detection that reports “installed” when the binary is half-written, a dependency chain that deadlocks, a supersedence relationship that leaves two versions side by side. None of these are Intune bugs – they are modeling errors. The Win32 app type is a small state machine (download, run, check return code, evaluate detection) and if you feed it honest rules it is deterministic across 50,000 endpoints. This guide is about feeding it honest rules.
1. How the Intune Management Extension installs Win32 apps
Win32 apps are not delivered by the MDM channel. They are delivered by the Intune Management Extension (IME), a Windows service (Microsoft Intune Management Extension) that is installed automatically the first time a PowerShell script or a Win32 app is assigned to the device or user. Understanding its loop is the prerequisite for everything else.
- The IME checks every hour – and on service start or device restart – for new Win32 app and script assignments. There is no “push.” If you want to force a cycle in a lab, restart the service.
- App content (
.intunewin) is downloaded over HTTPS, decrypted, and cached underC:\Program Files (x86)\Microsoft Intune Management Extension\Content. Delivery Optimization peer-to-peer is on by default, so the first device in a subnet pulls from the cloud and the rest can pull from it. - The install command runs in SYSTEM context by default (or in the signed-in user’s context if you choose user install behavior).
- After the command exits, the IME maps the return code to an outcome, then runs your detection rule. Detection – not the exit code alone – is the final arbiter of “installed.”
That last point is the one people miss. A return code of 0 with a detection rule that finds nothing is reported as a failure (0x87D1041C), because the IME does not trust the installer’s word over your detection logic. One hard limit worth pinning to memory: a single app’s content cannot exceed 30 GB.
Where it logs
All client-side troubleshooting starts in one folder:
C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\
| Log | What it carries |
|---|---|
IntuneManagementExtension.log |
Core IME service: policy fetch, check-in cycles, service health |
AppWorkload.log |
Win32 app activity (download, install command, return code, detection) – on newer agents this split out of the IME log |
AgentExecutor.log |
Output of AgentExecutor.exe, which runs your PowerShell scripts and detection scripts |
On older agents, Win32 app activity lives entirely in
IntuneManagementExtension.log. On current agents it is inAppWorkload.log. Check both. View them with CMTrace (highlights warnings/errors and tails live) rather than Notepad.
2. Wrapping installers with the Content Prep Tool
The .intunewin format is an encrypted, compressed envelope around your installer’s source folder. You build it with the Microsoft Win32 Content Prep Tool (IntuneWinAppUtil.exe). The syntax is deliberately minimal:
IntuneWinAppUtil.exe -c <setup_folder> -s <setup_file> -o <output_folder> [-a <catalog_folder>] [-q]
-c– the source folder that holds every file the installer needs (the whole payload, not just the EXE).-s– the setup file inside that folder, e.g.setup.exe,install.ps1, or the MSI.-o– where to drop the resulting.intunewin.-a– catalog folder, only for Windows 10/11 S mode.-q– quiet. Use this in pipelines.
A concrete run, packaging a 7-Zip MSI from C:\pkg\7zip into C:\out:
.\IntuneWinAppUtil.exe -c "C:\pkg\7zip" -s "7z2408-x64.msi" -o "C:\out" -q
The output is 7z2408-x64.intunewin. When you point -s at an MSI, the tool reads and embeds the MSI product code, version, and package type so Intune pre-populates those fields – the only real reason to package a bare MSI rather than a wrapper script.
Structuring install and uninstall commands
You set these in the admin center, not inside the package. The package only carries content. Get the silent switches right or every other rule is moot.
# MSI (machine-wide, no UI, log to a known path)
Install: msiexec /i "7z2408-x64.msi" /qn /norestart /l*v "%ProgramData%\IntuneLogs\7zip-install.log"
Uninstall: msiexec /x "{23170F69-40C1-2702-2408-000001000000}" /qn /norestart
# EXE with a vendor silent switch
Install: setup.exe /S /v"/qn"
Uninstall: "%ProgramFiles%\Vendor\App\uninst.exe" /S
For anything non-trivial – pre-reqs, registry, ACLs, conditional logic – wrap the installer in a script and make that the setup file:
Install: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\Install-App.ps1"
Uninstall: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\Uninstall-App.ps1"
Always pass
/norestartand let Intune own restart behavior via return codes. An installer that reboots on its own kills the IME mid-cycle and you get phantom failures.
3. Detection rules that don’t lie
Detection answers one question: “is this exact app, this exact version, present?” Get it wrong in either direction and you get reinstall loops or apps that report installed but aren’t there. There are four rule types; you can combine several (all must match).
MSI product code. The cleanest option for MSI installers. Intune extracts the GUID at packaging time. Add a version check if you supersede by version.
File or folder. Detect a known binary and gate on version, size, or existence. The high-fidelity pattern is “file exists AND version is greater than or equal to X”:
Path: %ProgramFiles%\7-Zip
File: 7z.exe
Detection method: File or folder exists -> or -> Version >= 24.08
Do not detect on a file that the installer writes early (e.g. a temp marker). Detect on something written last, like the main executable. That is how you avoid “installed” verdicts on a half-finished install.
Registry. Detect on a value the installer creates – a version string in the uninstall key is ideal because it appears only on a completed MSI/EXE install:
Key path: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}
Value: DisplayVersion
Method: String comparison >= 24.08
PowerShell script. The escape hatch for everything the GUI cannot express. The contract is strict and trips everyone at least once:
The app is detected only when the script exits with code 0 AND writes a non-empty string to STDOUT. Exit 0 with no output = not detected. Any non-zero exit = not detected. The actual text written does not matter; its mere presence does.
# Detection: installed only if v24.08+ is present
$key = 'HKLM:\SOFTWARE\7-Zip'
$installed = (Get-ItemProperty -Path $key -ErrorAction SilentlyContinue).Path
if ($installed -and (Test-Path (Join-Path $installed '7z.exe'))) {
$v = (Get-Item (Join-Path $installed '7z.exe')).VersionInfo.ProductVersion
if ([version]$v -ge [version]'24.08') {
Write-Output "Detected $v" # non-empty STDOUT
exit 0 # success
}
}
exit 1 # not detected -> Intune (re)installs
The IME runs as a 64-bit process. If detection needs the 32-bit registry/file view, tick “Run script as a 32-bit process on 64-bit clients.” Otherwise a script checking the wrong hive (WOW6432Node versus native) reports “not installed” forever.
4. Requirement rules: gating before download
Requirements are evaluated before content downloads. If a device fails a requirement, the app shows “Not applicable” – not failed – and no bytes move. Use them to keep packages off machines that cannot run them.
Built-in requirements: OS architecture (x86/x64/ARM64), minimum OS version, disk space, physical memory, logical processors, and the same file/registry/script checks you have in detection.
A custom requirement script is the powerful one. Same exit-code/STDOUT contract as detection, but you also declare an output data type and a comparison Intune applies to the STDOUT value:
# Requirement: only applicable on devices joined to the corp domain
$domain = (Get-CimInstance Win32_ComputerSystem).Domain
if ($domain -eq 'corp.contoso.com') { Write-Output 'True' } else { Write-Output 'False' }
exit 0
Set the script output type to String and the rule to equals True. Devices off-domain become “Not applicable” and never attempt install.
Requirements vs detection is the distinction people blur. Requirements decide whether to try; detection decides whether it worked. Putting “is the app already here?” logic in a requirement is a classic mistake – it makes the app go “Not applicable” once installed, which breaks reporting.
5. Modeling dependencies without circular chains
A dependency is “App B must be present before App A installs.” The IME walks the graph and installs prerequisites first. Two facts govern your design:
- Maximum of 100 apps across the entire dependency graph (the app plus all nested dependencies).
- Evaluation does not follow a guaranteed order at the same level, but sub-dependencies are always evaluated before their parent. So depth gives you ordering; breadth does not.
That second fact is the whole game. If App A genuinely needs B installed before C, do not list B and C as two flat dependencies of A and hope – there is no ordering guarantee between siblings. Instead, make C depend on B, and A depend on C. The chain A -> C -> B forces B, then C, then A.
VC++ Redistributable 2015-2022 (leaf prerequisite, "Automatically install" = Yes)
^
| depends on
LOB Runtime 3.2 (depends on VC++)
^
| depends on
Contoso ERP Client 8.1 (depends on LOB Runtime)
Each dependency has an “Automatically install” toggle, defaulting to Yes – Intune installs the prerequisite even if it was never separately assigned. Set it to No only when you want the dependency to be a gate (block the parent) rather than an installer. Pick one model per chain.
Avoid circular chains. Intune blocks obvious cycles at save time, but you can still build a logical deadlock across supersedence and dependency relationships. Keep prerequisites as leaf nodes that depend on nothing, and let higher-level apps depend downward only.
6. Supersedence and replacement: upgrading cleanly
Supersedence is how you ship version N+1 and retire version N without a separate uninstall assignment. The relationship says “this new app supersedes that old one.” There are two distinct behaviors, controlled by a single per-link toggle.
| “Uninstall previous version” | Behavior | Use when |
|---|---|---|
| No (update) | New installer runs over the old; the installer handles the upgrade in place | The MSI/EXE upgrades itself cleanly (most modern MSIs) |
| Yes (replace) | Intune runs the old app’s uninstall command first, then installs the new one | The vendor installs side-by-side, or you are switching products entirely |
Pick No for a normal version bump where the installer upgrades in place – faster, and no window with the app absent. Pick Yes when old and new would otherwise coexist (a different MSI product code, a new vendor) so you do not strand the prior version.
Graph limits mirror dependencies: a supersedence graph tops out at 10 apps beyond the parent. Chains supersede transitively, but two or three hops is plenty.
Two operational caveats that cost people deployments:
- Supersedence requires the superseded app to still exist in Intune and to have a working uninstall command (for the replace path). Delete the old app object and the relationship breaks.
- The detection rule of the superseding app must detect only the new version. If your v2 app’s detection also matches v1, the upgrade is reported “already installed” and never runs. Version-gated detection (
>= 24.08) is what makes supersedence reliable.
7. Return codes, retries, and install behavior gotchas
The IME ships with standard return-code mappings; you can add your own per app. The defaults you must know:
| Code | Meaning | IME behavior |
|---|---|---|
0 |
Success | Run detection |
1707 |
Success | Run detection |
3010 |
Soft reboot | Success; notify user, no forced restart |
1641 |
Hard reboot | Success; restart per your restart settings |
1618 |
Retry | Another install in progress – IME waits ~5 minutes and retries |
Map vendor-specific codes deliberately. If an installer returns 3 on “already up to date,” map 3 to Success so it stops reporting failure. Mark transient codes (lock, pending reboot) as Retry rather than Failed.
Restart behavior has three settings in the Program section: based on return codes (recommended – honor 3010/1641), force a mandatory restart, or suppress. The Restart grace period assignment setting (default 1,440 minutes / 24 hours) only appears for the first two; suppress restarts and the user gets no countdown.
System vs user context is the gotcha that generates the most tickets:
- System (default) is correct for machine-wide installs. The catch: a SYSTEM install cannot show UI to the signed-in user and cannot write per-user locations like
HKCUor%APPDATA%for the real user – it writes to the SYSTEM profile. - User context runs as the signed-in user and can write
HKCU, but needs a user logged in and fails for per-machine MSIs requiring admin rights.
If an app must land in every user’s profile, install machine-wide in SYSTEM context and seed per-user settings via Active Setup or a logon script – do not flip the whole app to user context to chase one HKCU value.
8. Troubleshooting failures: IME logs, AgentExecutor, and ServiceUI
When a Win32 app fails, work the logs top-down:
AppWorkload.log(orIntuneManagementExtension.logon older agents) – find the app by name/ID. You will see the download, the install command, the exit code, and the detection result, each timestamped.AgentExecutor.log– if you used a PowerShell install or detection script, its stdout/stderr and exit code land here. A detection script that “works on my machine” but fails in Intune is almost always the 64-bit vs 32-bit view or a missing STDOUT write.- The IME caches per-app state in the registry under
HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps. TheOperationalStateshows values likeSoftRebootPendingwhile a reboot is outstanding – useful when an app is stuck “in progress.”
Force a re-evaluation in a lab by restarting the Microsoft Intune Management Extension service. To clear a poisoned cache for one app, delete its key under the Win32Apps registry path and restart the service so the IME re-evaluates from scratch.
The ServiceUI pattern (and Microsoft’s caveat)
SYSTEM-context installs are invisible to the user, which is a problem for the rare installer that genuinely needs to prompt. The long-standing community workaround is ServiceUI.exe (shipped with the Microsoft Deployment Toolkit), which bridges a SYSTEM process into the interactive session:
# Wrapper install command, surfacing UI from SYSTEM context
ServiceUI.exe -process:explorer.exe "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File ".\Install-Interactive.ps1"
Be explicit with stakeholders: Microsoft does not formally support interactive Win32 installations through Intune, and calls out tools like ServiceUI as unsupported workarounds that may behave unpredictably. Treat it as a last resort. The supportable path is a silent installer plus, if you must reach the user, a separate notification (toast, PSADT in deploy-user mode) decoupled from the install. To test how an installer behaves in SYSTEM context before shipping, launch it locally with
PsExec -s -ifrom Sysinternals.
Verify
Validate end to end before assigning broadly:
# 1. Confirm the IME service is present and running
Get-Service -Name 'Microsoft Intune Management Extension' | Format-List Status, StartType
# 2. Confirm content cached for an app
Get-ChildItem 'C:\Program Files (x86)\Microsoft Intune Management Extension\Content' -Recurse |
Select-Object FullName, Length
# 3. Run your detection script by hand and check BOTH signals
& 'C:\temp\Detect-App.ps1'; "EXIT=$LASTEXITCODE" # need exit 0 AND printed output
# 4. Read the per-app IME state
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps' -Recurse -ErrorAction SilentlyContinue
- Detection script: must print non-empty STDOUT and exit 0 to count as installed.
- In
AppWorkload.log, confirm the sequence: download succeeded -> install exit code as expected -> detection = installed. - Force a cycle by restarting the IME service; confirm the app does not reinstall on the next pass (proves detection is stable, not looping).
- For a superseded app, confirm v2 detection does not match v1 (or the upgrade silently no-ops).
Enterprise scenario
A platform team rolling out a CAD suite to 9,000 engineering workstations hit a reinstall loop that filled AppWorkload.log and re-downloaded a 6 GB package every hour on a subset of machines. The app installed correctly; detection just kept reporting “not installed.”
The constraint: the vendor’s MSI dropped its main binary under %ProgramFiles%\Vendor\CAD\bin\, but their per-user license activation wrote a marker under HKCU. The original engineer had written a PowerShell detection script that checked the HKCU license key. Because Win32 apps install and evaluate detection in SYSTEM context by default, the script was reading SYSTEM’s HKCU (effectively HKEY_USERS\.DEFAULT), found no license, exited 1, and the IME dutifully reinstalled – forever.
The fix was to stop conflating installed with licensed. They rewrote detection to test the machine-wide install (binary version under %ProgramFiles%), which is what SYSTEM context can actually see, and moved license verification out of detection entirely into a separate compliance check:
# Detection in SYSTEM context: test the machine-wide install, not HKCU
$exe = "$env:ProgramFiles\Vendor\CAD\bin\cad.exe"
if (Test-Path $exe) {
$v = (Get-Item $exe).VersionInfo.ProductVersion
if ([version]$v -ge [version]'2026.1') { Write-Output "CAD $v present"; exit 0 }
}
exit 1
The reinstall loop stopped on the next IME cycle, bandwidth normalized, and licensing became a reporting concern rather than a packaging one. The lesson generalized into a team rule: detection rules may only assert facts visible to the install context. SYSTEM-context apps detect machine-wide artifacts; anything per-user belongs in compliance or Active Setup, never in detection.