← script library

Patch Windows and Reboot with a PowerShell Runbook

RunbooksAugust 17, 2026

The full patch run as seven steps: check the uplink, check administrator rights, make sure the NuGet provider and the PSWindowsUpdate module are present, search for updates, install them with an exclusion list, and reboot in a way the automation survives. Every prerequisite is its own step, so a failure tells you which one is missing instead of leaving you with a patch job that stopped for no visible reason.

powershell
# ===== Step 1 of 7: 01-CHECK-Internet.ps1 =====

# Check Internet Connection
#-----------------------------------------

$internet = (Test-Connection 8.8.8.8 -Count 1 -Quiet -ErrorAction SilentlyContinue)

if ($internet) {
	Write-Host "<WRITE-LOG = ""*Internet connection: Available*"">"
}
else{
	Write-Error "Internet connection: Unavailable"
}

# ===== Step 2 of 7: 02-CHECK-AdminRights.ps1 =====

# Ensure Administrator previlegues
#------------------------------------------------------

$isadm = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isadm) {
    Write-Host "<WRITE-LOG = ""*Please run this script as Administrator.*"">"
    Write-Host "<WRITE-LOG = ""*If this was a remote execution please provide Administrator credentials.*"">"
    Write-Error "Warning: Not running as Administrator."
}

# ===== Step 3 of 7: 03-CHECK-PackageProvider.ps1 =====

# Check Packageprovider NuGet
#---------------------------------------

Write-Host "<WRITE-LOG = ""*Checking for NuGet package provider...*"">"

