Install apps from a list you approve, machine-wide, with winget, on the device you name

Keep a catalogue of apps you've approved, and install one on a named Windows device, for every user of the device. The action installs the chosen package from the winget community repository by its exact ID. Engineers run it, and end users can ask for it with an engineer's approval. For MSPs and IT teams who get a steady stream of "please install Zoom" requests.

Runs in On the device (PowerShell, as SYSTEM)
Uses None
Suggested approval Specific groups (your engineers) → No approval needed, then Anyone → Require approval from an engineer group. Put licensed apps in a second action that needs a manager's approval
Category Devices

Test it with SuperIT before you rely on it

This is an example, not a ready-made action, and it hasn't been tested end to end. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, and adjust it together. Then run it on a test device, as in Test it. Device actions can't use Test, so every run is real.

Why use an action instead of asking SuperIT

Engineers don't need this action: SuperIT can run winget as SYSTEM in a chat after one approval, and a chat command can run for up to 30 minutes, longer than a device action's 5. End users can't do that, because a machine-wide install needs admin rights and end users are never asked to approve commands. This action runs as SYSTEM, isn't checked against command policies, and installs only the packages in its select list. Its authorization policy can let end users ask for one of those packages, with an engineer approving each run.

To install something once on a device, or anything not in your catalogue, an engineer can just ask SuperIT in chat: "install 7-Zip on LAPTOP-042".

When to use it

  • An end user asks "Can you install 7-Zip on my laptop?"
  • A ticket says "I need VLC for a training video."
  • A new starter needs the standard apps your team allows everyone to have.

What the action does

  1. Reads package_id, one choice from your approved list (for example 7zip.7zip).
  2. Stops without changing anything if expected_hostname isn't this device's computer name.
  3. Checks the ID is on the same approved list in the script, so a copy with a free-text input still can't install anything else.
  4. Finds winget.exe in the App Installer folder, because winget isn't on the path for SYSTEM.
  5. Runs winget install with the exact ID from the winget source, machine scope, silent, and with the source and package agreements accepted. If the package is already installed, winget may try to upgrade it or may return a non-zero code; the output shows which.
  6. Prints winget's output and exit code, and fails the run if winget reports an error.

What it doesn't do:

  • It doesn't install from the Microsoft Store (msstore source). That source doesn't work as SYSTEM.
  • It doesn't install per-user apps. Packages that offer only a user-scope installer fail with "No applicable installer found".
  • It doesn't install anything outside your list. Find each package's exact ID first with winget search or winget show on any PC, and add it to both the select choices and the script.
  • It doesn't pin versions. It installs the latest version in the winget repository.
  • It doesn't install winget itself or work on most Windows Server versions, where App Installer isn't present.

Winget as SYSTEM: what to know

These notes come from Microsoft's documentation, not from SuperIT testing. winget's behaviour as SYSTEM varies by App Installer version and Windows edition, so check them on your own devices.

  • winget comes with App Installer, a Store app. On a freshly imaged device, App Installer may not be registered until a user has signed in, so winget.exe may be missing. The script stops with a clear message if it can't find it.
  • Machine scope only. As SYSTEM there is no user profile to install into. The script passes --scope machine.
  • Agreements must be accepted on the command line, because nobody can answer a prompt.
  • Installers can take a long time. Device actions time out after 5 minutes by default, and you can raise that up to 60. Large packages (Office, Visual Studio, Adobe Acrobat) may not finish in time; a timed-out run may still be installing on the device.
  • Exit codes from winget are large negative numbers. 0 is success; anything else is shown in the output for you to look up in Microsoft's winget return code list.

Before you begin

  • A Windows 10 or 11 device with the SuperIT RMM agent, online, with App Installer present. Actions run on Windows devices only; the macOS and Linux agents are in Beta and can't run actions. See Requirements.
  • Your role. Creating an action in a chat needs the Admin role. Engineers and Admins can build it under Workflows → Actions.
  • No credential. See Credentials.
  • An authorization policy. With None, only Engineers and Admins can run it, without approval. To let end users ask for an app, use two rules: Specific groups superit:engineers → No approval needed, then Anyone → Require approval from superit:engineers. See Authorization policies and approvals.
  • Your approved list. Decide which free apps may be installed. Leave out anything licensed, anything that needs configuring after install, and large installers that won't finish within the timeout.

