Although this function requires ‘interactive’ or console sessions, it can be trigged by Windows Scheduled Tasks. Hence, it is included inside a more comprehensive program here.

# updateLocalWindowsUsingComObjects.ps1
# Version: 0.0.2
# This iteration includes the feature to output a log of update progress

function updateLocalWindowsUsingComObjects($autoReboot=$false,$logfile='c:\WindowsUpdate.log'){
    # in case contents of function is called from scheduled tasks without any default argument values
    $logFile=if($logFile){$logFile}else{'c:\WindowsUpdate.log'}
    if(!(test-path $logfile)){
        new-item $logfile -type file -force
    }
    $status="$(get-date) => $env:computername patching STARTED"
    write-host $status
    Add-Content -Path $logfile -Value $status
    $updateSession=New-Object -Com Microsoft.Update.Session
    $updateSearcher=$updateSession.CreateUpdateSearcher()
    $searchCriteria="IsInstalled=0 AND Type='Software'" # "IsHidden=0 AND IsInstalled=0 AND Type='Software'"
    $availableUpdates=$updateSearcher.Search($searchCriteria)
    $availableUpdatesCount=$availableUpdates.Updates.count
    if($availableUpdatesCount -eq 0){
        $status="$(get-date) => $env:computername has 0 available updates. Patching FINISHED"
        write-host $status
        Add-Content -Path $logfile -Value $status
        return $true
    }else{
        $status="$(get-date) => $availableUpdatesCount available updates detected"
        write-host $status
        Add-Content -Path $logfile -Value $status
        $updatesToDownload=New-Object -Com Microsoft.Update.UpdateColl
        For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
            $item = $availableUpdates.Updates.Item($i)
            $Null = $updatesToDownload.Add($item)
        }                        
        $updateSession=New-Object -Com Microsoft.Update.Session
        $downloader=$updateSession.CreateUpdateDownloader()
        $downloader.Updates=$updatesToDownload
        try{
            $null=$downloader.Download()
        }catch{
            write-warning $_
        }
        $downloadedUpdates=New-Object -Com Microsoft.Update.UpdateColl
        For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
            $item = $availableUpdates.Updates.Item($i)
            If ($item.IsDownloaded) {
                $null=$downloadedUpdates.Add($item)        
            }
        }
        $downloadedCount=$downloadedUpdates.count                        
        if($downloadedCount -eq 0){
            $status="$(get-date) => Installation cannot proceed 0 successful downloads."
            write-host $status
            Add-Content -Path $logfile -Value $status
            return $false
        }else{
            $status="$(get-date) => $downloadedCount of $availableUpdatesCount updates downloaded successfully"
            write-host $status
            Add-Content -Path $logfile -Value $status
            $installCount=0
            foreach($update in $downloadedUpdates){
                $thisUpdate = New-object -com "Microsoft.Update.UpdateColl"
                $thisCount=$installCount++ +1
                $null=$thisUpdate.Add($update)
                $installer = $updateSession.CreateUpdateInstaller()
                $installer.Updates = $thisUpdate
                $installResult = $installer.Install()
                $translatedCode=switch ($installResult.ResultCode){
                    0  {'not started'}
                    1  {'in progress'}
                    2  {'succeeded'}
                    3  {'succeeded with errors'}
                    4  {'failed'}
                    5  {'aborted'}
                }
                $status="$(get-date) => $thisCount of $downloadedCount $translatedCode`: $($update.Title)"
                write-host $status
                Add-Content -Path $logfile -Value $status                                
            }
        }
    }
    $pendingReboot=.{
        if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
        if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
        if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
        try { 
            $util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
            $status = $util.DetermineIfRebootPending()
            if (($null -ne $status) -and $status.RebootPending) {
                return $true
            }
        }catch{}                            
        return $false
    }
    If($pendingReboot){
        if($autoReboot){
            $status="$(get-date) => $env:computername REBOOTED"
            write-host $status
            Add-Content -Path $logfile -Value $status
            (Get-WMIObject -Class Win32_OperatingSystem).Reboot()
        }else{
            $status="$(get-date) => $env:computername required a REBOOT"
            write-host $status
            Add-Content -Path $logfile -Value $status
        }
    }else{
        $status="$(get-date) => $env:computername patching FINISHED"
        write-host $status
        Add-Content -Path $logfile -Value $status
    }
}

updateLocalWindowsUsingComObjects

Note: this function only works when login interactively – it does not work via WinRM or this error would occur:

# $computernames='testWindows'
# $autoreboot=$true
# function updateLocalWindowsUsingComObjects{...}
# $computerNames|%{invoke-command -computername $_ -scriptblock{
#     param($updateLocalWindows,$autoreboot)
#     write-host "Updating $env:computername..."
#     [scriptblock]::create($updateLocalWindows).invoke($autoreboot)
#     } -args ${function:updateLocalWindows},$autoreboot
# }

# This occurs because of 2nd-hop issues - COM objects require interactive logons
#
# Exception calling "Invoke" with "1" argument(s): "Access is denied. (Exception from HRESULT: 0x80070005
# (E_ACCESSDENIED))"
#     + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
#     + FullyQualifiedErrorId : RuntimeException
#     + PSComputerName        : testWindows
#
# This is an unsecured workaround:
# $username='domain\admin'
# $plaintextPassword='PASSWORD'
# $encryptedPassword = ConvertTo-SecureString $plaintextPassword -AsPlainText -Force
# $credentials = New-Object System.Management.Automation.PSCredential $username,$encryptedPassword
# Start-Process -Credential $credentials powershell -ArgumentList "-Command & {...}"