# Install NuGet provider if not present
if (!(Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
	Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -WhatIf:$false -ErrorAction SilentlyContinue
	Write-Host "<WRITE-LOG = ""*NuGet package provider installed successfully*"">"
} else {
    Write-Host "<WRITE-LOG = ""*NuGet package provider already available*"">"
}

# ===== Step 4 of 7: 04-CHECK-UpdateModule.ps1 =====

# Install/Update WU Moduel
#-----------------------------------------

Write-Host "<WRITE-LOG = ""*Checking PSWindowsUpdate module...*"">"

# Install PSWindowsUpdate module
$module = Get-Module -Name PSWindowsUpdate -ListAvailable
if (!$module) {
	Install-Module -Name PSWindowsUpdate -Force -AllowClobber -Confirm:$false -SkipPublisherCheck
	Write-Host "<WRITE-LOG = ""*PSWindowsUpdate module installed successfully*"">"
} else {
	Write-Host "<WRITE-LOG = ""*PSWindowsUpdate module already installed*"">"
}

# ===== Step 5 of 7: 05-CHECK-WindowsUpdates.ps1 =====

# Check Windows Updates
#----------------------------------------------------------

# Import the module
Import-Module PSWindowsUpdate -Force
Write-Host "<WRITE-LOG = ""*PSWindowsUpdate module imported*"">"
Write-Host "<WRITE-LOG = ""*Searching for Windows Updates...*"">"

# Get available updates with error handling
try {
	$updates = Get-WindowsUpdate -MicrosoftUpdate -ErrorAction Stop
	$count = $updates.Count
	Write-Host "<WRITE-LOG = ""*Found $count Updates...*"">"
	$store = $updates
} catch {
	Write-Host "<WRITE-LOG = ""*ERROR: Failed to search for updates: $($_.Exception.Message)*"">"
	Write-Error "ERROR: Failed to search for updates: $($_.Exception.Message)"
}

# ===== Step 6 of 7: 06-INSTALL-WindowsUpdates.ps1 =====

# Install Windows Updates, exclude KBs if needed
#------------------------------------------------------------

$updates = $store
Write-Host "<WRITE-LOG = ""*Load Store: $store*"">"

# Search Windows Updates again if needed
$updates = Get-WindowsUpdate -MicrosoftUpdate -ErrorAction Stop

if ($updates.Count -gt 0) {
	Write-Host "<WRITE-LOG = ""*Found $($updates.Count) update(s) to install*"">"

	# Display update details
	$updates | ForEach-Object {
	Write-Host "<WRITE-LOG = ""*Update: $($_.Title) (KB$($_.KB))*"">"
	}

	Write-Host "<WRITE-LOG = ""*Installing Windows Updates...*"">"

    $scriptBlock = {
	    try {
            Import-Module PSWindowsUpdate -Force

			# Hide problematic updates (expand this list as needed)
			$problematicKBs = @()
		    <# $problematicKBs = @('KB5034439', 'KB5034441',  'KB5005463', 'KB4535680', 'KB5008876') #>
	        foreach ($kb in $problematicKBs) {
                try {
					$update = Get-WindowsUpdate -KBArticleID $kb -ErrorAction SilentlyContinue
					    if ($update) {
                            Hide-WindowsUpdate -KBArticleID $kb -Confirm:$false
                            Write-Output "Hidden problematic update: KB$kb"
                        }
				} catch {
                        Write-Warning "Could not hide KB$kb : $($_.Exception.Message)"
				}
			}

			# Get and install updates
		    $updatesToInstall = Get-WindowsUpdate -MicrosoftUpdate -Install -AcceptAll -AutoReboot:$false -IgnoreReboot

		    # Stop any running PSWindowsUpdate scheduled tasks
	        $runningTasks = Get-ScheduledTask | Where-Object { $_.TaskName -like '*PSWindowsUpdate*' -and $_.State -eq 'Running' }
            foreach ($task in $runningTasks) {
	            try {
                    Stop-ScheduledTask -TaskName $task.TaskName -Confirm:$false
				} catch {
			        Write-Warning "Could not stop task $($task.TaskName): $($_.Exception.Message)"
		        }
	        }
		} catch {
	        Write-Error "Update installation failed: $($_.Exception.Message)"
		}
	} #end scriptBlock

	# Execute the update job
	try {
        $job = Invoke-WuJob -ComputerName $env:COMPUTERNAME -Script $scriptBlock -RunNow -Confirm:$false -ErrorAction Stop
		Write-Host "<WRITE-LOG = ""*Update job started successfully*"">"
	} catch {
		Write-Host "<WRITE-LOG = ""*ERROR: Failed to start update job: $($_.Exception.Message)*"">"
		Write-Error "ERROR: Failed to start update job: $($_.Exception.Message)"
	}

	# Monitor update progress
	Write-Host "<WRITE-LOG = ""*Monitoring update progress...*"">"
	$timeout = 3600 # 60 minute timeout
	$timer = 0
    $checkInterval = 30 # seconds

	while ($timer -lt $timeout) {
		$runningTasks = Get-ScheduledTask | Where-Object {
		$_.TaskName -like '*PSWindowsUpdate*' -and $_.State -eq 'Running'
		}

		if ($runningTasks.Count -eq 0) {
			Write-Host "<WRITE-LOG = ""*Windows Updates installation completed!*"">"

			# Check if reboot is required
		    $rebootRequired = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"

	        if ($rebootRequired) {
                Write-Host "<WRITE-LOG = ""*System restart required*"">"
			} else {
				Write-Host "<WRITE-LOG = ""*No restart required*"">"
			}
		break
	    }

		# Progress update every 60 seconds
		if ($timer % 60 -eq 0) {
	        Write-Host "<WRITE-LOG = ""*Updates in progress... ($([math]::Round($timer/60)) minutes elapsed)*"">"
        }

		Start-Sleep -Seconds $checkInterval
	    $timer += $checkInterval
    }
	if ($timer -ge $timeout) {
        Write-Host "<WRITE-LOG = ""*WARNING: Update process timed out after $timeout seconds*"">"
	}

} else {
	Write-Host "<WRITE-LOG = ""*System is up to date.*"">"
}

# ===== Step 7 of 7: 07-SE-ManagedReboot.ps1 =====

# ServerEngine can handle system reboot cycles
# and seemlesy continue the next scripts when using Runbooks.
# ServerEngine initiates a waiting sequence and after
# successfull reconnection the cycle ends
#-------------------------------------------------

# Use this specific line for managed reboot cycles
Invoke-Command { Start-Sleep 3;Write-Host " ""State : Reboot"" ";Restart-Computer -Force }; Exit-PSSession

Run it across your fleet

This script runs as-is on a single host. Paste it into ServerEngine to schedule it, run it on a whole server group in parallel, and keep the credentials out of the file — see the scripts documentation and the credential store.

Ready when you are.

Try ServerEngine free for 7 days.