Script to update Windows to version 20H2

Because of the way Microsoft has categorized this update, we cannot push it via an Update policy in Syncro.

This also applies to all other Feature Updates. The in-app Install button will not work.

Some have used the below script to push the update. Use at your own risk, but thus far it has worked fine for others. You will want to test it in your environment to make sure you are happy with it.

$dir = 'C:\_Windows_FU\packages' 
mkdir $dir $webClient = New-Object System.Net.WebClient
$url = '' $file = "$($dir)\Win10Upgrade.exe" $webClient.DownloadFile($url,$file)
Start-Process -FilePath $file -ArgumentList '/quietinstall /skipeula /auto upgrade /copylogs $dir'

This is a PowerShell script that installs the update; it does not check to see if it is already installed.

Warning: This will likely cause the machine to reboot after Win10Upgrade.exe completes the upgrade.

In the last line with the Warning, I think you meant to say reboot instead of update. And yes assuming the update starts successfully there will be an automatic reboot after 30 mins.


Good catch! It’s been fixed.

@Frank First you said

you can manually run it via an agent from the Windows Patches tab

Then you said

The in-app Install button will not work.

I am confused which one it is.

:grimacing: It has been updated. It should make more sense now Thanks for letting me know.

Is there something I need to change? WHen I try to run, I get the following.

error> At C:\ProgramData\Syncro\bin\945ab2e4-8d4b-409e-b41a-0e293020f4dd.ps1:7 char:57
error> + $url = '' $file = "$($d ...
error> +                                                         ~~~~~
error> Unexpected token '$file' in expression or statement.
error> At C:\ProgramData\Syncro\bin\945ab2e4-8d4b-409e-b41a-0e293020f4dd.ps1:7 char:92
error> + ... ?LinkID=799445' $file = "$($dir)\Win10Upgrade.exe" $webClient.Downloa ...
error> +                                                        ~~~~~~~~~~
error> Unexpected token '$webClient' in expression or statement.
error>     + CategoryInfo          : ParserError: (:) [], ParseException
error>     + FullyQualifiedErrorId : UnexpectedToken

$file… needs to be on the next line.
$webclient… also needs to be on it’s own line.

Bit more long winded my version of this, feel free to use.

Import-Module $env:SyncroModule

# Set Variables:
$targetVersion = "10.0.19044"   #Windows 10 21h2
$minimumSpace = 10          # Minimum drive space in GB
$downloadPath = "C:\temp"     # Folder for the download
$logPath = "C:\temp\logs"     # Folder for the Windows setup logs
$updateAssistantURL = "" # URL of the update assistant
$upgradeArguments = "/quietinstall /skipeula /auto upgrade /copylogs " + $logPath
$fileName = "Windows10Upgrade.exe"

Write-Host $fileName
  Prevent updates during working hours
  - if $workingHoursEnabled is set to true, if the script is run between the specified hours it will terminate
  - prevents accidental in-hours upgrades
$workingHoursEnabled = $false 
$workingHoursStart = 0900
$workingHoursEnd = 1730

  Show pop up to logged in user
  - if true, this will display a pop up to any logged in user
  - if timeout is set to 0, script will *need* user to click OK before script continues
  - so if you want this to be unattended, either set a time for the pop up greater than 0 seconds, or set $popupShow to $false

$popupShow = $true
$popupMessage = "A  Windows Feature Update is scheduled for your device. This will take 2-4 hours depending on the speed of your machine and your Internet. Please save all documents and close your work, leaving your PC turned on and connected to power"
$popupTitle = "Message from Alamo IT Support"
$popupTimeout = 20

