TL;DR
- Main limitation: Direct remote calls to the Windows Update Agent COM API can fail with an access-denied error because it expects a local interactive context.
- Recommended workaround: Use the PSWindowsUpdate module with Invoke-WUJob to create a scheduled task on the remote machine under the SYSTEM account.
- Core cmdlets: Use Get-WindowsUpdate to list available updates, Install-WindowsUpdate to install them, and Invoke-WUJob to run the process remotely.
- Install the module: PSWindowsUpdate must be available on both the initiating machine and the remote target.
- Patch all updates: Use Install-WindowsUpdate with
-AcceptAlland-AutoRebootfor unattended installation and restart handling. - Target one update: Use
-KBArticleIDto deploy a specific Microsoft KB to selected computers. - Patch multiple devices: Store computer names in a variable and pass the list to
-ComputerName. - Monitor jobs: Use Get-WUJob to check whether scheduled update tasks completed or returned errors.
- Improve reliability: Add retry logic because some updates depend on servicing-stack or prerequisite updates being installed first.
- Capture logs: Pipe update output to a log file for troubleshooting, auditing, and later review.
- Check patch levels: Query the operating-system version, build number, and Update Build Revision rather than relying only on the Windows product name.
- Handle offline devices: Use try/catch logic so unreachable machines are marked clearly instead of stopping the entire inventory script.
- Security warning: Broad administrative credentials and remote scripts can increase lateral-movement risk, so use delegated, granular, or just-in-time permissions where possible.
- Compatibility note: PSWindowsUpdate was originally designed around Windows PowerShell 5.1, so PowerShell 7 compatibility should be tested before production use.
In most organizations, Windows Updates are managed by Windows Server Update Services (WSUS) and Group Policy across the entire environment for security purposes, and client machines download and install them automatically from the WSUS server. But there are exceptions where critical servers or a large fleet of servers need to be updated manually; therefore, GPOs are not applied to those servers. If the number of servers is small, you can easily remote desktop into them and trigger updates manually; however, when the environment is very large, with hundreds of servers, this is not an option, and you would want to leverage a remote option.
Now, one way to manage automated Windows Updates is by accessing the Windows Update Agent COM API, such as using the Microsoft.Update.Session COM object in a New-Object cmdlet in a custom script. You can easily run that script using the below command in the PowerShell of local machine.
Invoke-Command -ComputerName Server1 -FilePath ‘C:\Windows Update Scripts\WindowsUpdate.ps1’
Now, as any system administrator, you would think that this cmdlet can be easily run using PowerShell remoting, but it is not; running this cmdlet remotely would give you the below generic error.
Access is denied. (Exception from HRESULT: 0x80070005 E_ACCESSDENIED))”
Now this problem is not related to PowerShell, as the Invoke-Command cmdlet is the foundation of PowerShell remoting and successfully runs on local and remote computers. This problem is related to the Windows Update Agent COM API, which offers administrators some control over the update process, but is not specifically designed for remote use; it was designed for local and interactive use and does not accept commands over the network. Cmdlets run over a remote PowerShell session use a network logon, whereas Windows Update COM API expects an interactive local login; hence, it blocks the action.
This is why admins precisely need an indirect PowerShell approach rather than direct remote call. PowerShell can be used to work around this limitation by installing the PSWindowsUpdate module and using the Invoke-WUJob cmdlet, which creates a local scheduled task on remote machines that runs under the local system account to execute any command or script.
Installing PSWindowsUpdate
PSWindowsUpdate PowerShell module is available via the PowerShell Gallery and is maintained by the community. It wraps the WUA COM API into easy-to-use cmdlets with remote logic that Microsoft did not build in a native utility. It must be installed on both the machine requesting the action and the target machine, executing the actual update logic still supported on the local machine. You can install and import the module by running the below cmdlets.
Install-Module -Name PSWindowsUpdate -Force
Import-Module PSWindowsUpdate
It is recommended to run the PowerShell console as a local administrator in the scripts rather than passing the -Credential parameter to every cmdlet, since the local administrator account already has permissions on the target machines. Most organizations add either the Domain Admins group or a custom group to the local Administrator group to enable remote access.
PSWindowsUpdate module provides several cmdlets, but the three cmdlets below are used most frequently for remote update management.
- Invoke-WUJob
- Get-WindowsUpdate
- Install-WindowsUpdate
Invoke-WUJob
It is an orchestration cmdlet; it does not check for updates or install them; it just creates a scheduled task under the SYSTEM account in Windows Task Scheduler on a remote machine for running a script block, resolving the remote execution “Access is Denied” issue. This cmdlet just creates the task rather than calling the Windows Update COM API, serving as a delivery mechanism only.
Invoke-WUJob -ComputerName “Windows10” -Script { Script Block } -RunNow
Get-WindowsUpdate
Get-WindowsUpdate is a read-only cmdlet; it does not make any changes to the system, just queries and lists the updates which are available for the remote machine but not yet installed. It is mostly run first to determine what would be installed before committing an install job, which makes it very useful for reporting, audit purposes, and getting visibility into patch status. A sample remote cmdlet would be as follows:
Get-WindowsUpdate -ComputerName “win10two”
Or running the below cmdlet including -Install parameter to get the updates and install them (Did not add parameter, just illustration)
Invoke-command -ComputerName “Win10two” -Script {Import-Module PSWindowsUpdate;Get-WindowsUpdate}
Install-WindowsUpdate
This cmdlet is that actually downloads and installs the pending updates on remote machines. This cmdlet can be passed to a script or code block within the Invoke-WUJob cmdlet, with parameters such as -AcceptAll to skip the interactive confirmation prompt for each update and -AutoReboot to handle reboot behavior after updates are installed. This cmdlet is not only meant to run remotely; you can also safely run it locally for direct patching. Below is a sample cmdlet to install all the available updates with automatic reboot.
Install-WindowsUpdate -AcceptAll -AutoReboot
Ensure that PSWindowsUpdate module and all its NuGet package provider dependencies are present on the local machine before running the Invoke-WUJob cmdlet, you can auto-bootstrap these commands in the script block as below or as per your requirement.
-Script “Install-PackageProvider NuGet -Force; Install-Module PSWindowsUpdate -Force; Import-Module PSWindowsUpdate; Install-WindowsUpdate -AcceptAll
PSWindowsUpdate module was initially built for PowerShell 5.1 and before when the executable name was powershell.exe, Microsoft explicitly changed the executable name as pwsh.exe to separate it completely from legacy version which only supports Windows, but PowerShell 7 is cross platform. Admins who script for cross platform must test compatibility explicitly rather than relying on behavior.
Running scripts remotely with an admin account on multiple machines can introduce lateral movement risk, if that account or management workstation gets compromised, attacker can gain access to every target machine in one step. It is recommended to use more delegated, granular or just-in-time permissions in high security environments.
Pushing the Updates (Invoke-WUJob)
This section explains the Invoke-WUJob and its script block in detail, which performs live update on one or multiple machines. There are various methods supported such as one or two liner cmdlets in script block to a fully scripted production wide patching by placing the script on remote machines and running them using the task created by Invoke-WUJob.
For a full production wide patching, use the below script as a standard approach, which creates a schedule under SYSTEM account context, installs all the updates, reboots the remote machines when needed and then logs the output in a file for later review.
Invoke-WUJob -ComputerName “SRV2022” -Script “ipmo PSWindowsUpdate; Install-WindowsUpdate -AcceptAll -AutoReboot | Out-File C:\PSUpdateLogs\WindowsUpdate.log” -RunNow -Confirm:$false
The above script mentions -ComputerName parameter for one remote machine, but you can create a variable with list of computers and use that to perform the job on multiple machines. -AcceptAll parameter bypasses the Yes/No prompt for all updates returned by the query, including quality updates, security updates and feature updates.
$Computers = @(“Server-1″,”Server-2”, “Laptop-1”)
If you are a small environment or want to use the script as ad hoc scenarios such as installing a single KB on some machines, the below script will first trust the target host for remoting using Set-Item cmdlet. Then will use the Invoke-WUJob to create schedule, Get-WindowsUpdate cmdlet with -KPArticleID to target a specific KB article rather than fetching all available updates.
Set-Item WSMan:\localhost\Client\TrustedHosts -Value * -Force
Invoke-WUJob -ComputerName “Desktop1” -Script {
Get-WindowsUpdate -KBArticleID “KB5034441” -AcceptAll -Install -Verbose
} -Confirm:$false -RunNow
Above script installs a Windows 10 Security update which was released to patch a Windows BitLocker device encryption vulnerability, attackers can bypass BitLocker encryption using the Windows Recovery Environment.
Both scripts can be used for different purpose and scale. Entire device deployment script is right choice for unattended and recurring patch cycles for multiple machines, with complete auditing and reboot handling. While the other script can be used for single purpose tasks such as single update or troubleshooting a specific security update on a single machine before deployment on all computers.
Sometimes Windows Update jobs do not fully complete due to some updates depending on other updates being installed first, such as Servicing stacks, it is recommended to add retry login into the script or run the same job again, it will re-check the pending updates, and this time with few, and will compete successfully.
You can also check the scheduled update task status by using the Get-WUJob cmdlet. This cmdlet will produce output with fields like Job State with value as either completed or with any error code returned. It helps administrators to check the status of multiple machines and retry updates on any which failed.
If you are using a variable to store all the computers being patched, you can use that variable in -ComputerName parameter.
Get-WUJob -ComputerName $Computers
If ran the job using specific computer names, use below cmdlet.
Get-WUJob -ComputerName “SRV2022”,”Desktop1”
Below is a complete retry script sample, which runs in a loop and checks update results using Get-WUJob, automatically runs the Invoke-WUJob for any machines which report failure.
$Computers = “Server01″,”Server02″,”Server03”
$MaxRetries = 2
for ($i = 0; $i -le $MaxRetries; $i++) {
Invoke-WUJob -ComputerName $Computers
-Script “ipmo PSWindowsUpdate; Install-WindowsUpdate -AcceptAll -AutoReboot | Out-File C:\Windows\PSWindowsUpdate.log”
-RunNow -Confirm:$false
Start-Sleep -Seconds 30
$Status = Get-WUJob -ComputerName $Computers
$Failed = $Status | Where-Object { $_.State -ne “Completed” }
if (-not $Failed) { break }
$Computers = $Failed.ComputerName
}
Determine Windows Version
It is a common practice by admins to create an inventory of what exact Operating System versions and builds are installed on each machine in their environment, for planned migration such as from Windows 10 to 11 or any Windows server upgrade and maintaining an ongoing compliance baseline. This can be easily done using any third-party patching tool or Windows Server Update Services (WSUS) but some administrators have been using the PowerShell scripts to perform the same job and want to stick to that due to maintaining those scripts over the decade.
Returning only the OS name is not sufficient as two machines with same OS could be entirely on a different patch level and can be exposed to vulnerabilities still not patched, the build number and Update Build Revision (UBR) are the details actually matter, as they pinpoint exact cumulative updates installed. An example would be two Windows Server 2022 on build 20348.1668 and 20348.2113 respectively, but both are several months apart by security updates.
You can query the version info for each computer using Get-CimInstance Wind32_OperatingSystem for newer Windows or using Get-WmiObject on older operating systems. These cmdlets pull the OS captions, version and build number with single command, you can put these cmdlets into a loop targeting multiple computers, a sample cmdlet would be as follows:
Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName “Server01”
CIM produces the ouput in a raw format, but that information can be wrapped in a PSCustomObject to produce a clean report with property names, which can be exported to CSV, can be filtered, sorted and used in other reporting tools, a sample custom object can be as follows:
[PSCustomObject]@{ ComputerName = “Server01”; OSVersion = “10.0.20348”; Build = “20348.2113” }
It should be in consideration that some machines could be offline or not reachable due to a firewall rule, admins should use a try/catch method to catch errors and define a clear status such as “Offline” or “Unreachable”, rather than failed machine stopping the entire script. An example would be as follows:
ComputerName: Server03, OSVersion: “Unreachable”, Build: “N/A”
Below is a base script to determine OS version across computers using above cmdlets and PowerShell custom object, producing clean status as Online or Offline.
$Computers = “win10″,”win10two”
$Results = foreach ($Computer in $Computers) {
try {
$OS = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $Computer -ErrorAction Stop
[PSCustomObject]@{
ComputerName = $Computer
OSName = $OS.Caption
Version = $OS.Version
Status = “Online”
}
} catch {
[PSCustomObject]@{
ComputerName = $Computer
OSName = “N/A”
Version = “N/A”
Status = “Unreachable”
}
}
}
$Results | Format-Table -AutoSize
The output of the above script will be as follows:
Above base script has a limitation, as WMI fetches the current operating system version of the computers e.g. 19045, but does not provide the exact cumulative update installed such as 19045.4046, that is why the script should pull the UBR from the following registry path:
HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion
Invoke-Command using Get-ItemProperty for getting the build number from registry should be added in the script. Below is a complete compliance ready script for determining OS version, build for each computer.
$Computers = “win10″,”win10two”
$Results = foreach ($Computer in $Computers) {
try {
$OS = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $Computer -ErrorAction Stop
$Reg = Invoke-Command -ComputerName $Computer -ScriptBlock {
Get-ItemProperty “HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion”
} -ErrorAction Stop
[PSCustomObject]@{
ComputerName = $Computer
OSName = $OS.Caption
Version = $OS.Version
Build = “$($Reg.CurrentBuildNumber).$($Reg.UBR)”
Status = “Online”
}
} catch {
[PSCustomObject]@{
ComputerName = $Computer
OSName = “N/A”
Version = “N/A”
Build = “N/A”
Status = “Unreachable”
}
}
}
$Results | Format-Table -AutoSize
This script generates the following output in a clean tabular format.









