I have been banging my head why some machines wouldn’t deploy Windows 11 Windows Feature Updates (24H2) as part of Intune AutoPatch / WUfB.
I have seen many reasons, both related to Windows 11 24H2 requirements like TPM which wasn’t enabled, diskspace issues, client issues – but lastly also backend issues, where WUfB enrollment state was “stuck” in Offering/Enrolling-state.
June 2025 Workaround (FINALLY!) – Update stack remains stuck in a local scan state and doesn’t ask for deployments in WUFB related to Feature Updates
Issue: Microsoft support has informed me, that in sporadic occurrences, there is a bug, where the Update stack remains stuck in a local scan state and doesn’t ask for deployments in WUFB related to Feature Updates.
Problem: Some devices that are running Windows 10 or Windows 11, version 19H1 and above,Β aren’t able to install monthly security updates because of file or metadataΒ corruption within the servicing stack. These devices may not be able to become up to date without a Feature Update.Β
Solution/Workaround: Microsoft is working on an fix, which will be included in Autopatch at some point this year, but until then we can fix it manually by added the below mentioned command on the client.
(Win11 23H2/24H2 only)
reg.exe Add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion /v AllowInplaceUpgrade /t REG_DWORD /f /d 4
(Win10 22H2/Win11 22H2 only)
Reg.exe Add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion /v UpgradeEligible /t REG_DWORD /f /d 1
Note:
* This registry value will be removed once the in-place upgrade is complete.
* It can take up to 48 hours for the in-place upgrade to be offered to the device.
* After the in-place upgrade, the device will be able to take new updates normally
* Don't confuse with the same regkey AllowInplaceUpgrade which normally resides in HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate. For this parameter to work with value = 4, it must be set in HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion
In a rather cryptic way, this is also covered in this Microsoft article, even though the above reg-keys and values are not mentioned.
This blog contains the scripts I used to solve this – both related to backend-issues like device is stuck in ‘Offering’ state – and also endpoint-related issues.
Microsoft has also released a troubleshooting guide, which you can find here
I’m using the Microsoft Graph API endpoint mentioned in the Microsoft guide (“https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets”). You can see link in each section below to the Graph documentation. Click here to access the documentation.
As always, be patience when you make backend changes (I have seen from a few hours to up to 72 hours).
I have done a support-case with Microsoft support, where they have concluded that in sporadic occurrences, there is a backend bug, where the Update stack remains stuck in a local scan state and doesn’t ask for deployments in WUFB related to Feature Updates.
Cause for this issue is, when devices are un-enrolling and enrolling in WUfB and the Managed Update Session Orchestrator (MUSE) in some scenarios doesn’t handle this as intended.
Microsoft is working on an fix, which will be included in Autopatch at some point this year, but until then we can fix it manually by added the below mentioned command on the client.
I waited 48 hours and then Windows Feature Packs was offered on the impacted devices.
Intune: Get overview of enrollment status in AutoPatch/WUfB
# Get Status about Windows Client devices from WUfB AutoPatch
function Get-WUfBEnrollmentStatus {
<#
.SYNOPSIS
Retrieves the WUfB AutoPatch enrollment status for Windows client devices managed by Intune (MDM).
.PARAMETER ShowStatus
Optional. If specified, displays output during processing using Write-Host.
.OUTPUTS
[PSCustomObject] array with DeviceName, DeviceId, EnrollmentStateFeature, EnrollmentStateQuality, EnrollmentStateDriver
#>
param (
[switch]$ShowStatus = $false
)
$StatusEnrollmentWUfB = @()
if ($ShowStatus) {
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
}
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"
}
if ($ShowStatus) {
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
}
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
if ($ShowStatus) {
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
}
$enrollmentStateFeature = $null
$enrollmentStateQuality = $null
$enrollmentStateDriver = $null
$errorsWUfB = 0
try {
$wuAsset = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/$deviceId"
$enrollmentStateFeature = $wuAsset.enrollment.feature.enrollmentState
$enrollmentStateQuality = $wuAsset.enrollment.quality.enrollmentState
$enrollmentStateDriver = $wuAsset.enrollment.driver.enrollmentState
$errorsWUfB = if ($wuAsset.errors) { $wuAsset.errors.Count } else { 0 }
if ($ShowStatus) {
Write-Host "π Enrollment State Feature: $enrollmentStateFeature"
Write-Host "π Enrollment State Quality: $enrollmentStateQuality"
Write-Host "π Enrollment State Driver : $enrollmentStateDriver"
Write-Host "π Errors (WUfB) : $errorsWUfB"
}
$StatusObj = [PSCustomObject]@{
DeviceName = $deviceName
DeviceId = $deviceId
EnrollmentStateFeature = $enrollmentStateFeature
EnrollmentStateQuality = $enrollmentStateQuality
EnrollmentStateDriver = $enrollmentStateDriver
ErrorsWUfB = $errorsWUfB
}
$StatusEnrollmentWUfB += $StatusObj
}
catch {
if ($_.Exception.Response.StatusCode.value__ -eq 404) {
if ($ShowOut) {
Write-Host "βΉοΈ Device not found in WUfB AutoPatch"
}
} else {
if ($ShowStatus) {
Write-Host "β Failed to process $deviceName ($deviceId): $($_.Exception.Message)"
}
}
continue
}
}
return $StatusEnrollmentWUfB
}
# Call the function and save results
$WUStatus = Get-WUfBEnrollmentStatus -ShowStatus
# Optionally view as a table
$WUStatus | Sort-Object EnrollmentStateFeature, EnrollmentStateQuality, EnrollmentStateDriver -Descending | Format-Table -AutoSize
# Call the function and save results
$WUStatus = Get-WUfBEnrollmentStatus -ShowStatus
# Optionally view as a table
$WUStatus | Sort-Object EnrollmentStateFeature, EnrollmentStateQuality, EnrollmentStateDriver -Descending | Format-Table -AutoSize
Authentication (Microsoft Graph)
# First, ensure you're connected to Graph with the right scopes:
Connect-MgGraph -Scopes "DeviceManagementManagedDevices.ReadWrite.All", "WindowsUpdates.ReadWrite.All"
Intune: Force Un-enroll Windows Client devices from WUfB AutoPatch, where state is NOT ‘Enrolled’ (Feature Updates only)
# Force Un-enroll Windows Client devices from WUfB AutoPatch, where state is NOT 'Enrolled' (Feature Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-unenrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
write-host "Get enrollment status for devices ... Pleae Wait !"
$StatusEnrollmentWUfB = Get-WUfBEnrollmentStatus
# Build a lookup dictionary from enrollment status for fast access
$StatusLookup = @{}
foreach ($entry in $StatusEnrollmentWUfB) {
$StatusLookup[$entry.DeviceId] = $entry
}
# Filter: MDM-managed Windows devices that are NOT enrolled for 'feature' updates
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and
$_.operatingSystem -like "Windows*" -and
$StatusLookup.ContainsKey($_.azureADDeviceId) -and
$StatusLookup[$_.azureADDeviceId].EnrollmentStateFeature -ne "enrolled"
}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
# Build JSON body for (un)enrollment
$body = @{
updateCategory = "feature"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/unenrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Force Delete All Windows Client devices from WUfB AutoPatch, where Feature Update enrollment state is stuck in ‘enrolling’ or ‘unenrolling’
# Force Delete All Windows Client devices from WUfB AutoPatch, where Feature Update enrollment state is stuck in 'enrolling' or 'unenrolling'
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-delete?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
write-host "Get enrollment status for devices ... Please Wait !"
$StatusEnrollmentWUfB = Get-WUfBEnrollmentStatus
# Build a lookup dictionary from enrollment status for fast access
$StatusLookup = @{}
foreach ($entry in $StatusEnrollmentWUfB) {
$StatusLookup[$entry.DeviceId] = $entry
}
# Filter: MDM-managed Windows devices that are NOT enrolled for 'feature' updates
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and
$_.operatingSystem -like "Windows*" -and
$StatusLookup.ContainsKey($_.azureADDeviceId) -and
$StatusLookup[$_.azureADDeviceId].EnrollmentStateFeature -eq "enrolling" -or $StatusLookup[$_.azureADDeviceId].EnrollmentStateFeature -eq "unenrolling"
}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
Invoke-MgGraphRequest -Method DELETE `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/$deviceId"
}
Intune: Enroll Windows Client devices into WUfB AutoPatch (Feature Updates only) – will also include devices, that was deleted before (catch-up)
# Enroll Windows Client devices into WUfB AutoPatch (Feature Updates only) - will also include devices, that was deleted before (catch-up)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
$ScopedDevices = $devices | Where-Object { $_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
# Build JSON body for enrollment
$body = @{
updateCategory = "feature"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Enroll Windows Client devices into WUfB AutoPatch with ‘NotEnrolled’ state (Feature Updates only)
# Enroll Windows Client devices into WUfB AutoPatch with 'NotEnrolled' state (Feature Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
write-host "Get enrollment status for devices ... Please Wait !"
$StatusEnrollmentWUfB = Get-WUfBEnrollmentStatus
# Build a lookup dictionary from enrollment status for fast access
$StatusLookup = @{}
foreach ($entry in $StatusEnrollmentWUfB) {
$StatusLookup[$entry.DeviceId] = $entry
}
# Filter: MDM-managed Windows devices that are NOT enrolled for 'feature' updates
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and
$_.operatingSystem -like "Windows*" -and
$StatusLookup.ContainsKey($_.azureADDeviceId) -and
$StatusLookup[$_.azureADDeviceId].EnrollmentStateFeature -eq "notEnrolled" # filter for 'NotEnrolled' state. We don't want to include 'unenrolling', but let them finish first
}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
#####################
# Feature Updates
#####################
# Build JSON body for enrollment
$body = @{
updateCategory = "feature"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Force Un-enroll Windows Client devices from WUfB AutoPatch (Feature Updates only)
# Force Un-enroll Windows Client devices from WUfB AutoPatch (Feature Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-unenrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
$ScopedDevices = $devices | Where-Object { $_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
# Build JSON body for (un)enrollment
$body = @{
updateCategory = "feature"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/unenrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Force Delete All Windows Client devices from WUfB AutoPatch (Only use, if you want complete reset !!!)
# Force Delete All Windows Client devices from WUfB AutoPatch (Only use, if you want complete reset !!!)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-delete?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
$ScopedDevices = $devices | Where-Object { $_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
Invoke-MgGraphRequest -Method DELETE `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/$deviceId"
}
Intune: Enroll All Windows Client devices into WUfB AutoPatch (Quality Updates only)
# Enroll All Windows Client devices into WUfB AutoPatch (Quality Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
$ScopedDevices = $devices | Where-Object { $_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
#####################
# Quality Updates
#####################
# Build JSON body for enrollment
$body = @{
updateCategory = "quality"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Enroll All Windows Client devices into WUfB AutoPatch (Driver Updates only)
# Enroll All Windows Client devices into WUfB AutoPatch (Driver Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
# Filter only Windows client devices managed by Intune
$ScopedDevices = $devices | Where-Object { $_.managementAgent -eq "mdm" -and $_.operatingSystem -like "Windows*"}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
#####################
# Driver Updates
#####################
# Build JSON body for enrollment
$body = @{
updateCategory = "driver"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Enroll Windows Client devices into WUfB AutoPatch with ‘NotEnrolled’ state (Quality Updates only)
# Enroll Windows Client devices into WUfB AutoPatch with 'NotEnrolled' state (Quality Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
write-host "Get enrollment status for devices ... Please Wait !"
$StatusEnrollmentWUfB = Get-WUfBEnrollmentStatus
# Build a lookup dictionary from enrollment status for fast access
$StatusLookup = @{}
foreach ($entry in $StatusEnrollmentWUfB) {
$StatusLookup[$entry.DeviceId] = $entry
}
# Filter: MDM-managed Windows devices that are NOT enrolled for 'feature' updates
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and
$_.operatingSystem -like "Windows*" -and
$StatusLookup.ContainsKey($_.azureADDeviceId) -and
$StatusLookup[$_.azureADDeviceId].EnrollmentStateQuality -eq "notEnrolled" # filter for 'NotEnrolled' state. We don't want to include 'unenrolling', but let them finish first
}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
#####################
# Quality Updates
#####################
# Build JSON body for enrollment
$body = @{
updateCategory = "quality"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Intune: Enroll Windows Client devices into WUfB AutoPatch with ‘NotEnrolled’ state (Driver Updates only)
# Enroll Windows Client devices into WUfB AutoPatch with 'NotEnrolled' state (Driver Updates only)
# https://learn.microsoft.com/en-us/graph/api/windowsupdates-updatableasset-enrollassets?view=graph-rest-beta&tabs=http
Write-Host ""
Write-Host "π₯ Retrieving all devices from Intune..."
$devices = @()
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices"
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$devices += $response.value
$uri = $response.'@odata.nextLink'
} while ($uri)
write-host "Get enrollment status for devices ... Please Wait !"
$StatusEnrollmentWUfB = Get-WUfBEnrollmentStatus
# Build a lookup dictionary from enrollment status for fast access
$StatusLookup = @{}
foreach ($entry in $StatusEnrollmentWUfB) {
$StatusLookup[$entry.DeviceId] = $entry
}
# Filter: MDM-managed Windows devices that are NOT enrolled for 'feature' updates
$ScopedDevices = $devices | Where-Object {
$_.managementAgent -eq "mdm" -and
$_.operatingSystem -like "Windows*" -and
$StatusLookup.ContainsKey($_.azureADDeviceId) -and
$StatusLookup[$_.azureADDeviceId].EnrollmentStateDriver -eq "notEnrolled" # filter for 'NotEnrolled' state. We don't want to include 'unenrolling', but let them finish first
}
Write-Host ""
Write-Host "β Scoped devices found: $($ScopedDevices.Count)"
# === LOOP THROUGH DEVICES ===
foreach ($device in $ScopedDevices) {
$deviceName = $device.deviceName
$deviceId = $device.azureADDeviceId
Write-Host ""
Write-Host "π Processing device: $deviceName ($deviceId)"
#####################
# Driver Updates
#####################
# Build JSON body for enrollment
$body = @{
updateCategory = "driver"
assets = @(
@{
"@odata.type" = "#microsoft.graph.windowsUpdates.azureADDevice"
id = $deviceId
}
)
}
$jsonBody = $body | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/beta/admin/windows/updates/updatableAssets/enrollAssets" `
-Body $jsonBody `
-ContentType "application/json"
}
Endpoint: Windows client troubleshooting
This script can be used on the Windows endpoint. You must adjust the variables to your needs
<#
.NAME
Windows Client Troubleshooting (
.SYNOPSIS
.NOTES
.VERSION
1.0
.AUTHOR
Morten Knudsen, Microsoft MVP - https://mortenknudsen.net
.LICENSE
Licensed under the MIT license.
.PROJECTURI
https://github.com/KnudsenMorten/Intune-Windows-Update-Troubleshooting
.WARRANTY
Use at your own risk, no warranty given!
#>
# === CONFIGURATION ===
$CollectCompleteLogs = $false
$CollectWULogsOnly = $false
$RunSystemRepairs = $false
$RunWUReset = $true
$RunPolicySimulation = $true
$RunWUCheck = $true
$RunTelemetryCheck = $true
$RunIMECheck = $true
$RunDSREGCheck = $true
$RunIMERepair = $false
# === SETUP ===
$LogPath = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\FeatureUpdateValidation.log"
New-Item -ItemType Directory -Path (Split-Path $LogPath) -Force | Out-Null
Start-Transcript -Path $LogPath -Append
# === ADMIN CHECK ===
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host ""
Write-Host "β Please run this script as Administrator."
Stop-Transcript
exit 1
}
# === SYSTEM REPAIR SECTION ===
if ($RunSystemRepairs) {
Write-Host ""
Write-Host "β Performing system file integrity checks..."
sfc /scannow
dism /online /cleanup-image /scanhealth
DISM /Online /Cleanup-Image /RestoreHealth
Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase
}
# === TELEMETRY & SERVICE CHECKS ===
if ($RunTelemetryCheck) {
Write-Host ""
Write-Host "π Checking telemetry and diagnostics settings..."
function Check-RegistryValue {
param ([string]$Path, [string]$Name)
try {
$value = Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
return $value.$Name
} catch {
return $null
}
}
$diagTrack = Get-Service -Name "DiagTrack" -ErrorAction SilentlyContinue
Write-Host ""
if ($diagTrack -and $diagTrack.Status -eq 'Running') {
Write-Host "β DiagTrack service is running."
} else {
Write-Host "β DiagTrack service is not running."
}
$telemetryLevel = Check-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "AllowTelemetry"
Write-Host ""
if ($telemetryLevel -ge 3) {
Write-Host "β Telemetry Level: $telemetryLevel"
} else {
Write-Host "β Telemetry Level too low: $telemetryLevel (min: 3)"
}
$policyTelemetry = Check-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device\System" -Name "AllowTelemetry"
Write-Host ""
if ($policyTelemetry) {
Write-Host "β PolicyManager Telemetry Level: $policyTelemetry"
} else {
Write-Host "β PolicyManager Telemetry Level not set."
}
$commercialId = Check-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "CommercialId"
Write-Host ""
if ($commercialId) {
Write-Host "β Commercial ID set: $commercialId"
} else {
Write-Host "β Commercial ID not set."
}
$safeguardHold = Check-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\TargetVersionUpgradeExperienceIndicators" -Name "UpgEx"
Write-Host ""
if ($safeguardHold) {
Write-Host "β Safeguard Hold Detected: $safeguardHold"
} else {
Write-Host "β No Safeguard Holds detected."
}
}
# === WINDOWS UPDATE CHECK ===
if ($RunWUCheck) {
Write-Host ""
Write-Host "π§Ή Checking for and removing policy keys that block Windows Update..."
$pathsToClean = @(
@{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; Name = "DoNotConnectToWindowsUpdateInternetLocations" },
@{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; Name = "DisableWindowsUpdateAccess" },
@{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"; Name = "NoAutoUpdate" }
)
foreach ($item in $pathsToClean) {
try {
$property = Get-ItemProperty -Path $item.Path -Name $item.Name -ErrorAction SilentlyContinue
if ($null -ne $property) {
Remove-ItemProperty -Path $item.Path -Name $item.Name -ErrorAction SilentlyContinue
Write-Host "β Removed $($item.Name) from $($item.Path)"
}
} catch {
# Do nothing
}
}
}
# === WINDOWS UPDATE RESET ===
if ($RunWUReset) {
Write-Host ""
Write-Host "π Performing Windows Update Reset..."
Stop-Service wuauserv -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Stop-Service bits -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Stop-Service cryptSvc -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Stop-Service msiserver -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Stop-Service uhssvc -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
$sdPath = "C:\Windows\SoftwareDistribution"
$catrootPath = "C:\Windows\System32\catroot2"
if (Test-Path $sdPath) {
Remove-Item -Path $sdPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $sdPath -ItemType Directory -Force | Out-Null
}
if (Test-Path $catrootPath) {
Remove-Item -Path $catrootPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $catrootPath -ItemType Directory -Force | Out-Null
}
Start-Service wuauserv -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Start-Service bits -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Start-Service cryptSvc -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Start-Service msiserver -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Start-Service uhssvc -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Write-Host ""
Write-Host "β Windows Update services restarted and folders reset."
}
# === DSREG STATUS ===
if ($RunDSREGCheck) {
Write-Host ""
Write-Host "π Checking Entra ID Join Status..."
$aad = dsregcmd /status | Select-String "AzureAdJoined"
$hybrid = dsregcmd /status | Select-String "DomainJoined"
Write-Host ""
if ($aad -match "YES") {
Write-Host "β Device is Enta ID joined."
} else {
Write-Host "β Not Entra ID joined."
}
Write-Host ""
if ($hybrid -match "YES") {
Write-Host "β Device is Hybrid joined."
} else {
Write-Host "β Not Hybrid joined."
}
}
# === INTUNE MANAGEMENT EXTENSION CHECK ===
if ($RunIMECheck) {
Write-Host ""
Write-Host "π Restarting Intune Management Extension..."
Stop-Service IntuneManagementExtension -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
Start-Service IntuneManagementExtension -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
# === INTUNE MANAGEMENT EXTENSION REPAIR ===
if ($RunIMERepair) {
# Uninstall
msiexec /x "{1F8496D2-52D3-4DA4-BC6D-48A74D1C42E0}" /quiet /norestart
# Re-download and reinstall
Invoke-WebRequest "https://go.microsoft.com/fwlink/?linkid=2156826" -OutFile "$env:TEMP\IME.msi"
Start-Process msiexec.exe -ArgumentList "/i `"$env:TEMP\IME.msi`" /quiet /norestart" -Wait
}
# === POLICY SIMULATION ===
if ($RunPolicySimulation) {
Write-Host ""
Write-Host "π Simulating MDM policy update to trigger MDM policy to refresh..."
reg add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\System" /v "DummyPolicy" /t REG_DWORD /d 1 /f >$null 2>&1
Start-Sleep -Seconds 3
reg delete "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\System" /v "DummyPolicy" /f >$null 2>&1
Write-Host ""
Write-Host "β Policy refresh simulated."
}
# === LOG COLLECTION ===
if ($CollectCompleteLogs) {
Write-Host ""
Write-Host "π Collecting logs..."
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$tempFolder = "C:\Temp\IntuneLogs_$timestamp"
$zipPath = "$tempFolder.zip"
New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null
wevtutil epl "Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin" "$tempFolder\MDM_EventLog.evtx"
wevtutil epl "Microsoft-Windows-WindowsUpdateClient/Operational" "$tempFolder\WU_EventLog.evtx"
Get-WindowsUpdateLog -LogPath "$tempFolder\WindowsUpdate.log"
mdmdiagnosticstool.exe -area "DeviceProvisioning;DeviceEnrollment;Autopilot;DeviceManagementEnterprise-Diagnostics-Provider;Accounts;ModernApps;Connectivity;WNS;PushNotifications" -cab "$tempFolder\MDMDiag.cab"
expand.exe "$tempFolder\MDMDiag.cab" -F:* $tempFolder
Remove-Item "$tempFolder\MDMDiag.cab" -Force
Compress-Archive -Path $tempFolder\* -DestinationPath $zipPath -Force
Write-Host ""
Write-Host "β Logs collected and saved to: $zipPath"
}
if ($CollectWULogsOnly) {
Write-Host ""
Write-Host "π€ Building Windows Update Logs ... Please Wait !"
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$tempFolder = "C:\Temp\IntuneLogs_$timestamp"
New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null
Get-WindowsUpdateLog -LogPath "$tempFolder\WindowsUpdate.log" -ForceFlush -Confirm:$false
}
# === FORCE COMPATIBILITY DATA SUBMISSION | Touching or updating LastDeviceScanTime can help re-initiate the compatibility assessment pipeline ===
Write-Host ""
Write-Host "π€ Forcing compatibility data submission to Windows Update for Business..."
# This registry key is often touched to ensure data is reprocessed
$null = New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\TargetVersionUpgradeExperienceIndicators" `
-Name "LastDeviceScanTime" -Value ([System.Management.ManagementDateTimeConverter]::ToDmtfDateTime((Get-Date))) `
-PropertyType String -Force
Start-Process -FilePath "C:\Windows\System32\compattelrunner.exe" -ArgumentList "-maintenance" -NoNewWindow -Wait
Write-Host "β Compatibility scan & data submission triggered."
# Scan for updates (recommended method)
write-host "Trigger 'Scan for Updates'"
UsoClient StartScan
wuauclt /detectnow
# UsoClient StartDownload # Begin downloading available updates
# UsoClient StartInstall # Install downloaded updates
# UsoClient ScanInstallWait # Full flow: scan + download + install
# PSWindowsUpdate method
<#
Get-WindowsUpdate -MicrosoftUpdate
#>
# COM method
<#
$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$SearchResult = $UpdateSearcher.Search("IsInstalled=0")
$SearchResult.Updates | Select-Object Title, IsDownloaded, IsInstalled
#>
Stop-Transcript