Installing Software with NinjaOne Scripting
Deploy software to endpoints at scale with NinjaOne PowerShell scripts — winget, Chocolatey, and direct-download methods, silent-install flags, idempotency checks, and the exit codes that make automations reliable.
NinjaOne runs PowerShell on your managed endpoints, which makes it a powerful software-deployment engine — but a few RMM-specific gotchas trip people up, chiefly that scripts run as SYSTEM with no logged-in user profile. This guide covers the three reliable install methods, how to make scripts idempotent (safe to re-run), and the exit codes NinjaOne watches to decide success or failure.
How NinjaOne runs scripts (read this first)
Everything below depends on understanding the execution context:
- Scripts run as `NT AUTHORITY\SYSTEM` by default — full machine rights, but no user profile, no mapped drives, and no per-user app data. You can also choose *Run As: Logged-on User* or supplied credentials per script.
- No interactive desktop — anything that pops a window or waits for a click will hang. Every installer must run silently/unattended.
- 64-bit vs 32-bit — NinjaOne can invoke PowerShell in either; some registry checks differ. Prefer 64-bit for modern software.
- You get the script’s exit code back — NinjaOne marks the result Success or Failure from it, so returning the right code matters (covered at the end).
winget is per-user — SYSTEM can’t call it directly
The winget command lives in a user profile, so a script running as SYSTEM usually can’t find it. For unattended RMM installs, prefer Chocolatey (machine-wide, SYSTEM-friendly) or a direct download + silent install. There is a winget workaround (resolving its path under Program Files\WindowsApps), shown below, but the other two methods are more reliable at scale.
Method 1 — Chocolatey (best for fleet installs)
Chocolatey installs machine-wide and runs cleanly as SYSTEM, which is why it’s the RMM favorite. A single script bootstraps Chocolatey if missing, then installs (or upgrades) the package idempotently:
$ErrorActionPreference = "Stop"$package = "googlechrome" # set per deployment, or read a NinjaOne script variable# 1) Ensure Chocolatey is installedif (-not (Test-Path "$env:ProgramData\chocolatey\bin\choco.exe")) {Set-ExecutionPolicy Bypass -Scope Process -Force[System.Net.ServicePointManager]::SecurityProtocol = 3072 # TLS 1.2iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))}$choco = "$env:ProgramData\chocolatey\bin\choco.exe"# 2) Install or upgrade (idempotent - safe to re-run on every machine)& $choco upgrade $package -y --no-progress$code = $LASTEXITCODE# 3) Pass Chocolatey/MSI exit codes through to NinjaOneif ($code -in 0,1641,3010) { exit 0 } else { exit $code }
Use upgrade, not install
choco upgrade installs the package if it’s missing and updates it if it’s outdated — so the same script works whether the endpoint has the app or not. That idempotency is what lets you safely attach it to a policy that runs fleet-wide.
Method 2 — Direct download + silent install
When a vendor ships its own MSI/EXE (and it’s not in Chocolatey), download and run it with the silent flags. Check whether it’s already installed first so you don’t reinstall on every run:
$ErrorActionPreference = "Stop"$appName = "7-Zip" # display name to detect$url = "https://www.7-zip.org/a/7z2408-x64.exe"$installer = "$env:TEMP\7zsetup.exe"$silent = "/S" # 7-Zip uses NSIS /S; MSIs use /qn# 1) Skip if already installed (check both 64- and 32-bit uninstall keys)$paths = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*","HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*")$installed = Get-ItemProperty $paths -EA SilentlyContinue |Where-Object { $_.DisplayName -like "*$appName*" }if ($installed) { Write-Output "$appName already installed"; exit 0 }# 2) Download over TLS 1.2 and install silently[System.Net.ServicePointManager]::SecurityProtocol = 3072Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing$p = Start-Process -FilePath $installer -ArgumentList $silent -Wait -PassThru# 3) Clean up and return the installer exit codeRemove-Item $installer -Force -EA SilentlyContinueif ($p.ExitCode -in 0,1641,3010) { exit 0 } else { exit $p.ExitCode }
Common silent-install flags
The flag depends on the installer technology:
- MSI —
msiexec /i app.msi /qn /norestart(quiet, no reboot) - NSIS installers —
/S(capital S) - Inno Setup —
/VERYSILENT /SUPPRESSMSGBOXES /NORESTART - InstallShield —
/s /v"/qn"(often needs a response file)
When in doubt, run installer.exe /? on a test machine — most reveal their silent switches.
Method 3 — winget from SYSTEM (when you must)
If a package is only practical via winget, resolve its full path under WindowsApps and call the executable directly rather than the winget alias:
$ErrorActionPreference = "Stop"$appId = "Notepad++.Notepad++"# Resolve the machine-wide winget executable (Desktop App Installer)$wingetPath = (Resolve-Path "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe\winget.exe" -EA SilentlyContinue |Sort-Object Path | Select-Object -Last 1).Pathif (-not $wingetPath) { Write-Output "winget not available in SYSTEM context"; exit 1 }& $wingetPath install --id $appId --silent --accept-package-agreements --accept-source-agreements --scope machine$code = $LASTEXITCODEif ($code -in 0,1641,3010) { exit 0 } else { exit $code }
Deploying the script across your fleet
Once a script is in the NinjaOne Scripting library, you can run it several ways:
- Ad hoc — run against one device or a multi-device selection for a one-off push
- Scheduled automation / policy — attach the (idempotent) script to a policy so every device in a group converges to “has this app”; new devices get it automatically
- As a condition response — pair with a monitor (e.g. “app missing”) so NinjaOne self-heals by reinstalling
- With script variables — parameterize the package name/URL so one script serves many apps instead of duplicating it per application
Stage before you ship
Test every install script on a single pilot device (or a Hyper-V VM — see Test in a VM) before attaching it to a fleet-wide policy. A bad silent flag that hangs is annoying on one machine and a mass incident on 500.
Exit codes: how NinjaOne judges success
NinjaOne marks a script result from its exit code, so translate installer codes deliberately:
- `0` — success
- `3010` — success, reboot required (very common for MSIs). Treat as success, then schedule the reboot separately.
- `1641` — success, installer initiated a reboot
- `1618` — another install is in progress (Windows Installer is busy) — worth a retry
- anything else — real failure; return it so NinjaOne flags the device
The pattern in every script above — if ($code -in 0,1641,3010) { exit 0 } else { exit $code } — reports honest success while not failing a device just because it needs a reboot.
Log what you do
Use Write-Output generously — NinjaOne captures script output per device, and “which machines actually got Chrome and which errored” is exactly what you’ll want when a rollout is half-done. Silent success with no log is impossible to audit.