HomeNewsletterCommunityMVP FeedToolsArchiveBlogToday's NewsAboutServicesQuick Links★ Pro Subscribe free
← Back to Blog
Intune IntuneWindows 11Feature UpdatesPowerShellReportingTroubleshooting

Why Won’t Windows 11 Upgrade? Build an Intune Upgrade Blocker Analyzer with PowerShell

IA
Imran Awan
8 October 2026

You have a feature update policy aimed at 1,200 Windows 11 devices. A few weeks in, 300 of them have not moved. Intune shows some as Error, some as Pending, a few as Needs attention. Every one of those is a separate little investigation, and each investigation starts in a different place.

This post builds a small, read-only PowerShell tool that does the first pass for you. It reads the feature update report you already have, sorts every device into a likely blocker, and puts a confidence level on each call: Confirmed, Suspected or Unknown. The point is not to be clever. The point is to never claim a cause the data does not show.

The short version

Intune's feature update report is produced per policy, mixes data that arrives on different clocks, and names an alert but not always a cause. The Windows Upgrade Blocker Analyzer takes those exports (plus an optional device inventory), maps all 46 alert messages Microsoft documents to a cause and a next step, and writes a standalone HTML dashboard, CSV and JSON. It never changes a device, never touches a safeguard hold, and says Unknown when the evidence is missing. Version 1 works from CSV files, not Graph, because Microsoft's documented export-creation call needs write permissions.

Watch this post — YouTube walkthrough
The Ultimate Guide to Troubleshooting Windows 11 Updates in Intune
The Ultimate Guide to Troubleshooting Windows 11 Updates in Intune
Endpoint Weekly
Watch on YouTube Subscribe at @EndpointWeekly

The problem: Intune says "Error", and then you are on your own

Microsoft gives you two views of a feature update rollout. The Windows Feature Update Report lists every targeted device with an update state. The Feature update failures report lists devices that hit an alert, with a named alert message such as DiskFull or SafeguardHold. Both are useful. Neither answers the question you actually have, which is: for each of these 300 devices, what should I look at first?

In practice you want five answers:

  1. Which devices are behind the version I am targeting?
  2. Which of those have a confirmed error, or a hold?
  3. Which have a policy problem, such as a conflict, a pause or a deferral?
  4. Which need someone to look at the device itself?
  5. What is the next step for each group?

Getting those answers by hand means opening the feature update report, the failures report, the devices list and the policy, and keeping the results straight in a spreadsheet. It is slow, and it is easy to quietly treat "no data" as "no problem".

Note: Scope is deliberately narrow: Windows 11 feature updates through Intune feature update policies (24H2, 25H2 and 26H2 builds). The tool is read-only. It will not deploy an upgrade, remove an update, edit a policy or bypass a safeguard hold, and version 1 does not call Microsoft Graph at all.

Why it happens: three data sources, two clocks, one report per policy

The reports are accurate. The trouble is how they fit together. Four facts, all from Microsoft's own documentation, explain most of the confusion.

The report is per policy

Microsoft's Graph reference for the feature update device report (FeatureUpdateDeviceState) lists PolicyId as a required filter. In the portal you also pick one feature update profile before you can generate the report. So a tenant with three feature update policies has three exports.

The consequence is easy to miss: a device that is missing from one export is not necessarily untargeted. It may simply belong to a policy you did not export. A tool that reports "no targeting" from a single file is guessing.

The data arrives on two clocks

Microsoft describes two kinds of data in these reports:

So two exports taken minutes apart can legitimately disagree, and an inventory taken last week disagrees with both. The tool therefore prints the time the evidence is judged against, shows the observation time on every finding, and raises a specific mismatch finding instead of choosing a winner.

An alert is a clue, not always a cause

Microsoft documents 46 alert messages for the failures report. Some name a real cause (DiskFull). Some name a symptom (InstallSetupError). One is described by Microsoft itself as usually false: for PostRestartIssue the documentation says the error is usually false and the update probably succeeded. A tool that treats every alert as a confirmed blocker would send you chasing devices that are fine.

