Deploy custom applications with scripts
Create a script-based App Hub app — install, detection, and optional update/uninstall scripts — and roll it out with Software Deployment.
Deploy custom applications with scripts
Winget, Chocolatey, and Flathub cover the common catalogue apps, but plenty of software is not in any of them: an internal line-of-business tool, a vendor MSI behind a download URL, a .deb from a private repository, a macOS .pkg. For those, App Hub has a Script source: you write the install (and optionally update, uninstall, and detection) commands yourself, and Software Deployment runs them on the target devices in the agent's context.
This guide covers the whole flow: creating a script-based app in App Hub, writing an install script that reports success or failure correctly, adding an optional detection script so already-installed devices are skipped, deploying the app, and reading the per-device results.

Before you start
- Required permissions:
collections_enabled,collections_app_hub_enabledto create the app; pluscollections_software_deployment_enabledandcollections_software_deployment_addfor the deployment. - Target devices are online and assigned a policy.
- You know the silent-install command line for your software (for example an MSI's
/qnswitches, an installer's/S,apt-get install -y, orinstaller -pkg). - The install and detection scripts run as the agent —
SYSTEMon Windows,rooton Linux and macOS. There is no interactive user, so every command must be fully non-interactive.
Create the application
- Open
Collections → App Hub(route/app_hub_manage_apps). - Click
Addto open the Add Manual App dialog. - Fill in the shared metadata:
Name(required) — how the app appears in the catalogue and the deployment wizard.Description,Version,Publisher,Author,License,Homepage— optional metadata.- An icon upload (
.png,.ico,.jpg/.jpeg, up to 256 KB).
- Under Source, choose
Script. (The other sources —Winget (Windows),Chocolatey (Windows),Flatpak (Linux)— are package-manager entries and do not take scripts.) - Pick the
Script Shellthat matches the platform you are targeting:PowerShellfor Windows,Bashfor Linux,Zshfor macOS. This sets the editor language and the shell your scripts run in. - Fill in the script editors:
Install Script(required) — the commands that install the software.Update Script— run when a deployment item uses theupdateaction.Uninstall Script— run when a deployment item uses theuninstallaction.Detection Script(optional) — decides whether the app is already installed; see The detection script.
- Set
Target OStoWindows,Linux, ormacOS. Keep this consistent with theScript Shell— the install script runs in that platform's native shell (PowerShell on Windows, Bash on Linux, Zsh on macOS). - Leave
Requires Elevationon so the install runs asSYSTEM/root. (Turning it off routes Windows installs to the user session through the Tray Icon; see the note in The detection script.) - Optionally set
Tagsas a JSON array, e.g.["dev","cli"]. - Click
Confirm. The app appears in the App Hub list with thescriptsource chip.
The Edit App dialog exposes the same fields against an existing entry.
The install script
The install script is the heart of a script-based app. A few rules govern how it runs and how its result is judged:
- Context. It runs as the agent —
SYSTEMon Windows,rooton Linux/macOS — with no interactive desktop. Use silent/quiet installer switches and absolute paths. - Shell. Windows scripts run through PowerShell, Linux through Bash, macOS through Zsh.
- Timeout. The install script has a 30-minute timeout. If it exceeds that, the agent kills it and the item is reported as failed.
- Success rule. This is the important one: a script install is marked failed only when its captured output starts with the text
Error(case-insensitive). Exit codes are not evaluated, and a thrown exception or output on stderr alone does not mark it failed. If the script produces no output beginning withError, the item is reported assuccess— even if the underlying installer returned a non-zero exit code.
Because of the success rule, you must make your script emit a line beginning with Error: on every failure path — check the installer's exit code (or catch the exception) and write the error to standard output yourself. A bare throw or exit 1 is not enough on its own.
PowerShell:
$ErrorActionPreference = 'Stop'
try {
$p = Start-Process msiexec.exe -ArgumentList '/i', 'C:\path\to\app.msi', '/qn', '/norestart' -Wait -PassThru
if ($p.ExitCode -ne 0) { Write-Output "Error: msiexec exited $($p.ExitCode)"; exit 1 }
Write-Output "MyApp installed."
}
catch {
Write-Output "Error: $($_.Exception.Message)"
exit 1
}Bash / Zsh:
if ! apt-get install -y ./app.deb; then
echo "Error: apt-get install failed"
exit 1
fi
echo "MyApp installed."Write idempotent scripts. The same install script can run more than once on a device — for example when detection is absent, or when a deployment uses Force reinstall. Write the script so that running it on a machine that already has the app is harmless: check before you act, use installer switches that repair or upgrade in place, and avoid steps that fail when a previous run already completed them.
The detection script
A detection script answers one question before an install runs: is this application already installed on the device? If it is, NetLock RMM skips the install and reports the device as Skipped instead of reinstalling. This is the same idea as an Intune detection rule — a small script that returns "present" or "absent" — but written as a shell snippet you control. It is optional; without it, every deployment install runs the installer unconditionally.
The rules are fixed and identical across every source type and platform:
- The detection script runs on the device, before every install — never for
updateoruninstall. - It runs in the same shell as the install script: PowerShell on Windows, Bash on Linux, Zsh on macOS.
- It runs in the agent's context (SYSTEM on Windows, root on Linux/macOS), with a 5-minute timeout.
- Exit code 0 AND at least one non-whitespace character on stdout → installed. The install is skipped and the job item is reported as
Skipped(which counts as a success, not a failure). - Any other result → not installed. A non-zero exit code, or exit code 0 with no output, runs the install normally.
- A detection crash or timeout is treated as "not installed" (fail-open): the install proceeds, and the failure is logged and noted in the result output.
- An empty detection script means the install always runs.
- Force reinstall on a deployment bypasses detection entirely — the installer always runs.
v1 limitation: Windows apps configured with "Requires elevation" turned off run in the user session through the Tray Icon and do not run detection. Their installs always proceed.
Note that detection and install use different success rules. Detection cares about the exit code plus stdout (0 + output = installed); the install cares about whether its output starts with Error. Keep that in mind when reusing snippets between the two editors.
Deploy the application
App Hub is only the catalogue — installing the app on devices happens through Software Deployment.
- Open
Collections → Software Deployment(route/software_deployment) and start aNew Deployment. - Step 1 — Packages. Select your script app and choose the
installaction. (updateruns the Update Script;uninstallruns the Uninstall Script.) - Step 2 — Targets. Pick the devices, groups, locations, or tenants that should receive it. Keep the Target step aligned with the app's
Target OS— a Windows PowerShell app will fail on Linux devices. - Step 3 — Config. Set the mode, schedule, retries, and flags. Leave
Force reinstalloff to let detection skip already-installed devices; turn it on to reinstall everywhere (this bypasses detection entirely, so no device is reported asSkipped). - Step 4 — Review. Confirm and submit.
For a fuller walkthrough of the wizard, see Deploy software with App Hub.
Read the results
Open the deployment's detail page. The per-device results table shows an outcome chip per device:
success(green) — the install script ran and did not emit output starting withError.failed(red) — the install script's output started withError, or it timed out.Skipped(blue) — the detection script reported the app as already installed, so the install did not run. This counts as a success.
Click into a device to open the per-device results dialog. Per attempt it shows the exit code, duration, timestamps, an error summary, and the stdout tail / stderr tail. For a skipped device the stdout tail carries a [detection] note explaining that the app was already installed. This is the primary forensic view when something goes wrong.
Complete examples
Each example is an install-and-detection pair for one platform.
Windows (PowerShell) — MSI install + file-path detection:
# Install Script
$ErrorActionPreference = 'Stop'
try {
$installer = "$env:TEMP\myapp.msi"
Invoke-WebRequest -Uri 'https://example.com/myapp.msi' -OutFile $installer -UseBasicParsing
$p = Start-Process msiexec.exe -ArgumentList '/i', "`"$installer`"", '/qn', '/norestart' -Wait -PassThru
if ($p.ExitCode -ne 0) { Write-Output "Error: msiexec exited $($p.ExitCode)"; exit 1 }
Write-Output "MyApp installed."
}
catch {
Write-Output "Error: $($_.Exception.Message)"
exit 1
}# Detection Script
if (Test-Path 'C:\Program Files\MyApp\MyApp.exe') { Write-Output 'installed'; exit 0 } else { exit 1 }Linux (Bash) — .deb install + command detection:
# Install Script
tmp="$(mktemp --suffix=.deb)"
if ! curl -fsSL -o "$tmp" 'https://example.com/myapp.deb'; then
echo "Error: download failed"
exit 1
fi
if ! apt-get install -y "$tmp"; then
echo "Error: apt-get install failed"
exit 1
fi
echo "MyApp installed."# Detection Script
command -v myapp >/dev/null 2>&1 && echo installed && exit 0
exit 1macOS (Zsh) — pkg install + app-bundle detection:
# Install Script
tmp="$(mktemp -d)/myapp.pkg"
if ! curl -fsSL -o "$tmp" 'https://example.com/myapp.pkg'; then
echo "Error: download failed"
exit 1
fi
if ! installer -pkg "$tmp" -target /; then
echo "Error: installer failed"
exit 1
fi
echo "MyApp installed."# Detection Script
[ -d '/Applications/MyApp.app' ] && echo installed && exit 0
exit 1Troubleshooting
- The deployment reports
success, but the app did not install. A script install is marked failed only when its standard output starts withError. A non-zero exit code, a thrown exception, or a message on stderr does not mark it failed on its own. Add explicit failure handling that writes a line beginning withError:to standard output on every failure path — see The install script. - The app always installs, even where it is already present. Detection reported "not installed". Check that the detection script exits
0and prints at least one non-whitespace line when the app is present — exit0with no output counts as "not installed". Also confirmForce reinstallis off. On Windows user-context apps ("Requires elevation" off), detection never runs by design. - The app is always skipped, even on clean devices. Your detection script is exiting
0with output on machines that do not have the app. Tighten the check (a specific file path or a specific registryDisplayName) so it only succeeds when the app is truly present. - Detection errored (crash or timeout). The install proceeded anyway (fail-open) and the per-device
stdout tailcarries a[detection] script failednote. Detection has a hard 5-minute timeout — keep the script fast and avoid network calls. - The install script is killed mid-run. The install script has a 30-minute timeout. If your installer legitimately needs longer, break the work up or pre-stage large downloads.
- Windows user-context installs never skip. This is the v1 limitation: apps with "Requires elevation" off run through the Tray Icon in the user session, which does not run detection. Mark the app as requiring elevation if you need detection and the installer can run as
SYSTEM.
Related
- Chapter 8.5 — App Hub — the catalogue, manual and browser-based entries, and the script fields.
- Chapter 8.8 — Software Deployment — the deployment reference, including the per-device result detail and the
Force reinstallflag. - Deploy software with App Hub — deploying a Winget catalogue app end to end.
- Guide H.7 — Write, test, and schedule a PowerShell script — for one-off scripts that are not packaged as App Hub apps.