Patch one named server on a schedule by starting a background Windows Update install, and check its progress later
Patch a specific device, typically a server outside your normal patch tool, on a schedule: every N days the action starts installing its missing Windows updates without restarting it, and you can check on the install afterwards. For MSPs and IT teams with a few servers that must be patched the same way every week.
| Runs in | On the device (PowerShell, as SYSTEM) |
| Uses | None |
| Suggested approval | Specific groups (your engineers) → No approval needed, so scheduled runs don't wait for an approver |
| 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
SuperIT can't repeat a chat command on a timer, and creating a scheduled task on the device is denied by the default command policies. An action can run on a schedule, for example every 7 days on SRV-APP02, with the same reviewed script each time. It runs as SYSTEM and isn't checked against command policies; its authorization policy decides who can run it.
For one device, now, just ask SuperIT in chat: it can list missing updates and run the install, and a chat command can run for up to 30 minutes, longer than a device action's 5.
When to use it
- "Install updates on SRV-APP02 every Saturday night."
- A line-of-business server that your patch tool doesn't cover must be patched weekly.
- "Did last night's scheduled update install on SRV-APP02 finish?"
What the action does
The mode input picks one of three jobs. Every mode first stops, without changing anything, if expected_hostname isn't this device's computer name.
- Report
- Asks the Windows Update Agent for software updates that aren't installed, hidden or optional (
BrowseOnly). Driver updates and feature upgrades (the Upgrades category) aren't included. - Prints each update's title, KB number, severity and size, and whether a restart is already pending.
- Asks the Windows Update Agent for software updates that aren't installed, hidden or optional (
- Install
- Refuses to start if a previous install is still running.
- Creates (or replaces) a scheduled task called
SuperIT action - Install Windows updates, which runs as SYSTEM, and starts it. - Returns straight away. The task searches the same way, accepts licence terms, downloads and installs every update that doesn't need user input, and writes a log to
C:\Windows\Logs\WindowsUpdateAction.log.
- Status
- Prints the task's state, the last 25 lines of the log, and whether a restart is pending.
The background task exists only because device actions time out after 5 minutes by default and installing updates often takes longer: Install hands the work to a scheduled task on the device instead of waiting for it.
What it doesn't do:
- It never restarts the device. Use Schedule a reboot with a warning when Status says a restart is needed.
- It doesn't choose individual KBs, install drivers, optional preview updates or feature upgrades, or unhide updates.
- It doesn't remove its scheduled task. The task stays on the device, with no trigger, after the install. See Remove the scheduled task.
- It doesn't override WSUS, Windows Update for Business or Intune. The device searches whichever source it's configured to use, and policies such as deferrals still apply.
Before you begin
- A Windows device with the SuperIT RMM agent, online. 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 that doesn't need approval for the schedule's owner. For example: Requester Specific groups
superit:engineers, Approver No approval needed. Don't add an Anyone rule. A scheduled run uses the current policy, so a run that needs approval waits, and expires after 7 days if nobody approves it. See Authorization policies and approvals.
Build it with SuperIT
Create an action called "Check and install Windows updates".
Runtime: PowerShell, running on the device as SYSTEM (execution target: device). Timeout 5 minutes (the default).
Inputs: expected_hostname (string, required), mode (select: Report, Install, Status; required; default Report).
Stop before doing anything unless expected_hostname matches $env:COMPUTERNAME.
Report: Microsoft.Update.Session search "IsInstalled=0 and IsHidden=0 and BrowseOnly=0 and Type='Software'", skip updates in the Upgrades category (match CategoryID 3689bdc8-b205-4af4-8d4a-a63924c5e9d5, not the localized name), print title, KBs, severity, size; print pending restart.
Install: refuse if the task "SuperIT action - Install Windows updates" is running; otherwise register it (SYSTEM, highest, -EncodedCommand, no trigger) and start it. The task uses the same search and skip, downloads and installs all updates that can't request user input, accepts EULAs, logs to C:\Windows\Logs\WindowsUpdateAction.log, ends with a "DONE" or "FAILED" line, and never restarts.
Status: task state, last 25 log lines, pending restart.
Windows PowerShell 5.1 compatible. Authorization policy: superit:engineers, no approval needed.
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 | Example |
|---|---|---|---|
expected_hostname |
string | Yes | SRV-APP02 |
mode |
select (Report, Install, Status) |
Yes | Report |
On a device, the script runs in Windows PowerShell as SYSTEM with $ErrorActionPreference = "Stop" already set. Inputs arrive as strings in $ctx.inputs. Invoke-WindowsUpdateAction 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.
# Report: lists missing software updates. Install: starts a SYSTEM scheduled task that installs
# them without restarting. Status: shows that task's state and log. Prints whether a restart is pending.
function Invoke-WindowsUpdateAction {
param($ExpectedHostname, $Mode)
Assert-TargetDevice -ExpectedHostname $ExpectedHostname
$m = Assert-Inputs -Mode $Mode
$taskName = 'SuperIT action - Install Windows updates'
$logPath = "$env:SystemRoot\Logs\WindowsUpdateAction.log"
$restartPending = Test-RestartPending
Write-Output "=== WINDOWS UPDATES: $($m.ToUpper()) ==="
switch ($m) {
'Report' { Write-UpdateReport -RestartPending $restartPending }
'Install' { Start-UpdateInstall -TaskName $taskName -LogPath $logPath }
'Status' { Write-InstallStatus -TaskName $taskName -LogPath $logPath -RestartPending $restartPending }
}
}
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 changed." }
}
function Assert-Inputs {
param($Mode)
if ("$Mode" -notin @('Report', 'Install', 'Status')) { throw "mode must be Report, Install or Status." }
"$Mode"
}
function Test-RestartPending {
$cv = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion'
(Test-Path "$cv\WindowsUpdate\Auto Update\RebootRequired") -or (Test-Path "$cv\Component Based Servicing\RebootPending")
}
function Write-UpdateReport {
param([bool]$RestartPending)
Write-Output "Searching for updates on $env:COMPUTERNAME..."
$updates = @(Find-MissingUpdate)
foreach ($u in $updates) { Write-Output (" {0} [{1}] {2} ({3} MB)" -f $u.kb, $u.severity, $u.title, $u.size_mb) }
Write-Output "Missing updates: $($updates.Count). Restart pending: $RestartPending."
# $result is read after the script ends, so set it at script scope, not in this function.
$script:result = @{
missing = @($updates | ForEach-Object { @{ title = $_.title; kb = $_.kb; severity = $_.severity } })
restart_pending = $RestartPending
}
}
function Find-MissingUpdate {
try {
$searcher = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher()
# BrowseOnly=0 leaves out optional updates, such as preview cumulative updates.
$search = $searcher.Search("IsInstalled=0 and IsHidden=0 and BrowseOnly=0 and Type='Software'")
} catch {
throw "Failed to search for updates on $($env:COMPUTERNAME): $_"
}
foreach ($u in $search.Updates) {
if (@($u.Categories | Where-Object { $_.CategoryID -eq '3689bdc8-b205-4af4-8d4a-a63924c5e9d5' }).Count -gt 0) { continue }
@{
title = $u.Title
kb = (@($u.KBArticleIDs) | ForEach-Object { "KB$_" }) -join ','
severity = "$($u.MsrcSeverity)"
size_mb = [math]::Round($u.MaxDownloadSize / 1MB)
}
}
}
function Start-UpdateInstall {
param([string]$TaskName, [string]$LogPath)
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existing -and $existing.State -eq 'Running') { throw "An update install is already running. Use Status to follow it." }
Register-UpdateTask -TaskName $TaskName
Add-Content -Path $LogPath -Value ("{0:yyyy-MM-dd HH:mm:ss} Install started from SuperIT" -f (Get-Date))
Start-ScheduledTask -TaskName $TaskName
Write-Output "Started the update install in the background. Run this action with mode Status to follow it."
$script:result = @{ started = $true; log = $LogPath }
}
function Register-UpdateTask {
param([string]$TaskName)
# A task, not this script, does the install: it outlives the action's timeout.
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes((Get-UpdateJobScript)))
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -EncodedCommand $encoded"
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $TaskName -Action $action -Principal $principal -Force | Out-Null
}
function Get-UpdateJobScript {
@'
$log = "$env:SystemRoot\Logs\WindowsUpdateAction.log"
function Log($m) { Add-Content -Path $log -Value ("{0:yyyy-MM-dd HH:mm:ss} {1}" -f (Get-Date), $m) }
try {
Log 'Searching for updates'
$s = New-Object -ComObject Microsoft.Update.Session
$r = $s.CreateUpdateSearcher().Search("IsInstalled=0 and IsHidden=0 and BrowseOnly=0 and Type='Software'")
$c = New-Object -ComObject Microsoft.Update.UpdateColl
foreach ($u in $r.Updates) {
if (@($u.Categories | Where-Object { $_.CategoryID -eq '3689bdc8-b205-4af4-8d4a-a63924c5e9d5' }).Count -gt 0) { Log "Skipped (feature upgrade): $($u.Title)"; continue }
if ($u.InstallationBehavior.CanRequestUserInput) { Log "Skipped (needs user input): $($u.Title)"; continue }
if (-not $u.EulaAccepted) { $u.AcceptEula() }
[void]$c.Add($u); Log "Queued: $($u.Title)"
}
if ($c.Count -eq 0) { Log 'DONE: nothing to install' } else {
$d = $s.CreateUpdateDownloader(); $d.Updates = $c; [void]$d.Download(); Log 'Downloaded'
$i = $s.CreateUpdateInstaller(); $i.Updates = $c; $res = $i.Install()
for ($n = 0; $n -lt $c.Count; $n++) { Log ("Result code {0}: {1}" -f $res.GetUpdateResult($n).ResultCode, $c.Item($n).Title) }
Log ("DONE: result code {0}, restart needed {1}" -f $res.ResultCode, $res.RebootRequired)
}
} catch { Log "FAILED: $($_.Exception.Message)" }
'@
}
function Write-InstallStatus {
param([string]$TaskName, [string]$LogPath, [bool]$RestartPending)
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
$state = 'never run'
if ($task) { $state = "$($task.State)" }
Write-Output "Install task: $state. Restart pending: $RestartPending."
$tail = @()
if (Test-Path $LogPath) { $tail = @(Get-Content -Path $LogPath -Tail 25) }
foreach ($line in $tail) { Write-Output " $line" }
$script:result = @{ task_state = $state; restart_pending = $RestartPending; log_tail = $tail }
}
Invoke-WindowsUpdateAction -ExpectedHostname $ctx.inputs.expected_hostname -Mode $ctx.inputs.mode
In the log, result code 2 means succeeded, 3 succeeded with errors, 4 failed and 5 aborted.
Remove the scheduled task
The task SuperIT action - Install Windows updates runs as SYSTEM and stays on the device after the install, with no trigger, so it runs only when this action starts it. When you stop patching the device with this action, remove it in an elevated PowerShell on the device: Unregister-ScheduledTask -TaskName 'SuperIT action - Install Windows updates' -Confirm:$false.
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 is real, so use a test device.
- Publish the action under Workflows → Actions.
- Select Run, enter the test device's computer name in
expected_hostname, choose Report, pick that device under Run target, and select Run action. Check Stdout. A search can take a minute or two. - Run it with Install. The run finishes in seconds.
- Run it with Status every few minutes until the log ends with a
DONEorFAILEDline. - Run it with a different name in
expected_hostnameand confirm it fails with "Nothing was changed".
Use it
- On a schedule. Ask SuperIT "Run Check and install Windows updates in Install mode on SRV-APP02 every 7 days, starting Saturday at 10pm, with expected_hostname SRV-APP02". A schedule is tied to one device, so create one per server. Pair it with a later scheduled restart on the same device.
- Check the result. Scheduled runs don't send their output anywhere. Ask SuperIT "Run Check and install Windows updates in Status mode on SRV-APP02" the next morning, or see the run under the action's Executions tab.
- From the app. Open the action, select Run, enter the device's name, choose the mode, pick the same device under Run target, and select Run action.
- Not from tickets automatically. 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 device named in the ticket.
- 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
- Post the result to your team's channel: schedule a Status run a few hours after Install, and add the webhook call from Post a message to a Teams or Slack channel to
Write-InstallStatus. Actions can't call each other, so the call goes in this script. - Install only named KBs: add a
kb_numbersinput and skip updates whoseKBArticleIDsaren't in it. - Include driver updates by removing
Type='Software'from both searches.