Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Deploy the Most-Needed Updates in MECM with PowerShell

Find the updates the most machines are actually missing, bundle them into a group, and deploy them to a pilot collection, with a WhatIf preview first.

AT A GLANCENew-CMTopUpdateDeployment.ps1
What it does
Picks the top N required updates by number of clients missing them, puts them in a new software update group, downloads the content into an existing deployment package, and deploys the group to a collection.
Requires
  • Windows PowerShell 5.1
  • Configuration Manager console installed, for the ConfigurationManager module
  • A software update point that's syncing, and clients that have finished a compliance scan
  • An existing software update deployment package
Permissions
A ConfigMgr role that can manage software updates and deploy to the target collection, such as Software Update Manager.
Runs on
Any machine with the Configuration Manager console
Tested
Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4

Every so often you land on a site where patching has drifted. Clients are months behind, there are forty update groups with names like "Aug fix 2 FINAL", and nobody's sure what's actually missing. You don't need a grand plan on day one. You need the handful of updates that the most machines are missing, deployed to a pilot group, today.

That's what this script does. ConfigMgr already knows, for every update, how many clients report it as required. The script asks for the ones that aren't superseded or expired, filters by title (cumulative and security updates by default), and takes the top ten. Run it with -WhatIf and you get the list without anything being created. Run it for real and you get a group, downloaded content and a deployment.

The first version of this had a site code, server name and share path hard-coded, created a brand new deployment package every run, and set restart behavior that could surprise people. This one takes everything as parameters, reuses a package you already have, and stays out of the restart business so your maintenance windows decide.

New-CMTopUpdateDeployment.ps1Download
<#
.SYNOPSIS
    Builds a software update group from the most-needed updates and deploys it to a collection.
.DESCRIPTION
    Finds updates that are required on at least one client, aren't superseded or expired, and match
    your title filters. Takes the top N by number of clients missing them, puts them in a new software
    update group, downloads them into a deployment package, and creates a deployment.
    Supports -WhatIf, which shows the update list without creating anything.
.PARAMETER SiteCode
    Your three-character site code, for example ABC.
.PARAMETER CollectionName
    The device collection to deploy to. Start with a pilot collection.
.PARAMETER DeploymentPackageName
    An existing software update deployment package to download the content into.
.PARAMETER TitleLike
    Wildcard patterns an update title must match (any one of them). Default: cumulative and security updates.
.PARAMETER Top
    How many updates to include, most-missing first. Default: 10.
.PARAMETER DeadlineDays
    Days from now until the deadline. Default: 7.
.PARAMETER Available
    Deploy as Available instead of Required.
.EXAMPLE
    .\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'Pilot - Workstations' -DeploymentPackageName 'Windows Updates 2026' -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)][ValidatePattern('^[A-Za-z0-9]{3}$')][string]$SiteCode,
    [string]$ProviderMachineName = $env:COMPUTERNAME,
    [Parameter(Mandatory)][string]$CollectionName,
    [Parameter(Mandatory)][string]$DeploymentPackageName,
    [string[]]$TitleLike = @('*Cumulative Update*', '*Security Update*'),
    [ValidateRange(1, 1000)][int]$Top = 10,
    [ValidateRange(0, 90)][int]$DeadlineDays = 7,
    [string]$GroupName = ('{0:yyyy-MM-dd} Top Missing Updates' -f (Get-Date)),
    [switch]$Available
)

$ErrorActionPreference = 'Stop'

if (-not (Get-Module ConfigurationManager)) {
    if (-not $env:SMS_ADMIN_UI_PATH) { throw 'The Configuration Manager console is not installed on this machine.' }
    Import-Module (Join-Path $env:SMS_ADMIN_UI_PATH '..\ConfigurationManager.psd1')
}
if (-not (Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue)) {
    New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName | Out-Null
}

Push-Location "$($SiteCode):\"
try {
    # Fail early on typos, before we build anything.
    if (-not (Get-CMDeviceCollection -Name $CollectionName)) { throw "Collection '$CollectionName' not found." }
    if (-not (Get-CMSoftwareUpdateDeploymentPackage -Name $DeploymentPackageName)) { throw "Deployment package '$DeploymentPackageName' not found." }

    Write-Verbose 'Querying software updates. On a big site this takes a minute.'
    $candidates = Get-CMSoftwareUpdate -Fast -IsSuperseded $false -IsExpired $false | Where-Object {
        $update = $_
        $update.NumMissing -gt 0 -and ($TitleLike | Where-Object { $update.LocalizedDisplayName -like $_ })
    }
    $selected = @($candidates | Sort-Object NumMissing -Descending | Select-Object -First $Top)

    if ($selected.Count -eq 0) {
        Write-Warning 'No required updates matched your filters. Nothing to do.'
        return
    }

    # Show the list either way, so -WhatIf is actually useful.
    $report = foreach ($u in $selected) {
        [pscustomobject]@{
            ArticleID  = "KB$($u.ArticleID)"
            Title      = $u.LocalizedDisplayName
            NumMissing = $u.NumMissing
            CI_ID      = $u.CI_ID
            InGroup    = $false
        }
    }

    if (-not $PSCmdlet.ShouldProcess("$($selected.Count) updates -> '$CollectionName'", "Create group '$GroupName' and deploy")) {
        return $report
    }

    if (Get-CMSoftwareUpdateGroup -Name $GroupName) { throw "Update group '$GroupName' already exists. Use -GroupName to pick another." }
    New-CMSoftwareUpdateGroup -Name $GroupName -Description "Top $Top missing updates, created by script" | Out-Null

    foreach ($row in $report) {
        try {
            Add-CMSoftwareUpdateToGroup -SoftwareUpdateGroupName $GroupName -SoftwareUpdateId $row.CI_ID
            $row.InGroup = $true
            Write-Verbose "Added $($row.ArticleID)"
        }
        catch {
            Write-Warning "Couldn't add $($row.ArticleID): $($_.Exception.Message)"
        }
    }

    if (-not ($report | Where-Object InGroup)) { throw "No updates could be added to '$GroupName'. Nothing was deployed." }

    Write-Verbose "Downloading content into '$DeploymentPackageName'"
    Save-CMSoftwareUpdate -SoftwareUpdateGroupName $GroupName -DeploymentPackageName $DeploymentPackageName

    $now = Get-Date
    $deployArgs = @{
        SoftwareUpdateGroupName = $GroupName
        CollectionName          = $CollectionName
        DeploymentName          = "$GroupName - $CollectionName"
        DeploymentType          = if ($Available) { 'Available' } else { 'Required' }
        AvailableDateTime       = $now
        TimeBasedOn             = 'LocalTime'
        UserNotification        = 'DisplaySoftwareCenterOnly'
    }
    if (-not $Available) { $deployArgs.DeadlineDateTime = $now.AddDays($DeadlineDays) }
    New-CMSoftwareUpdateDeployment @deployArgs | Out-Null

    $report
}
finally {
    Pop-Location
}

