Information on and Troubleshooting Detection Scripts

Information on and Troubleshooting Detection Scripts

What is a Detection Script?

A detection script determines whether an application is already installed on a target machine. It runs before install/uninstall actions and after the actions to verify success. The UAM engine uses the script's return value to decide whether to install, skip, or upgrade an app.

Detection scripts are used by:

  • Shell Apps — always required
  • Winget Apps — optional custom detection (overrides default winget query)

Return Value Rules

Your detection script must return one of the following:

Return Meaning
A version string (e.g. "1.2.3") App is installed at this version
$true App is installed (version unknown — upgrade decisions cannot be made)
$false App is NOT installed
$null App is NOT installed

Any other return type will cause an immediate failure for that action.


Troubleshooting: Symptoms and Causes

Symptom: Entire deployment fails with no results reported

Likely cause: Using exit in your detection script

# WRONG — causes total deployment failure
if (-not (Test-Path "C:\Program Files\MyApp")) {
    exit 1
}

The exit command terminates the entire deployment process — not just your script. No results are reported back to the portal, and the deployment appears to hang or timeout.

This also applies to:

  • exit 0
  • exit 1
  • [System.Environment]::Exit(0)

Fix: Use return instead:

if (-not (Test-Path "C:\Program Files\MyApp")) {
    return $null  # Not installed
}
return $true

Symptom: Action fails with "unexpected type" error

Likely cause: Returning a complex object or an array

# WRONG — returns a registry object, not a string
return Get-ItemProperty "HKLM:\SOFTWARE\MyApp"

# WRONG — returns an array
return @("1.0", "2.0")

# WRONG — returns an integer
return 1

Fix: Always cast your return value explicitly:

$app = Get-ItemProperty "HKLM:\SOFTWARE\MyApp" -ErrorAction SilentlyContinue
if ($app) {
    return [string]$app.DisplayVersion
}
return $null

Symptom: Action fails with "unexpected type" even though you return a string

Likely cause: Using Write-Output in your script

# WRONG — Write-Output adds to the return value
Write-Output "Checking for MyApp..."
$version = (Get-ItemProperty "HKLM:\SOFTWARE\MyApp").Version
return $version

Any Write-Output (or bare string expressions) adds data to the script's output. The final return becomes a mixed array instead of a clean string.

Fix: Use $context.Log() for diagnostic messages:

$context.Log("Checking for MyApp...")
$version = (Get-ItemProperty "HKLM:\SOFTWARE\MyApp" -ErrorAction SilentlyContinue).Version
if ($version) {
    $context.Log("Found: $version")
    return [string]$version
}
return $null

Symptom: Action reports failure even though the app isn't installed

Likely cause: Using throw to signal "not found"

# WRONG — throw means "error", not "not installed"
$app = Get-Package -Name "MyApp" -ErrorAction SilentlyContinue
if (-not $app) {
    throw "Application not found"
}

An exception in a detection script is treated as a script error, not as "app not installed." The action fails rather than proceeding with installation.

Fix: Return $null or $false to signal the app is absent:

$app = Get-Package -Name "MyApp" -ErrorAction SilentlyContinue
if ($app) {
    return [string]$app.Version
}
return $null

Symptom: Deployment hangs indefinitely or times out

Likely cause: Unbounded loop or long-running operation in detection script

# WRONG — may never exit
while ((Get-Service "MyAppService").Status -ne "Running") {
    Start-Sleep -Seconds 5
}
return $true

Detection scripts have no individual timeout. A script that never finishes blocks the entire deployment until the platform-level timeout is reached (which can take over an hour).

Fix: Always use bounded checks:

$maxAttempts = 3
for ($i = 0; $i -lt $maxAttempts; $i++) {
    $svc = Get-Service "MyAppService" -ErrorAction SilentlyContinue
    if ($svc -and $svc.Status -eq "Running") {
        return $true
    }
    Start-Sleep -Seconds 2
}
return $null

Symptom: Detection is unreliable — sometimes reports "not installed" for apps that are present

Likely cause: Detection depends on network or external services

# WRONG — network may be unavailable
$response = Invoke-RestMethod "https://api.myapp.com/status"
if ($response.installed) { return $response.version }
return $null