This is how the analyzer groups them. The full mapping is in the repository README.

GroupExample alert messagesMicrosoft's guidance, paraphrased
Safeguard holdSafeguardHoldRead the hold ID in the report's Deployment Error Code column, then check the Windows release health dashboard.
CompatibilityIncompatible, IncompatibleArchitectureReview ScanResult.xml on the device for Block Type=Hard.
Disk spaceDiskFullFree up space on the Windows partition and retry.
Policy conflictPolicyConflict, PolicyConflictDeferral, DeploymentConflictCheck MDM and Group Policy settings against the deployment, or remove the device from the extra deployment.
Device registrationDeviceRegistrationIssue and three variantsCheck Microsoft Entra join state, and that the Microsoft Account Sign-in Assistant service is not disabled.
DownloadDownloadTimeout, DownloadConnectionIssueCheck network, proxy or firewall, WSUS if used, and the BITS service.
InstallationInstallSetupError, InstallFileLockedCheck that the BIOS and drivers are up to date, then retry.
RollbackRollbackInitiatedRun SetupDiag and do not retry until the cause is understood.
Probably a false alarmPostRestartIssueCheck the update actually installed before doing anything.

What the export does not contain

The documented columns include the policy, the target version, the update state and substate, the last scan time and the latest alert message with its recommended action. They do not include the setup logs, a hardware inventory, or the result of Intune's assignment evaluation for your groups. That is why some of the tool's findings stop at Unknown. When the cause lives on the device, a report cannot name it, and the honest output is a pointer to the device-level evidence.

Do not assume read-only automation: Microsoft's Graph documentation for creating an Intune report export job (POST /deviceManagement/reports/exportJobs) lists only ReadWrite permissions: DeviceManagementConfiguration.ReadWrite.All, DeviceManagementApps.ReadWrite.All or DeviceManagementManagedDevices.ReadWrite.All, for delegated and application access alike. A script that creates its own exports cannot honestly be called read-only on that basis. This is the reason version 1 works from files you export yourself.

How to verify: get the right exports before you analyse anything

Nothing downstream is better than its input. Spend five minutes here and the rest is quick.

Step 1: list your feature update policies

Open Devices › Manage updates › Windows updates › Feature updates. Every policy shown here needs its own export. Write the names down.

Step 2: generate the report for each policy

  1. Sign in to the Microsoft Intune admin center.
  2. Go to Reports › Windows Updates. The default view is the Summary tab.
  3. Select Windows Feature Update Report.
  4. Select Select a feature update profile, choose the first policy, then select Generate report.
  5. Export the result as CSV, if your report offers an export option, and name the file after the policy.
  6. Repeat for every other policy.
Reports › Windows Updates › Windows Feature Update Report › Generate report
Gotcha: I could not confirm from Microsoft's documentation that the Windows Feature Update Report has an export button in every tenant. Microsoft says Intune's reporting infrastructure supports export in general, and that this report supports filtering, searching, paging and sorting, but its feature update article does not name an export option. If yours has none, the alternative is the Graph export API, which brings the permission issue above. Check your own portal before you plan around it.

Step 3 (recommended): export the device inventory

Export the Intune Devices report with at least the device ID, device name, OS version and last check-in time. It lets the tool catch things the feature update report cannot show: devices that stopped checking in, devices still on Windows 10, and devices that are behind but in none of your exports.

Step 4: check the columns

The tool accepts both the Graph property names and the portal's column labels, compared without regard to case or spaces, so DeviceId and Intune Device ID are the same column. Confirm your file has a device ID column. This one-liner prints the column names of the first row:

PowerShell - list the columns in an export
(Import-Csv .\Samples\sample-feature-update-report.csv | Select-Object -First 1).PSObject.Properties.Name AADDeviceId AggregateState Build CurrentDeviceUpdateStatus CurrentDeviceUpdateStatusEventDateTimeUTC CurrentDeviceUpdateSubstatus DeviceId DeviceName EventDateTimeUTC FeatureUpdateVersion LastWUScanTimeUTC LatestAlertMessage LatestAlertRecommendedAction OwnerType PolicyId PolicyName UpdateCategory UPN