Parameters

ParameterTypeDefaultWhat it's for
-SiteCodestring—Your three-character site code, like ABC.
-ProviderMachineNamestring$env:COMPUTERNAMEThe SMS Provider server.
-CollectionNamestring—Device collection to deploy to. Please start with a pilot.
-DeploymentPackageNamestring—An existing software update deployment package. The content gets downloaded into it.
-TitleLikestring[]'*Cumulative Update*', '*Security Update*'Wildcard patterns an update title has to match. Any one match is enough.
-Topint10How many updates to include, most-missing first.
-DeadlineDaysint7Days until the installation deadline. Ignored with -Available.
-GroupNamestring<today> Top Missing UpdatesName for the new software update group.
-Availableswitch—Deploy as Available in Software Center instead of Required.

Run it

Preview the list. Nothing gets created.

.\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'Pilot - Workstations' -DeploymentPackageName 'Windows Updates 2026' -WhatIf

Deploy the top ten to the pilot group with a five-day deadline.

.\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'Pilot - Workstations' -DeploymentPackageName 'Windows Updates 2026' -DeadlineDays 5

Only Windows 11 cumulative updates, offered in Software Center.

.\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'IT Staff' -DeploymentPackageName 'Windows Updates 2026' -TitleLike '*Cumulative Update for Windows 11*' -Top 3 -Available

Keep a record of exactly what went out.

.\New-CMTopUpdateDeployment.ps1 -SiteCode ABC -CollectionName 'Pilot - Workstations' -DeploymentPackageName 'Windows Updates 2026' | Export-Csv .\deployed.csv -NoTypeInformation

What you'll see

Example outputvalues are illustrative
What if: Performing the operation "Create group '2026-09-29 Top Missing Updates' and deploy" on target "3 updates -> 'Pilot - Workstations'".

ArticleID Title                                                                    NumMissing    CI_ID InGroup
--------- -----                                                                    ----------    ----- -------
KB5065426 2026-09 Cumulative Update for Windows 11, version 24H2 for x64-based ...        212 16801234   False
KB5064401 2026-09 Cumulative Update for .NET Framework 3.5 and 4.8.1 for Windo...         188 16801177   False
KB5065431 2026-09 Cumulative Update for Windows 10 Version 22H2 for x64-based ...          41 16801202   False

How it works

  1. Check the inputs. The collection and deployment package are looked up first, so a typo fails in two seconds instead of after a long query.
  2. Ask ConfigMgr what's missing. Get-CMSoftwareUpdate -Fast -IsSuperseded $false -IsExpired $false returns current updates. The script keeps the ones where NumMissing is above zero and the title matches one of your patterns, sorts by NumMissing, and takes the top N.
  3. Show the list, then ask. The report objects are built before anything changes, so -WhatIf gives you something useful: the exact updates, their KB numbers and how many machines need each one.
  4. Build the group. It creates a new software update group and adds each update in its own try. One stubborn update gets a warning; the others carry on.
  5. Download and deploy. Save-CMSoftwareUpdate pulls the content into your existing package, which then refreshes on its distribution points, and New-CMSoftwareUpdateDeployment deploys the group with an available time of now and your deadline.

Take it further

  • Run it on a schedule against a pilot. Weekly, with -Top 5, against a small collection of IT machines, it's a decent early-warning system.
  • Check how it went. The companion script, Get-CMUpdateDeploymentStatus, shows compliance per deployment so you can see what the pilot thinks before going wider.
  • Widen in rings. Once the pilot looks good, run New-CMSoftwareUpdateDeployment against the next collection with the same group name. No need to build a second group.

Things that'll trip you up

  • NumMissing is only as fresh as your scans. The counts come from client compliance scans and summarization. A brand new site, or clients that haven't scanned since the last sync, will show zeros. Run the Software Updates summarization and give clients a scan cycle before trusting the list.
  • Automatic deployment rules usually win long-term. For regular Patch Tuesday work, an ADR is the right tool. This script is for catching up, for a one-off push, or for when you want a person to look at the list before anything goes out.
  • Updates with license terms. A few updates need their EULA accepted before they can be added to a group. Those get a warning and InGroup stays False; the rest still go out.
  • Maintenance windows still apply. The deployment doesn't override them, so a Required deployment installs inside the collection's maintenance window after the deadline. If a pilot machine has no window at all, it installs and may restart right at the deadline.