← All Guides
🚀

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:

NinjaOne script — install via Chocolatey
$ErrorActionPreference = "Stop"
$package = "googlechrome" # set per deployment, or read a NinjaOne script variable
# 1) Ensure Chocolatey is installed
if (-not (Test-Path "$env:ProgramData\chocolatey\bin\choco.exe")) {
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = 3072 # TLS 1.2
iex ((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 NinjaOne
if ($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:

NinjaOne script — download and silent-install
$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 = 3072
Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing
$p = Start-Process -FilePath $installer -ArgumentList $silent -Wait -PassThru
# 3) Clean up and return the installer exit code
Remove-Item $installer -Force -EA SilentlyContinue
if ($p.ExitCode -in 0,1641,3010) { exit 0 } else { exit $p.ExitCode }

Common silent-install flags

The flag depends on the installer technology:

  • MSImsiexec /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:

NinjaOne script — winget as SYSTEM
$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).Path
if (-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 = $LASTEXITCODE
if ($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.