If the state columns hold numbers instead of words, Intune has given you the raw enum values. Microsoft's export API documentation explains that each localisable column can have a second column ending in _loc with the readable text, and the tool prefers that column when it exists. If neither is readable the tool reports the state as unrecognised and does not guess.

Tip: Microsoft's own advice for export automation is to select columns explicitly rather than depend on a report's default columns. Even for manual exports, keep the same columns every time so that month-to-month comparisons mean something.

The fix: classify the evidence, with a confidence level on every finding

"Fix" is a strong word for a reporting tool. What it fixes is the triage: instead of 300 equal-looking devices you get a ranked list, grouped by cause, where every call says how sure it is.

How it works

1. Your files
Feature update report CSV per policy, optional device inventory CSV
2. Normalise
Match columns, prefer readable values, correlate by device ID (never by name)
3. Rules
46-alert map, state rules, staleness checks, report-versus-inventory checks
4. Outputs
HTML dashboard, findings CSV, devices CSV, evidence JSON

Two design choices matter more than the rest.

Match on IDs, not names. Two different laptops can carry the same name, for instance after a re-image. The tool keys every record on the Intune device ID, keeps the two records apart, and flags both as having a duplicate name.

Keep every finding. A device can be stale and have an older install error. Forcing one diagnosis would hide one of them, so the tool keeps both, each with the time it was observed.

The confidence model

LevelIt meansExample
ConfirmedAn explicit status or alert from the report or inventory names the cause.Alert DiskFull on a device that is behind target.
SuspectedSeveral signals point at a likely cause, but none proves it.An install has shown no new event for 12 days.
UnknownThe evidence is missing, stale or contradictory.A device behind target that appears in none of the exports you supplied.

Run it

The tool needs PowerShell 7 or Windows PowerShell 5.1 and no extra modules. The repository ships synthetic sample data, so you can try it before touching real exports. -AsOf pins the clock so the output is repeatable.

PowerShell - analyse the bundled sample data
.\Invoke-UpgradeBlockerAnalyzer.ps1 ` -FeatureUpdateReportPath .\Samples\sample-feature-update-report.csv ` -DeviceInventoryPath .\Samples\sample-devices.csv ` -AsOf '2026-10-08T09:00:00Z'

For your own data, pass one report file per policy and the inventory:

PowerShell - your own exports
.\Invoke-UpgradeBlockerAnalyzer.ps1 ` -FeatureUpdateReportPath .\Prod-Ring.csv, .\Pilot-Ring.csv ` -DeviceInventoryPath .\Devices.csv ` -RedactDeviceNames

It writes four files to .\wuba-output: the dashboard, a findings CSV, a devices CSV and an evidence JSON. The three thresholds (-StaleCheckinDays 30, -StaleScanDays 14, -StalledDays 7) are this tool's defaults, not Microsoft values. Change them to suit your estate.

The findings it produces

FindingBased onConfidence
SafeguardHoldAlert SafeguardHoldConfirmed
CompatibilityBlockAlerts Incompatible*Confirmed
InsufficientDiskSpaceAlert DiskFullConfirmed
PolicyConflictAlerts PolicyConflict*, DeploymentConflictConfirmed
InstallationFailure, RollbackInitiatedInstall and rollback alerts or statesConfirmed
UpdateOnHoldOn hold state: admin paused, service paused, deferredConfirmed
StaleDeviceInventory check-in older than thresholdConfirmed
Windows10DeviceInventory build below 22000Confirmed
PostRestartResultUnknownAlert PostRestartIssueSuspected
StalledInstall, StaleWindowsUpdateScanNo recent event, or an old scanSuspected
ReportInventoryMismatchReport says installed, inventory build is lowerSuspected
NotInFeatureUpdateReportBehind target and in none of your exportsUnknown
NeedsAttentionNoDetail, UnmappedAlert, InsufficientEvidenceA problem state with no usable detailUnknown

Reading the dashboard

