I wanted a simple overview of the Feature Update level for one of our customers’ Windows clients; Intune reports didn’t give me what I needed… In this blog post, I will show you the script I wrote for it 🙂
What are Feature Updates and why are they important?
“Windows feature updates are periodic releases that introduce new Windows capabilities, improvements, and changes to existing functionality. These updates typically ship once per year and represent a full operating system upgrade, such as moving a device from one Windows version to a newer release. Because feature updates can affect user experience, application compatibility, and device readiness, organizations often deploy them using controlled and phased rollouts.”
Source: https://learn.microsoft.com/en-us/intune/device-updates/windows/manage-feature-updates
Intune reports
You can check the Feature Updates level of Windows devices in Intune in the Windows Autopatch Pane here, but this depends on your policies and whether the devices are at that level.
The PowerShell way
I wrote a script that connects to the tenant, retrieves all Managed Windows devices, checks their builds, and outputs totals per Feature Update. And yes, I could have written it in a lot fewer lines, but as always… Error handling is important, and I also wanted to verify the correct Scopes and output the connecting user and tenant (convenient when reusing an existing Microsoft Graph connection that could be connected to the wrong tenant, etc.).
Running the script
When you run the script, it will check if the required Microsoft Graph Modules are installed (It will also install PSResourceGet when not installed, which might be the case when running PowerShell < 7.4 or 5.1), connect to a tenant if not connected already with enough permissions, and will output a simple Group-Object Output:

In the output above, I redacted the Admin Account and Tenant Name (When you run this, it will be shown, of course 🙂 ), and it shows the number of Windows 11 devices running 26H2 and 25H2 for this tenant. It does support all Windows 10 and Windows 11 Feature Update levels. When 27H1/27H2 comes out, you’ll need to update the script to support those build numbers.
It also mentions that the Output was copied to your clipboard and that it can be pasted into Excel. (The output is in CSV format)

