NetLock RMMNetLock RMM Docs
III — How-To Guides

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.

App Hub manual app dialog with the Script source selected

Before you start

  • Required permissions: collections_enabled, collections_app_hub_enabled to create the app; plus collections_software_deployment_enabled and collections_software_deployment_add for 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 /qn switches, an installer's /S, apt-get install -y, or installer -pkg).
  • The install and detection scripts run as the agentSYSTEM on Windows, root on Linux and macOS. There is no interactive user, so every command must be fully non-interactive.

Create the application

  1. Open Collections → App Hub (route /app_hub_manage_apps).
  2. Click Add to open the Add Manual App dialog.
  3. 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).
  4. Under Source, choose Script. (The other sources — Winget (Windows), Chocolatey (Windows), Flatpak (Linux) — are package-manager entries and do not take scripts.)
  5. Pick the Script Shell that matches the platform you are targeting: PowerShell for Windows, Bash for Linux, Zsh for macOS. This sets the editor language and the shell your scripts run in.
  6. Fill in the script editors:
    • Install Script (required) — the commands that install the software.
    • Update Script — run when a deployment item uses the update action.
    • Uninstall Script — run when a deployment item uses the uninstall action.
    • Detection Script (optional) — decides whether the app is already installed; see The detection script.
  7. Set Target OS to Windows, Linux, or macOS. Keep this consistent with the Script Shell — the install script runs in that platform's native shell (PowerShell on Windows, Bash on Linux, Zsh on macOS).
  8. Leave Requires Elevation on so the install runs as SYSTEM/root. (Turning it off routes Windows installs to the user session through the Tray Icon; see the note in The detection script.)
  9. Optionally set Tags as a JSON array, e.g. ["dev","cli"].
  10. Click Confirm. The app appears in the App Hub list with the script source 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 — SYSTEM on Windows, root on 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 with Error, the item is reported as success — 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 update or uninstall.
  • 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.

  1. Open Collections → Software Deployment (route /software_deployment) and start a New Deployment.
  2. Step 1 — Packages. Select your script app and choose the install action. (update runs the Update Script; uninstall runs the Uninstall Script.)
  3. 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.
  4. Step 3 — Config. Set the mode, schedule, retries, and flags. Leave Force reinstall off to let detection skip already-installed devices; turn it on to reinstall everywhere (this bypasses detection entirely, so no device is reported as Skipped).
  5. 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 with Error.
  • failed (red) — the install script's output started with Error, 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 1

macOS (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 1

Troubleshooting

  • The deployment reports success, but the app did not install. A script install is marked failed only when its standard output starts with Error. 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 with Error: 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 0 and prints at least one non-whitespace line when the app is present — exit 0 with no output counts as "not installed". Also confirm Force reinstall is 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 0 with output on machines that do not have the app. Tighten the check (a specific file path or a specific registry DisplayName) 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 tail carries a [detection] script failed note. 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.