The report is one standalone HTML file. It needs no web server, loads nothing from the internet and works offline. The top shows how many devices are up to date, in progress, confirmed blockers, and needing investigation, then a chart of findings by category.

Windows Upgrade Blocker Analyzer dashboard showing 147 devices assessed, 52 up to date, 20 in progress, 48 confirmed blockers and 27 needing investigation, above a bar chart of findings by category
The dashboard header and category chart, generated from the bundled synthetic sample data. No real tenant data is shown.

Below the chart is a filterable, sortable device table. Select a device and its evidence opens: every finding, its confidence and severity, the source it came from, when it was observed, and the next step.

Dashboard evidence panel for one device showing two findings, InsufficientDiskSpace confirmed high from the feature update report and StaleDevice confirmed medium from the devices inventory, each with a next step and an observation timestamp
One device, two findings kept side by side: a DiskFull alert from the feature update report, and a 55-day-old check-in from the inventory. Synthetic data.
Tip: Views are shareable. Add a fragment to the report's file path, for example wuba-report.html#category=SafeguardHold, and it opens with that filter applied. Supported keys are status, confidence, category, q and device.

What to do with each group

Watch out: Never bypass a safeguard hold to make a number go green. Holds exist because Microsoft found a known problem on that hardware or software combination. A device that is held is safer behind than upgraded into a broken state.
Gotcha: PostRestartIssue looks alarming in the failures report, but Microsoft says the error is usually false. The tool marks it Suspected at low severity. Confirm the build on the device before you open a ticket.

Privacy and safe sharing

A device name is not trusted data. It is a string set by whoever imaged the machine, and it ends up in a CSV and a web page. The tool treats every name that way:

Treat the output files as sensitive regardless. They still contain device IDs and, unless redacted, device names.

Proof it worked: 147 sample devices, from export to answer

The sample set has 136 report rows and 147 devices in the inventory, so 11 devices appear only in the inventory. All of it is synthetic. Here is the real console output of the command above:

PowerShell - output on the bundled sample data
WUBA - Windows Upgrade Blocker Analyzer (read-only) Evidence as of 2026-10-08 09:00 UTC | 136 report row(s) in 1 file(s) | inventory: 147 device(s) Devices assessed : 147 Up to date : 52 In progress : 20 Confirmed blockers : 48 Needs investigation : 27 Findings by category and confidence: InsufficientDiskSpace Confirmed 9 SafeguardHold Confirmed 7 NotInFeatureUpdateReport Unknown 6 CompatibilityBlock Confirmed 5 PolicyConflict Confirmed 5 UpdateOnHold Confirmed 5 StaleDevice Confirmed 5 StaleWindowsUpdateScan Suspected 5 InsufficientEvidence Unknown 4 StalledInstall Suspected 4 DeviceRegistrationIssue Confirmed 3 DownloadFailure Confirmed 3 InstallationFailure Confirmed 3 NeedsAttentionNoDetail Unknown 3 Windows10Device Confirmed 3 UpdateCancelled Confirmed 2 ReportInventoryMismatch Suspected 2 RollbackInitiated Confirmed 2 PostRestartResultUnknown Suspected 2 UnmappedAlert Unknown 1 WindowsUpdateHealth Confirmed 1 Report : .\wuba-output\wuba-report.html CSV : .\wuba-output\wuba-findings.csv JSON : .\wuba-output\wuba-evidence.json

Read it from the top. Of 147 devices, 52 are up to date (50 report an installed update, 2 are inventory-only devices already on the target build). 48 have at least one Confirmed blocker of medium or high severity. 27 need a human to look because the evidence is only Suspected or Unknown. 20 are simply in progress, with nothing wrong in the data.

Three devices worth opening

A device with two findings. WIN11-SIN-132 has a DiskFull alert and has not checked in for 55 days. The tool keeps both: the alert says what stopped the install, the stale check-in says its other evidence may be out of date. See the evidence screenshot above.

A device that is probably fine. WIN11-NYC-111 shows PostRestartIssue. The tool reports it as Suspected, low severity, with Microsoft's own caveat as the next step.