Network connectivity is not guaranteed during deployment (especially on newly provisioned VMs). DNS failures or API timeouts will produce false "not installed" results, triggering unnecessary reinstalls.

Fix: Always detect locally using registry, file system, or services:

$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MyApp"
$app = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
if ($app) {
    return [string]$app.DisplayVersion
}
return $null

Symptom: App is detected but triggers an unexpected upgrade (or skip)

Likely cause: Returned version doesn't match the version names defined in your app

For Shell Apps, the returned version string should match one of the version names configured in your app definition. If you return "1.0.0.1234" but your version is named "1.0.0", the system may make incorrect upgrade or skip decisions.

Fix: Return the exact version name that matches your app's version definitions:

$installedBuild = (Get-ItemProperty "HKLM:\SOFTWARE\MyApp").DisplayVersion
# Map to the version names defined in your shell app
$knownVersion = $context.Versions | Where-Object { $installedBuild -like "$($_.Name)*" } | Select-Object -First 1
if ($knownVersion) {
    return $knownVersion.Name
}
return $null

Symptom: Detection works in testing but fails during deployment

Likely cause: Cmdlet throws a terminating error you're not catching

# Fails if the registry path doesn't exist
$version = (Get-ItemProperty "HKLM:\SOFTWARE\NonExistent\MyApp" -ErrorAction Stop).Version

When run on a machine where the app isn't installed, this throws an error that is treated as a detection failure (not "app not installed").

Fix: Use -ErrorAction SilentlyContinue for all probing operations:

$app = Get-ItemProperty "HKLM:\SOFTWARE\MyApp" -ErrorAction SilentlyContinue
if ($app -and $app.Version) {
    return [string]$app.Version
}
return $null

Quick Reference: Do's and Don'ts

DO

  • Use return to provide your result
  • Return a [string] version, $true/$false, or $null
  • Use $context.Log() for diagnostic messages
  • Use -ErrorAction SilentlyContinue when probing for app presence
  • Wrap your entire script in try/catch for safety
  • Keep detection fast (under 5 seconds)
  • Detect locally (registry, files, services)

DON'T

  • Never use exit (in any form)
  • Never use Write-Output (corrupts return value)
  • Never use throw to mean "not installed"
  • Never rely on network/API calls for detection
  • Never use unbounded loops
  • Never modify system state (env vars, registry writes, stopping services)
  • Never return objects, arrays, or integers

Recommended Template

Shell App

try {
    $context.Log("Detecting MyApp")

    # Primary: Registry check
    $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}"
    $app = Get-ItemProperty $regPath -ErrorAction SilentlyContinue

    if ($app -and $app.DisplayVersion) {
        $context.Log("Found version: $($app.DisplayVersion)")
        return [string]$app.DisplayVersion
    }

    # Fallback: File check
    $exePath = "C:\Program Files\MyApp\myapp.exe"
    if (Test-Path $exePath) {
        $version = (Get-Item $exePath).VersionInfo.ProductVersion
        $context.Log("Found binary version: $version")
        return [string]$version
    }

    $context.Log("MyApp not found")
    return $null
}
catch {
    $context.Log("Detection error: $($_.Exception.Message)")
    return $null
}

Winget App (Custom Detection)

try {
    $context.Log("Detecting app (target: $($context.TargetVersion))")

    $regPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}"
    )

    foreach ($path in $regPaths) {
        $app = Get-ItemProperty $path -ErrorAction SilentlyContinue
        if ($app -and $app.DisplayVersion) {
            $context.Log("Detected: $($app.DisplayVersion)")
            return [string]$app.DisplayVersion
        }
    }

    $context.Log("Not detected")
    return $null
}
catch {
    $context.Log("Error: $($_.Exception.Message)")
    return $null
}

Available Context

Property/Method Shell Apps Winget Apps Description
$context.TargetVersion Yes Yes The version being installed (or "not_specified")
$context.DetectedVersion Yes No Previously detected version (if known)
$context.Versions Yes No List of all defined version names
$context.Log("message") Yes Yes Write to deployment logs
$context.GetAttachedBinary() Yes No Download the app's binary package

Need help?

Raise a support ticket for this item.

Was this article helpful?

0 out of 0 found this helpful
Have more questions? Submit a request

Comments (0 comments)

Please sign in to leave a comment.