Device actions and end users

A device action runs as SYSTEM on the device chosen for the run, or on the action's default device or your team's designated device if none is chosen. SuperIT doesn't check that the device belongs to the person who asked. Never give this action an Anyone → No approval needed rule. Approvers should check which device the run is for before approving.

Build it with SuperIT

Create an action called "Install approved software".
Runtime: PowerShell, running on the device as SYSTEM (execution target: device). Timeout 5 minutes (the default).
Inputs: expected_hostname (string, required), package_id (select, required) with choices 7zip.7zip, VideoLAN.VLC, Mozilla.Firefox, Notepad++.Notepad++, Zoom.Zoom.
Stop before changing anything unless expected_hostname matches $env:COMPUTERNAME. Check package_id is in the same list inside the script. Find winget.exe under C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe (newest version), fail clearly if missing.
Run: winget install --id <id> --exact --source winget --scope machine --silent --accept-source-agreements --accept-package-agreements.
Capture output without failing on stderr, drop progress-bar lines, print the exit code, fail on non-zero.
Windows PowerShell 5.1 compatible. Authorization policy: superit:engineers no approval; Anyone requires approval from superit:engineers.

SuperIT confirms the brief, drafts the script, validates it and creates the action as a draft. Review the script, then publish it under Workflows → Actions.

Or build it yourself

Create the action under Workflows → Actions with the PowerShell runtime and these inputs, then set it to run on a device.

Name Type Required Choices or example
expected_hostname string Yes LAPTOP-042
package_id select Yes 7zip.7zip, VideoLAN.VLC, Mozilla.Firefox, Notepad++.Notepad++, Zoom.Zoom

Enter the package IDs under Choices (comma-separated). SuperIT refuses any other value before the run starts. On a device, the script runs in Windows PowerShell as SYSTEM with $ErrorActionPreference = "Stop" already set. Inputs arrive as strings in $ctx.inputs. Invoke-WingetInstall at the top lists the steps in order; each step is a small function below it, and the last line runs it. $result is set as $script:result, because a plain $result = inside a function stays local and never reaches the Result tab.

# Installs one package from the approved list machine-wide by exact winget ID, prints
# winget's output and exit code, and fails the run if winget reports an error.
function Invoke-WingetInstall {
    param($ExpectedHostname, $PackageId)

    Assert-TargetDevice -ExpectedHostname $ExpectedHostname
    $id = Assert-Inputs -PackageId $PackageId
    $winget = Find-Winget
    $arguments = @(Get-WingetArgument -PackageId $id)

    Write-Output '=== WINGET INSTALL ==='
    Write-Output "Running winget $($arguments -join ' ')"
    $run = Invoke-Winget -Path $winget -Arguments $arguments
    Write-WingetOutput -Lines $run.lines
    Write-Output "winget exit code: $($run.code)"

    # $result is read after the script ends, so set it at script scope, not in this function.
    $script:result = @{ package_id = $id; exit_code = $run.code }
    if ($run.code -ne 0) { throw "winget couldn't install $id (exit code $($run.code))." }
}

function Assert-TargetDevice {
    param($ExpectedHostname)
    $expected = ("$ExpectedHostname".Trim() -split '\.')[0]
    if (-not $expected) { throw "expected_hostname is required: enter this device's computer name." }
    if ($expected -ne $env:COMPUTERNAME) { throw "This run reached '$env:COMPUTERNAME', not '$expected'. Nothing was installed." }
}

function Assert-Inputs {
    param($PackageId)
    # Repeats the select's choices so a copy with a free-text input still installs only these.
    $approved = '7zip.7zip', 'VideoLAN.VLC', 'Mozilla.Firefox', 'Notepad++.Notepad++', 'Zoom.Zoom'
    $id = "$PackageId".Trim()
    $match = @($approved | Where-Object { $_ -eq $id })
    if ($match.Count -eq 0) { throw "'$id' isn't on the approved software list." }
    # winget's --exact is case-sensitive, so pass the ID as written in the list.
    $match[0]
}

