Every MECM application needs a detection method. That check decides whether the app is already installed, whether an installation succeeded, and whether a newer deployment should supersede it. A bad rule creates false "installed" states, endless reinstalls, or compliance numbers that lie.
I maintain a packaging toolkit with 91 applications in it, so I have had to make this choice a lot of times. Here is how I actually decide. The examples below are the detection blocks my packagers write into their stage manifests, which the package phase turns into MECM detection clauses.
Windows Installer (MSI product code)
The console's native option for MSI installs: MECM checks the Windows Installer database for the product code, optionally comparing the product version. The wizard fills it in from the MSI for you, which makes it the path of least resistance.
You can read the same values out of an MSI without installing it, which is what my packagers do at stage time:
$installer = New-Object -ComObject WindowsInstaller.Installer
$database = $installer.GetType().InvokeMember(
'OpenDatabase', 'InvokeMethod', $null, $installer, @($MsiPath, 0))
$view = $database.GetType().InvokeMember(
'OpenView', 'InvokeMethod', $null, $database,
@("SELECT Property, Value FROM Property"))
# ProductCode, ProductVersion, ProductName, Manufacturer
It is more precise than it looks. Vendors that issue a new product code with every release turn code-only detection into an exact-version check. That works well when each package deploys one version and supersedence handles the generations. Vendors that keep one product code across versions need an added version comparison, or every old installation reads as current.
In my own tooling I usually use the uninstall-key registry check because it behaves the same way for MSI and EXE installers and keeps 91 definitions uniform. For a one-off MSI package, product code is a sensible default.
Registry key value
Read a registry value and compare it, usually DisplayVersion under the application's uninstall key, with an equals or greater-than-or-equal operator.
Detection = @{
Type = "RegistryKeyValue"
RegistryKeyRelative = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{23170F69-...}"
ValueName = "DisplayVersion"
Operator = "IsEquals"
ExpectedValue = "26.00.00.0"
Is64Bit = $true
}
This is my catalog default for MSI and for EXE installers that write a proper Add/Remove Programs entry. The uninstall key is what the vendor's own uninstaller relies on, so it tends to be accurate. 7-Zip, Chrome, and most of my definitions use this.
One thing to watch: the 64-bit flag matters. A 32-bit detection check reads the WOW6432Node view of the registry. Point it at the wrong view and the app is invisible.
Registry key existence
This check asks only whether a registry key exists. There's no version comparison.
Detection = @{
Type = "RegistryKey"
RegistryKeyRelative = "SOFTWARE\Microsoft\Updates\.NET Core\..."
}
I use this when a version comparison is impossible or pointless: runtimes and components that manage their own updates, or products whose registry version format doesn't compare cleanly. The ASP.NET Core Hosting Bundle is a good example. The cost is that existence checks can't tell an old install from a current one, so this only fits applications where that distinction doesn't matter for the deployment.
File
Check whether a file exists or compare the version stamped on the main executable.
Detection = @{
Type = "File"
FilePath = "C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader"
FileName = "AcroRd32.exe"
PropertyType = "Version"
Operator = "GreaterEquals"
ExpectedValue = $detectionVersion
Is64Bit = $false
}
This is the right tool when the registry story is unreliable but the binary's version resource is honest: Adobe Reader, Firefox, most Electron-style apps. It's also the only sane option for per-user installs (VS Code user setup, Postman), where the detection has to run in the user context and look under the user's profile.
Watch for vendors whose file version and marketing version disagree, such as a 25.1.0 product with an executable stamped 25.1.0.8842. The packaging pipeline has to record the version as detection will see it. The version advertised on the download page may fail the comparison.
Compound
Compound detection joins multiple clauses with AND or OR.
AND is for products that are really two installs pretending to be one. The .NET Desktop Runtime is the clearest case: the x86 and x64 runtimes install independently, into version-stamped paths, and a machine that only got one of them is not done.
# .NET Desktop Runtime 8 -- both architectures must be present.
# The version lives in the path itself, so existence is the version check.
$x86 = "${env:ProgramFiles(x86)}\dotnet\host\fxr\$version"
$x64 = "$env:ProgramFiles\dotnet\host\fxr\$version"
Detection = @{
Type = "Compound"
Connector = "And"
Clauses = @(
@{ Type = "File"; FilePath = $x86; FileName = "hostfxr.dll"
PropertyType = "Existence"; Is64Bit = $false },
@{ Type = "File"; FilePath = $x64; FileName = "hostfxr.dll"
PropertyType = "Existence"; Is64Bit = $true }
)
}
That detection needs no version comparison at all. hostfxr.dll sits in a folder named for the runtime version, so the path is the assertion — on a host with 8.0.29, 9.0.18, and 10.0.10 side by side, each rule matches only its own folder.
OR is for products with two legitimate installed states — Microsoft Edge, where either of two file paths may be the real one depending on how it arrived:
Detection = @{
Type = "Compound"
Connector = "Or"
Clauses = @(
@{ Type = "File"; FilePath = $detectionPath1; FileName = "msedge.exe"
PropertyType = "Version"; Operator = "GreaterEquals"
ExpectedValue = $version; Is64Bit = $false },
@{ Type = "File"; FilePath = $detectionPath2; FileName = "msedge.exe"
PropertyType = "Version"; Operator = "GreaterEquals"
ExpectedValue = $version; Is64Bit = $false }
)
}
Compound rules can also verify a package's customizations. Many packages carry a policy that disables the vendor's auto-updater, a preferences file, or an agent configuration ID along with the vendor's files. When that customization matters, detection should prove it landed by checking the policy value or configuration file alongside the version. A machine with a hand-installed, unconfigured copy could otherwise read as compliant while the fleet drifts away from the standard. At minimum, validate the customization explicitly during testing. Detection is the basis for the compliance numbers you will see later.
Script
A PowerShell script reports whether the app is present.
The contract is narrow: write something to stdout and exit 0 when the app is present, and write nothing when it is not. Any output at all counts as detected, so an unguarded error message can read as a successful install.
# Detect: agent registered with the right tenant.
# Not a registry or file rule -- the tenant ID only exists in WMI.
$agent = Get-CimInstance -Namespace 'root\VendorAgent' -ClassName 'AgentConfig' `
-ErrorAction SilentlyContinue
if ($agent -and $agent.TenantId -eq '4f2c...') { Write-Output 'Installed' }
exit 0
I use this as a last resort for state that won't fit a registry or file rule. Some products expose the useful information only through WMI. Others put it inside an XML, JSON, or text file. PowerShell can inspect those values directly.
Scripts stay at the bottom of my list because they are harder to audit, slower to evaluate, and prone to failures that the console doesn't surface well. When I write one, I add a comment explaining why the simpler detection types couldn't represent the state. The next person who opens it shouldn't have to reconstruct that decision.
One more thing: MECM also accepts VBScript detection scripts, and older environments are full of them. VBScript is deprecated and on its way out of Windows. Treat every detection VBS as migration work with a deadline and replace it with PowerShell on a planned schedule.
The short version
MSI with per-version product codes → product code. MSI or well-behaved EXE in a uniform catalog → registry value. Self-updating runtime → registry existence. Reliable binary version → file version. Per-user app → file, user context. Two-part product → compound. Important customization → a clause that proves it landed. State available only through WMI or inside a file → PowerShell script, with an explanation in the comments. VBScript → replace on a planned schedule.