function Get-CurrentVersion {
  $OS = (Get-CimInstance Win32_OperatingSystem).caption
  $currentVersion = (Get-CimInstance Win32_OperatingSystem).version
  if (-Not ($OS -like "Microsoft Windows 10*"))
      Write-Output "This machine does not have Windows 10 installed. Exiting."
  if ($currentVersion -eq $targetVersion)
      Write-Output "OS is up-to-date - no action required. Exiting"
    else {
      Write-Output "OS is out of date, continuing"

function Test-DiskSpace ($minimumSpace){
  $drive = Get-PSDrive C
  $freeSpace = $ / 1GB
  if ($minimumSpace -gt $freeSpace)
          Write-Output "Not enough space for the upgrade to continue. Exiting script."
      } else {
        Write-Output "There is enough free disk space. Continuing."

function Test-WorkingHours($enabled, $startTime, $endTime){
  if ($enabled -eq $true)
        [int]$time = Get-Date -format HHMM
        if ($time -gt $startTime -And $time -lt $endTime)
          Write-Output "Script has been executed within working hours. Exiting script."
        } else {
          Write-Output "Confirmed script has been run outside working hours. Continuing."
      } else {
        Write-Output "Working hours flag disabled. Continuing."

function Get-UpdateAssistant($URL, $path, $log, $file){
# Download File
  If(!(test-path $path))
      New-Item -ItemType Directory -Force -Path $path
      Write-Output "Created download path."
  If(!(test-path $log))
    New-Item -ItemType Directory -Force -Path $log
    Write-Output "Created log path."
  Invoke-WebRequest -Uri $URL -OutFile $path\$file
  Write-Output "Downloaded Update Assistant"

function Show-Message($enabled, $title, $message, $time) {
  if ($enabled -eq $true) {
    $popUp = New-Object -ComObject Wscript.Shell
    Write-Output "Message displayed. Continuing"
    } else {
      Write-Output "Pop up has been disabled. Continuing."

function Start-Upgrade($path, $file, $arguments) {
  Write-Output "Starting upgrade process..."
  Start-Process -FilePath $path\$file -ArgumentList $arguments
  Write-Output "Upgrade process has been started"
  $startAt = (Get-Date)
  Set-Asset-Field -Subdomain "YOUR-DOMAIN" -Name "Script Upgrade Started" -Value $startAt
  Start-Sleep -s 120 # Pause a little, to make sure the process is started

Test-DiskSpace $minimumSpace
Test-WorkingHours $workingHoursEnabled $workingHoursStart $workingHoursEnd
Get-UpdateAssistant $updateAssistantURL $downloadPath $logPath $fileName
Show-Message $popupShow $popupTitle $popupMessage $popupTimeout
Start-Upgrade $downloadPath $fileName $upgradeArguments

That is a beautiful script! I’ll definitely be adding/using this script from here on out.

1 Like

You do not need Subdomain when setting an asset field, this was depreciated over a year ago and can cause script failure. Script looks great otherwise!

Ah didn’t know that will remove. Glad the script is good for you. If you fancy a script exchange let me know.

I have been running this script and most of the machines have not been updated. I have not received an execution error on the script and I can tell the Win10Upgrade.exe is running, but nothing is happening after hours. I ran the script on my VM and it worked fine. I can also go into the Windows Update GUI and update there manually if I choose, but that’s not very practical. The log folder is blank, so I have no feedback as to what is happening. This is all one customer, so I am not sure if it is environmental.

Can anyone point me to the switches for the upgrade.exe? I am considering changing it to visible, so I can see what is happening.

It just uses the update assistant, so you can download and run it on the machine to see if it errors out. I have seen it take 4+ hours on some machines plus it has the ~30 minute reboot timer.

Can’t get this to work on my side. Getting the following returned on the three different machines I’m running it on (two with 1909 and one with 20H1):

OS is out of date, continuing
There is enough free disk space. Continuing.
Working hours flag disabled. Continuing.
Downloaded Update Assistant
Message displayed. Continuing
Starting upgrade process…
Upgrade process has been started
Call-SyncroApi: success

Getting this error while running main script at the top:

error> mkdir : A positional parameter cannot be found that accepts argument ‘$null’.
error> At C:\ProgramData\Syncro\bin\cafbf76c-0d93-4d16-954c-c5c07292da44.ps1:4 char:1
error> + mkdir $dir $webClient = New-Object System.Net.WebClient
error> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error> + CategoryInfo : InvalidArgument: (:slight_smile: [mkdir], ParameterBindingException
error> + FullyQualifiedErrorId : PositionalParameterNotFound,mkdir
error> You cannot call a method on a null-valued expression.
error> At C:\ProgramData\Syncro\bin\cafbf76c-0d93-4d16-954c-c5c07292da44.ps1:7 char:1
error> + $webClient.DownloadFile($url,$file)
error> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error> + CategoryInfo : InvalidOperation: (:slight_smile: , RuntimeException
error> + FullyQualifiedErrorId : InvokeMethodOnNull
error> Start-Process : This command cannot be run due to the error: The system cannot find the file specified.
error> At C:\ProgramData\Syncro\bin\caf…

Great script, used it last night unattended and it worked. I know the $targetVersion variable is specific to 21H2 to make sure it doesn’t install on top of the same or a newer version. But is there any control as to what version of the feature pack is downloaded and installed in this script? For example, let’s say 6 months from now I want to use this script to deploy 21H2 but another feature update has been released sometime in the last 6 months that is newer than 21H2. If I use this script will it always just install the latest feature update that has been released (something newer than 21H2) or is there something in this script that specifically pulls down 21H2, regardless if a newer feature update has been released?

Update Assistant always installs the newest version, you can’t choose which version to install. Sometimes it take a few days/weeks after a new build comes out for it to use it, but otherwise. To install a specific build you’d have to use the ISO way of installing/upgrading. If you want to target machines that need upgrading by age instead of version number you can use my script Monitor - Windows Update - to generate alerts and then auto remediate that with a script that just does the upgrade part. Or combine them if you’d rather.

1 Like

I just found your script and I am testing it out. One adjustment I would suggest is the popup message. If you set this to run as SYSTEM then it cannot send a popup; If you run it as the logged in user it will not run if no one is logged in.

I changed it from a wscript popup to using the MSG.exe windows command

function Show-Message($enabled, $title, $message, $time) {
  if ($enabled -eq $true) {
    # $popUp = New-Object -ComObject Wscript.Shell
    # $popUp.Popup($message,$time,$title,0x0)
    & MSG * /v /time:$time $message
    # Write-Output "Message displayed. Continuing"
    } else {
      Write-Output "Pop up has been disabled. Continuing."
1 Like

will syncro release a formal script for this update or include this option in the patch system?

They’re not able to integrate this into the patch system because these kinds of patches are purposely missing switches to do silent installs. Thank MS for that. There are plenty of scripts floating around to do this, and there’s one in the Community Library, so probably not a need for them to write another one.