Dashboard evidence panel for one device showing a single PostRestartResultUnknown finding marked Suspected and Low, with the next step to verify the build actually installed
A Suspected, low-severity finding. The next step is to verify, not to remediate. Synthetic data.

Two devices with the same name. WIN11-LON-010 exists twice with different device IDs. One installed the update. The other is in the inventory, behind target, and in no report, so it is flagged NotInFeatureUpdateReport with Unknown confidence rather than "not targeted". Matching on names would have merged them and hidden the second.

The tests, and proof they can fail

The repository has 35 Pester tests: the rules, ID-based correlation, input validation, output escaping, redaction, end-to-end runs on the sample data and on a fixture of deliberately hostile device names, and a check that all 46 documented alert messages are mapped. They pass on PowerShell 7.6 and Windows PowerShell 5.1 with Pester 3.4.

PowerShell - Invoke-Pester .\Tests
PowerShell 7.6: Passed 35 Failed 0 Total 35 Windows PowerShell 5.1: Passed 35 Failed 0 Total 35

A green test run only means something if the tests can go red. So I broke two behaviours on purpose, in a throwaway copy: I switched off the escaping of < in the HTML data block, and I let the generic "In progress" aggregate override a specific "Pending" status. The matching tests failed both times. Building the escaping, I also hit a real bug: the escape sequences in the source file had been silently decoded when it was saved, so the "escape" replaced a less-than sign with itself and the hostile device names went into the page raw. I caught it by inspecting the generated HTML, fixed it by building the escapes from character codes, and then added the tests that now guard it.

Speed

On a laptop with PowerShell 7, 2,000 devices take about 7 seconds from CSV to finished report, and 10,000 devices about 26 seconds, producing a 6.7 MB HTML file. Opening that 10,000-device report in a Chromium-based browser took about 1.4 seconds, and applying a filter about 0.1 seconds.

What this still cannot tell you

Where this goes next

Version 1 stops at files on purpose. Three things are not built yet, and I would rather say so than imply otherwise: automatic export through Graph (blocked on the permission question above, and note Microsoft documents throttling of 100 export requests per tenant per minute, with lower per-user and per-app limits), a read-only local probe for disk, TPM, Secure Boot and SetupDiag results, and scheduled trend reports. If you try the tool on real exports and a column name or a state word is not recognised, that is exactly the feedback that makes the next version better.

References

Community deep-dives and related tools

AuthorPost or projectWhat it adds
Peter van der WoudeEasily exporting Intune reports using Microsoft GraphThe export request, status polling and download, walked through step by step (2020, so check current docs).
Jannik ReinhardIntune mass export with the Graph Report APIA PowerShell export loop that lists FeatureUpdateDeviceState. The author notes its authentication code is retired, so use it for the request shape only.
UniFy EndpointWindows-Updates-ReadinessIntune remediation scripts that check, and can fix, update readiness on the device itself. It starts where this tool stops: on the endpoint. Its remediation script changes settings, so pilot it first.

Related on EndpointWeekly

Windows Upgrade Blocker Analyzer

Download it from Imran76Awan/Windows-Upgrade-Blocker-Analyzer — public, no sign-in required. It is read-only: it reads CSV files you supply and writes four output files. It calls no APIs and changes nothing on any device. Validate it in your own environment before relying on it.

● Invoke-UpgradeBlockerAnalyzer.ps1 — the tool
● Samples — synthetic input data and the generator
● Tests — 35 Pester tests and fixtures
● Live sample report — open the dashboard on the sample data, no download needed
View the repository on GitHub
Was this post helpful?
React below — no account needed
Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Intune
Microsoft Store Apps in Intune — Deployment & Troubleshooting,…
Store for Business is gone — modern Intune Store apps are winget-backed. The full…
Intune
Enrollment Time Grouping and Client-Driven Compliance: Two…
Enrollment Time Grouping puts a device in its target group during enrollment instead of…
Intune
Your Intune Portal Shows What Was Assigned. This Script Shows…
The Intune admin center reports what a policy was assigned to. It doesn't tell you what…