Wrapping up
And that’s how you can create an overview of all the current Feature Update versions in your environment in a small overview in Excel with more information. Have a lovely weekend!
The script
Below are the contents of the script; copy/paste it into a .ps1 file.PS1 file and run it in VSCode, Terminal, or ISE.
# Check if PSResourceGet is available; install it if not, and check that the required modules are available, and install them if not
if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet)) {
try {
Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force -ErrorAction Stop
}
catch {
Write-Warning "Error installing PSResourceGet, exiting..."
Write-Warning ("Error message:`n{0}" -f $_)
return
}
}
# Import Module PSResourceGet
try {
Import-Module Microsoft.PowerShell.PSResourceGet -ErrorAction Stop
}
catch {
Write-Warning "Error importing PSResourceGet, exiting..."
Write-Warning ("Error message:`n{0}" -f $_)
return
}
# Check if the required Graph Modules are available, and install them if not
foreach ($Module in "Microsoft.Graph.Authentication", "Microsoft.Graph.DeviceManagement") {
if (-not (Get-InstalledPSResource -Name $Module -ErrorAction SilentlyContinue)) {
try {
Install-PSResource -Name $Module -Scope CurrentUser -ErrorAction Stop
Import-Module -Name $Module -ErrorAction Stop
}
catch {
Write-Warning ("Error installing and importing required Module {0}, exiting..." -f $Module)
Write-Warning ("Error message: `n{0}" -f $_)
return
}
}
else {
try {
Import-Module -Name $Module -ErrorAction Stop
}
catch {
Write-Warning ("Error importing required Module {0}, exiting..." -f $Module)
Write-Warning ("Error message: `n{0}" -f $_)
return
}
}
}
# Connect to Graph with permissions to read devices and the tenant display name
# Check if already connected with the correct scopes, (re)connect if not
$Context = Get-MgContext
$DeviceScopes = @(
'DeviceManagementManagedDevices.Read.All'
'DeviceManagementManagedDevices.ReadWrite.All'
)
$OrganizationScope = 'Organization.Read.All'
$HasDeviceScope = @($Context.Scopes | Where-Object { $_ -in $DeviceScopes }).Count -gt 0
$HasOrganizationScope = $OrganizationScope -in $Context.Scopes
if (-not $Context -or -not $HasDeviceScope -or -not $HasOrganizationScope) {
try {
Connect-MgGraph -Scopes 'DeviceManagementManagedDevices.Read.All', $OrganizationScope -NoWelcome -ErrorAction Stop
$Context = Get-MgContext
}
catch {
Write-Warning "Error connecting to Microsoft Graph, exiting..."
Write-Warning ("Error message:`n{0}" -f $_)
return
}
}
# Retrieve Tenant information to show connecting user information
try {
$Organization = Invoke-MgGraphRequest -Method GET -Uri ('/v1.0/organization/{0}?$select=displayName' -f $Context.TenantId) -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($Organization.displayName)) {
throw "Microsoft Graph returned an empty Tenant display name."
}
Write-Output ("`n`nConnected to Microsoft Graph with account {0} from Tenant {1}, continuing..." -f $Context.Account, $Organization.displayName)
}
catch {
Write-Warning "Error retrieving Tenant name, exiting..."
Write-Warning ("Error message:`n{0}" -f $_)
return
}
# Retrieve all Managed Windows Devices
try {
$Devices = Get-MgDeviceManagementManagedDevice -Filter "operatingSystem eq 'Windows' and managedDeviceOwnerType eq 'company'" -All -ErrorAction Stop
}
catch {
Write-Warning ("Error retrieving devices, exiting...")
Write-Warning ("Error message: `n{0}" -f $_)
return
}
# Check Feature version based on Buildnumber and add to $Total
$Total = foreach ($Device in $Devices) {
switch -Wildcard ($Device.OSVersion) {
'10.0.10240.*' { $Version = 'Windows 10 1507' }
'10.0.10586.*' { $Version = 'Windows 10 1511' }
'10.0.14393.*' { $Version = 'Windows 10 1607' }
'10.0.15063.*' { $Version = 'Windows 10 1703' }
'10.0.16299.*' { $Version = 'Windows 10 1709' }
'10.0.17134.*' { $Version = 'Windows 10 1803' }
'10.0.17763.*' { $Version = 'Windows 10 1809' }
'10.0.18362.*' { $Version = 'Windows 10 1903' }
'10.0.18363.*' { $Version = 'Windows 10 1909' }
'10.0.19041.*' { $Version = 'Windows 10 2004' }
'10.0.19042.*' { $Version = 'Windows 10 20H2' }
'10.0.19043.*' { $Version = 'Windows 10 21H1' }
'10.0.19044.*' { $Version = 'Windows 10 21H2' }
'10.0.19045.*' { $Version = 'Windows 10 22H2' }
'10.0.22000.*' { $Version = 'Windows 11 21H2' }
'10.0.22621.*' { $Version = 'Windows 11 22H2' }
'10.0.22631.*' { $Version = 'Windows 11 23H2' }
'10.0.26100.*' { $Version = 'Windows 11 24H2' }
'10.0.26200.*' { $Version = 'Windows 11 25H2' }
'10.0.28000.*' { $Version = 'Windows 11 26H1' }
'10.0.26300.*' { $Version = 'Windows 11 26H2' }
default { $Version = 'Unknown' }
}
[PSCustomObject]@{
Name = $Device.DeviceName
Manufacturer = $Device.Manufacturer
Model = $Device.Model
User = $Device.UserPrincipalName
OperatingSystemVersion = $Device.OSVersion
Version = $Version
LastSyncDateTime = $Device.LastSyncDateTime
}
}
# Output statistics if there are results saved to $Total
if ($null -ne $Total) {
Write-Output ("Statistics for the Windows Managed Intune devices are:")
$total | Group-Object Version | Select-Object Count, Name | Sort-Object Count
# Copy data to clipboard in CSV format
try {
$Total | ConvertTo-Csv -NoTypeInformation -Delimiter ';' | Set-Clipboard -ErrorAction Stop
Write-Output ("`nOutput saved to clipboard; paste in Excel for further analysis")
}
catch {
Write-Warning ("Error copying results to clipboard, exiting...")
Write-Warning ("Error message: `n{0}" -f $_)
return
}
}
else {
Write-Warning ("No Company-Owned Windows Devices were found")
}
Download the script(s) from GitHub here