function Find-Winget {
    # winget isn't on SYSTEM's path; it lives in the newest App Installer package folder.
    $exe = Get-ChildItem -Path "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe\winget.exe" -ErrorAction SilentlyContinue |
        Sort-Object { [version](($_.Directory.Name -split '_')[1]) } -Descending |
        Select-Object -First 1
    if (-not $exe) { throw "winget isn't available on $env:COMPUTERNAME. App Installer may be missing or not yet registered." }
    $exe.FullName
}

function Get-WingetArgument {
    param([string]$PackageId)
    'install', '--id', $PackageId, '--exact', '--source', 'winget', '--scope', 'machine', '--silent',
        '--accept-source-agreements', '--accept-package-agreements'
}

function Invoke-Winget {
    param([string]$Path, [string[]]$Arguments)
    # winget writes progress to stderr; under "Stop" the first such line would end the script.
    $ErrorActionPreference = 'Continue'
    $lines = @(& $Path @Arguments 2>&1 | ForEach-Object { "$_" })
    @{ code = $LASTEXITCODE; lines = $lines }
}

function Write-WingetOutput {
    param([string[]]$Lines)
    # Drop progress bars, which are lines of block characters.
    $Lines | Where-Object { $_ -match '[A-Za-z]' -and $_ -notmatch '[^\x00-\x7F]{3,}' } | ForEach-Object { Write-Output $_.Trim() }
}

Invoke-WingetInstall -ExpectedHostname $ctx.inputs.expected_hostname -PackageId $ctx.inputs.package_id

The output filter drops winget's progress bars, which are lines of block characters.

To add an app to the catalogue, add its ID to the select's choices and to $approved, then publish again.

Test it

You can't use Test on an action that runs on a device. (On a cloud-sandbox action, Test isn't a dry run either: it runs the script for real and skips approvals.) Every device run really installs software, so use a test device.

  1. Publish the action under Workflows → Actions.
  2. Select Run, enter the test device's computer name in expected_hostname, choose a small package such as 7zip.7zip, pick that device under Run target, and select Run action.
  3. Check Stdout for "Successfully installed" and exit code 0.
  4. Run it again with the same package and note what winget reports for a package that's already installed.
  5. Run it with a different name in expected_hostname and confirm it fails with "Nothing was installed".
  6. If end users can ask for it, sign in to SuperIT as a test end user, ask "Can you install VLC on my laptop?", and confirm the run waits for an engineer's approval.

Use it

  • In a chat. Select the device and ask "Install 7-Zip from the approved list on this device". SuperIT matches the app to an approved ID and fills in the inputs. Check expected_hostname is the device you selected.
  • By end users, with approval. End users ask SuperIT in self-service chat, for example "Can you install Zoom?". The run waits for an engineer to approve it; check which device it's for first. End users see whether the run succeeded, not winget's output.
  • Not from tickets automatically. Investigations can recommend it on software requests, but don't switch on Run automatically for this action. A run started from a ticket's Recommended actions panel goes to the action's default device or your team's designated device, not the computer of the person who raised the ticket. To act on a ticket, open a chat, select the device and run it there.
  • From the app. Open the action, select Run, enter the device's name, choose the package, pick the same device under Run target, and select Run action.
  • For clients. MSPs can publish it to managed teams.

Always pick the device. If no device is chosen, SuperIT runs a device action on the action's default device or your team's designated device, and expected_hostname stops the run if that's the wrong one.

Variations

  • Licensed apps: copy the action with a second list, such as Adobe Acrobat or Visio, and a policy with Require approval from a group of managers or licence owners.
  • Pin versions: add '--version', '<version>' per package in Get-WingetArgument when you need everyone on the same version.
  • MSI from a URL, for software that isn't in winget. The installer runs as SYSTEM, so check it before running it:
    • download only over https://, to a new folder under C:\Windows\Temp;
    • compare Get-FileHash -Algorithm SHA256 with a hash you pinned in the script, and stop if it differs;
    • check Get-AuthenticodeSignature returns Valid and the signer is the publisher you expect;
    • only then run msiexec /i <file> /qn /norestart.