The feature update finished. Compliance went green. Then the tickets started arriving.
Three said the keyboard layout had changed. Two said Server Manager had disappeared. One said the DNS snap-in was gone from a jump box that had had it for two years. Nobody reported an error, because nobody saw one.
Nothing on those devices is corrupt. sfc /scannow will come back clean. DISM /RestoreHealth will come back clean. The component store will report itself healthy, because it genuinely is healthy. What you are looking at is documented servicing behaviour around optional content, and it has a precise mechanism, a cause that depends entirely on how you deploy, and a detection method that does not involve waiting for your users to notice.
Features on Demand and language packs are not in the Windows image. Microsoft ships the metadata and leaves the payload out, so every FoD install is a fetch from somewhere. During an in-place upgrade, Windows Setup builds a second clean OS alongside the old one, enumerates the optional content you already had, and plans to reinstall the new version of it — but it "must look elsewhere to get the Windows packages". When it cannot find them, you get "an update that is missing features on the device". That is a source failure, not corruption, so SFC and DISM repair cannot help and cleanup did not cause it. Whether it happens at all depends on your deployment path: Windows Update and post-22H2 WSUS-with-UUP migrate optional content, while a media-based task sequence with Dynamic Update disabled does not. Inventory it with Get-WindowsCapability -Online before you upgrade, diff it after, and remember that Add-WindowsCapability "fails silently; no exceptions are thrown" — so a re-add script can report success while doing absolutely nothing.
The problem: the update succeeded and the features are gone
This failure has a signature that makes it hard to triage from a ticket queue. The tickets do not describe an update problem. They describe missing software.
| What the user reports | What is actually missing | The name you need to search for |
|---|---|---|
| "My keyboard layout changed" / "Windows is in English now" | A language pack or one of its language features | Language.Basic~~~en-GB~0.0.1.0 |
| "Handwriting stopped working on my pen device" | The handwriting recognition language FoD | Language.Handwriting~~~en-US~0.0.1.0 |
| "Dictation and Narrator voices are gone" | Speech and text-to-speech language FoDs | Language.Speech~~~en-US~0.0.1.0 |
| "My admin tools disappeared" | RSAT capabilities | Rsat.ServerManager.Tools~~~~0.0.1.0 |
| "The AD Users and Computers snap-in is gone" | The AD DS and AD LDS tools capability | Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 |
| "My scripts stopped working" (WMI command line) | The WMIC capability | WMIC~~~~ |
| "ssh is not recognised" | The OpenSSH client capability | OpenSSH.Client~~~~0.0.1.0 |
Look at what is not in that table: an error code. No failed KB in update history, no rollback, no red banner in the Windows Update pane. From the fleet's point of view the upgrade was a clean success, which is exactly why this reaches you as seven unrelated application tickets rather than one patching incident.
It is worse than that. The evidence you need — what the device had installed before the upgrade — lived on the device, and the upgrade has already replaced it. By the time the first ticket lands, the baseline is gone unless you captured it.
sfc /scannow, then DISM /Online /Cleanup-Image /RestoreHealth, then a component store cleanup "to tidy up". All three are wrong and the third is actively harmful to your investigation. The payload for a missing FoD is not damaged, it is absent by design — Microsoft documents that for Features on Demand "the metadata for Features on Demand is included. However, the binaries and other files associated with the feature are not included." A repair tool that fixes damaged files has nothing to fix. Meanwhile /StartComponentCleanup /ResetBase "removes all superseded versions of every component", which permanently narrows your rollback options while you are still trying to work out what changed.The one thing worth doing in the first sixty seconds is the opposite of a repair: take a read-only inventory and write it down. Everything in the rest of this post depends on having that list.
Why it happens: optional content was never in the image
A capability is a request, not a component
Start with the thing most people get wrong. A Feature on Demand is not a Windows component that got deleted. It is a component that was never fully there.
Microsoft's definition is short: "Features on Demand (FODs) are Windows features that can be added at any time", and "when Windows 11 or Windows Server needs a new feature, it can request the feature package from Windows Update." The DISM reference is blunter — capabilities are "a Windows package type allows you to request services like .NET or languages without specifying the version". You ask for Rsat.Dns.Tools; the servicing stack works out which build matches this OS and fetches it. A normal component, by contrast, ships inside the image and is serviced in place from the component store.
The split between metadata and payload is where the trouble starts. The .NET Framework 3.5 support article states it plainly for the best-known FoD: "The metadata for Features on Demand is included. However, the binaries and other files associated with the feature are not included. When you enable a feature, Windows tries to contact Windows Update to download the missing information to install the feature."
So the device always knows the feature exists. It does not necessarily have the bits. Every FoD operation is a fetch, and every fetch can fail.
The capability name tells you which kind you are dealing with. It is four tilde-delimited fields after the feature name, and the third is the language:
| Capability name | Language field | What that means |
|---|---|---|
Rsat.Dns.Tools~~~~0.0.1.0 | empty | Language-neutral capability; resources arrive as satellite packages |
Language.OCR~~~en-GB~0.0.1.0 | en-GB | A language FoD, scoped to one locale |
WMIC~~~~ | empty, no version | Language-neutral, and per Microsoft it has satellites |
Language.Fonts.Thai~~~und-THAI~0.0.1.0 | und-THAI | Script-scoped font FoD, not locale-scoped |
Satellites explain the duplicated package names in your logs
Microsoft documents two flavours of FoD. "FODs without satellite packages" are monolithic — one .cab with all language resources inside. "FODs with satellite packages" are language-neutral, with "language and/or architecture resources in separate packages (satellites)", and "only the packages that apply to the Windows image are installed, which reduces disk footprint".
This is why a single capability produces several package names in the Setup log, one per installed language. On the lab device I have in front of me — Windows 11 Enterprise 25H2, build 26200.9168 — the Setup log carries real Event 2 entries for exactly that pattern.
Two things are worth pulling out of that block. First, the en-US and en-GB pair for WMIC is not a duplicate or an error — it is the satellite model working as documented. Second, Microsoft-Windows-WinOcr-FOD-en-US is the payload behind the Language.OCR capability, which is the sort of thing nobody notices is gone until a document-scanning workflow quietly stops returning text.
DISM /Export-Source.What the upgrade actually does
Now the mechanism itself, because this is the part that turns a mystery into a diagnosis. Microsoft's own description of an in-place update is worth reading twice:
"When Windows Setup performs an in-place update, the new operating system is written to the user's disk alongside the old version in a temporary folder, where a second clean operating system is installed and prepared for the user to move into. When operation happens, Windows Setup enumerates optional content installed already in the current version and plans to install the new version of this content in the new operating system."
Setup does the right thing: it looks at what you had and intends to give it back. Then comes the sentence that explains every ticket in your queue:
"Windows Setup needs access to the optional content. Since optional content isn't in the Windows image by default, Windows Setup must look elsewhere to get the Windows packages, stage them, and then install them in the new operating system. When the content can't be found, the result is an update that is missing features on the device, a frustrated end user, and likely a help desk call."
Microsoft even has a name for it: "failure to migrate optional content during update". The feature was not dropped by a policy or eaten by a cleanup task. Setup asked for it, could not find a source, and moved on without it.
The reason it is absent by design is deliberate: optional content "isn't included by default in the Windows image file", because shipping it separately "reduces the disk footprint of Windows" and "reduces the time needed to service the operating system". Smaller image, faster servicing, and a hard dependency on reaching a source.
Whether it happens at all depends on how you deploy
This is the nuance to get right, and it is where a lot of blog advice overreaches. It is not true that a feature update always drops Features on Demand. Microsoft documents seven acquisition options with different migration behaviour. Condensed to what matters operationally:
| Deployment path | Migrates optional content across the upgrade? | Notes from the documentation |
|---|---|---|
| Windows Update / Windows Update client policies | Yes | Migration and acquisition "just work when the device is connected to an update service that uses the Unified Update Platform". A failing language pack rolls the update back. |
| WSUS or Configuration Manager with on-premises UUP | Yes, from Windows 11 22H2 | Since March 2023: "FODs and languages will automatically migrate for devices that perform an in-place update using an approved Windows 11, version 22H2 client feature update from WSUS." |
| Media-based upgrade with Dynamic Update enabled | Yes | Dynamic Update "acquires optional content during the update process", and Setup enables it by default. |
| Media-based upgrade with Dynamic Update disabled | No | Stated directly: "media-based upgrades don't migrate FOD and languages (unless Dynamic Update is enabled)". |
| Customised image (FODs injected before deployment) | Yes, for what you injected | Anything a user added themselves is still lost. |
Setup /InstallLangPacks | Partial | Language content only. "We treat InstallLangPacks failures as fatal, and roll back the entire upgrade." |
| Alternate source policy only | No | Addresses user-initiated acquisition afterwards, not migration during the upgrade. |
If your fleet upgrades through a Configuration Manager task sequence with Dynamic Update turned off — a very common choice, usually made to control bandwidth or to freeze the cumulative update level — then row four is your answer, and it is working exactly as documented.
There is one mercy worth knowing about. For media-based updates, "Windows will automatically try again once the new operating system boots. We call this latent acquisition." So some devices heal themselves a few hours later, once they have a route to a source. That is also why this failure can look intermittent across a ring: the devices that reached Windows Update recovered, the ones sitting behind a strict proxy did not.
Add-WindowsCapability reference still carries the resulting hard line: "starting with Windows 10 version 1809, FOD and language packs can only be installed from Windows Update." But that note is now dated. "Starting in Windows 11, version 22H2, on-premises Unified Update Platform (UUP) updates were introduced. FoDs and language packs are available from WSUS again." If you are on 22H2 or later, the cmdlet's own note is describing a world you have left./Disable-Feature /Remove. Second, there is no Microsoft documentation linking /StartComponentCleanup or /ResetBase to FoD payload removal. Blaming your cleanup schedule is unverified; the documented removal route is /Disable-Feature /Remove.One last piece of the design worth knowing, because it explains why FoDs usually do survive: Reserved Storage is sized with them in mind. Microsoft notes that FoD install sizes vary because the figure "accounts for growth in the Reserved Storage size to ensure there is enough disk space for all installed Feature on Demand packages to be preserved during updates." Preservation is the intent. Missing features mean the intent was defeated by a missing source.
How to verify: take the inventory before you touch anything
Here is the diagnostic path. It is deliberately read-only until step 7, and step 1 is the one that decides whether the rest is easy or guesswork.
- Capture a capability census on every device before the upgrade ring runs.
Get-WindowsCapability -Onlineand store the result. Without this, you are reconstructing history from tickets. - On an affected device, census again and diff. Anything that was
Installedbefore and isNotPresentnow is your loss list. This is the whole diagnosis in one comparison. - Separate capabilities from packages. Language packs are packages, not capabilities.
Get-WindowsCapability -Onlinewill not show you a display language;Get-WindowsPackage -OnlineandGet-InstalledLanguagewill. - Check the language view per feature, not per language. A locale can be present with Basic and OCR but no handwriting or speech. "Language installed" is not one boolean.
- Read the Setup log for Event 2 entries naming FoD packages. If the packages were staged, the source was reachable and your problem is elsewhere. If they are absent, Setup never got the content.
- Determine which source the device would actually use. Read the repair-source policy value, and confirm whether the device can reach Windows Update at all. This is what decides whether your re-add can succeed.
- Only now re-add, one capability at a time, respecting dependencies.
- Re-census to prove it. Never trust the exit code — see the silent-failure trap below.
Step 1 and 2: the census
This is the single most useful command in the whole topic, and it is entirely read-only.
The 369-to-56 ratio is the single most clarifying number in this post. When a colleague sends you a screenshot of Get-WindowsCapability output full of NotPresent and asks whether the device is broken, the answer is no. NotPresent is the default state of almost everything.
Step 3 and 4: the language view is a different view
Language packs live in a different namespace from capabilities, and they need different cmdlets. The LanguagePackManagement module handles the language-level picture; the DISM module handles the capability-level picture underneath it.
That asymmetry is the reason fleet-scale detection has to be done at capability granularity. A dashboard that reports "installed display languages" will show green on a device whose speech recognition vanished.
Get-WindowsCapability -Online to a per-device file keyed on hostname and OS build, ship it somewhere central, and have the post-upgrade run diff against it. The comparison is trivial once you have both sides; the entire difficulty of this problem is that most shops only ever have the "after" side. Add Get-WindowsPackage -Online and Get-InstalledLanguage to the same snapshot and you cover packages and locales too.Step 6: which source would this device actually use?
The repair-source policy is registry-backed, and there is a real trap in reading it. All of these values live under one key:
| Value | Meaning | What to look for |
|---|---|---|
LocalSourcePath | The "Alternate source file path" from the Specify settings for optional component installation and component repair policy. The only element the in-box Servicing.admx declares. | Present and populated = an alternate source is configured. A fully qualified path; several separated by semicolons; a WIM needs the wim: prefix and an image index, e.g. wim:\\server\share\install.wim:3. Absent = no alternate source, so the device depends on Windows Update or WSUS. |
CountryCode | An unrelated value that happens to live in the same key. Nothing to do with the repair-source policy. | The trap. On the lab device this key exists and contains only CountryCode. A script that does Test-Path on the key and concludes "policy configured" is wrong. Test for LocalSourcePath specifically. |
RepairContentServerSource | Community-attributed to the "download directly from Windows Update instead of WSUS" option. | Unverified. Not documented on Microsoft Learn and not declared in the shipped Servicing.admx. Treat as community lore. Microsoft documents that the corresponding policy option was removed starting in Windows 11, version 24H2. |
UseWindowsUpdate | Community-attributed to the same policy family. | Unverified. Same caveat. Do not build fleet logic on either of these two without testing on your own build. |
That last point deserves the evidence, because it is the sort of claim that gets repeated for years after it stops being true.
The event log view
All four of these are in the Setup log, provider Microsoft-Windows-Servicing.
| Event ID | Message | What it tells you |
|---|---|---|
| 2 | Package <name> was successfully changed to the Staged state. | The payload was found and registered. For an FoD package name, this is positive proof the source was reachable. Absence of these during an upgrade is the fingerprint of a migration failure. |
| 4 | A reboot is necessary before package <name> can be changed to the Installed state. | Staged but not finished. Do not diagnose a "missing" feature until the pending reboot has happened — you may be looking at an incomplete transition, not a loss. |
| 1013 | Corruption detection started (provider Microsoft-Windows-Servicing). | Component store corruption scanning. Present here only to rule it out: if you see no 1013/1014 pair, this is not a corruption story. "Detection Only: 1" in the payload distinguishes a ScanHealth from a RestoreHealth. |
| 1014 | Corruption detection completed. | Pairs with 1013. A clean pair alongside missing features is the confirmation that you have a source problem, not a damage problem. |
Which binaries are actually doing this work
DISM is a thin host over pluggable providers, and knowing which provider owns which job tells you where to look when something misbehaves. All of the following were read off the lab device.
| File | Role | Where it lives |
|---|---|---|
Dism.exe | "Dism Image Servicing Utility" — the command-line host. Version 10.0.26100.8457 here. | C:\Windows\System32\ |
DismCore.dll | "DISM Core Framework" — loads and dispatches to the providers. | C:\Windows\System32\Dism\ |
CbsProvider.dll | "DISM Package Provider" — the provider behind /Get-Capabilities, /Add-Capability and the package options. 1,304,040 bytes, one of the largest, since it owns the whole CBS surface. | C:\Windows\System32\Dism\ |
IntlProvider.dll | "DISM International Provider" — 357,864 bytes. The half of DISM that handles language packs rather than capabilities. | C:\Windows\System32\Dism\ |
Microsoft.Dism.PowerShell.dll | DISM PowerShell module assembly, 649,216 bytes. Provides Get-WindowsCapability, Add-WindowsCapability, Get-WindowsPackage. | C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Dism\ |
Microsoft.LanguagePackManagement.Powershell.Commands.dll | LanguagePackManagement module assembly (v1.0), 27,648 bytes. Provides Get-InstalledLanguage and Install-Language. | ...\WindowsPowerShell\v1.0\Modules\LanguagePackManagement\ |
TrustedInstaller.exe | "Windows Modules Installer" — commits the change. | C:\Windows\servicing\ |
CbsApi.dll | The CBS API surface TrustedInstaller exposes, 83,432 bytes. | C:\Windows\servicing\ |
cbscore.dll | The servicing stack core, and widely mislocated: it is not in System32. This device runs stack 10.0.26100.9156. | versioned WinSxS servicing-stack dir |
Servicing.admx | Policy template for the repair-source setting. 1,321 bytes, declaring only LocalSourcePath. | C:\Windows\PolicyDefinitions\ |
The fix: pick a source, re-add, then prove it happened
The source resolution order, which explains every failure
DISM and the capability cmdlets resolve a source in a fixed documented order. Memorise it, because every source-missing error is a statement about one of these three steps:
- If
/Sourceis specified, DISM looks in the specified locations first. With multiple sources, "the files are gathered from the first location where they are found and the rest of the locations are ignored." - If
/Sourceis not specified, or the files were not found there, DISM checks the locations specified by Group Policy — that isLocalSourcePath. - If the files are still not found, the image is online, and
/LimitAccesswas not specified, it looks on Windows Update.
Read step 2 again. A wrong /Source does not fail fast; it falls through to policy and then to Windows Update. That is why the same command behaves differently on two devices in the same ring.
The trap that makes re-add scripts lie
This is the single most important operational detail in the post, and it is documented in one sentence in the Add-WindowsCapability reference under the -Source parameter:
"If you do not specify a Source, the default location set by Group Policy is used. If that fails, Windows Update is also used for online images, unless LimitAccess is specified. When all fail, the cmdlet fails silently; no exceptions are thrown."
A remediation script that wraps Add-WindowsCapability in a try/catch and reports success on no exception will report success on every device in your fleet while installing nothing on any of them. There is a second, quieter version of the same problem: "if the package specified by the Name parameter is already installed, this command does not return an error message, regardless of whether the required files are present."
The only safe pattern is to verify state afterwards, from the same source of truth you used to detect the loss.
Language packs take a different route
Do not try to fix a missing display language with Add-WindowsCapability alone. Use the language module, which handles the pack and its associated FoDs together:
Install-Language -Language en-GB "downloads and installs the available language components for the specified language onto a device". Two switches matter operationally: -CopyToSettings also sets the system and default device settings, and -ExcludeFeatures means "the associated language Features on Demand won't be installed" — which is almost certainly not what you want when you are recovering from exactly that loss. -AsJob runs it asynchronously, useful at scale.
There is a hard constraint to plan around. The alternate-source policy "doesn't support installing language packs from an alternate source file path, only Features on Demand. If the policy is configured to acquire content from Windows Update, language packs will be acquired." A network share fixes FoDs. It does not fix language packs.
When the re-add fails with a source error
If step 3 of the resolution order is unavailable — no configured LocalSourcePath, no route to Windows Update, WSUS not serving optional content — the re-add fails with a source error rather than doing nothing. The one you will see most is 0x800F081F, documented as CBS_E_SOURCE_MISSING, "the source files could not be found". Microsoft lists three conditions behind it: the path does not contain the required files, the user lacks at least READ access to the location and files, or the file set is "corrupted, incomplete, or invalid for the version of Windows that you are running".
That third condition catches people constantly with FoDs specifically, because the ISO must match the OS release — "make sure you're using the ISO that matches your Windows image version". A 24H2 Languages and Optional Features ISO against a 25H2 device is an invalid file set, not a network problem. A sibling post covers 0x800F081F and the /Source mechanics in depth, so I will not duplicate it here.
Two neighbours are worth recognising on sight, since they point somewhere different: 0x800F0906 is CBS_E_DOWNLOAD_FAILURE — "the computer cannot download the required files from Windows Update", so look at proxy and firewall. 0x800F0907 is CBS_E_GROUPPOLICY_DISALLOWED, which Microsoft ties to the policy being configured to never attempt a download from Windows Update. Given that this policy option was removed in 24H2, seeing 0x800F0907 on a modern build is a strong hint about leftover configuration.
The structural fix
Re-adding capabilities device by device is remediation, not a fix. The fix is to remove the source dependency from the upgrade path, and there are only three durable ways to do that: move feature updates onto Windows Update client policies, adopt on-premises UUP with WSUS or Configuration Manager on 22H2 or later, or leave Dynamic Update enabled on media-based upgrades. Microsoft's own framing of the first option is unambiguous — optional content migration "just works" on a UUP-connected service. Everything else in this post is a workaround for not having done one of those three.
Proof it worked: a real read-only capability census
Every number below was read off one live Windows 11 Enterprise 25H2 device, build 26200.9168, servicing stack 10.0.26100.9156, without changing anything on it.
| Measurement | Result | What it proves |
|---|---|---|
Get-WindowsCapability -Online total | 425 capabilities | The catalogue of what a device could have is large, and enumerating it is cheap and read-only. |
| State breakdown | 369 NotPresent / 56 Installed | NotPresent is the normal state for 87% of capabilities. A screenshot full of it is not a broken device. |
| RSAT capabilities installed | 4 of 21 (ServerManager, ActiveDirectory.DS-LDS, Dns, DHCP) | The documented dependency chain is visible in live data: the AD DS-LDS tools are present and so is their parent, Rsat.ServerManager.Tools. |
| Language FoDs installed | Basic for en-AU, en-GB, en-US; OCR for en-GB and en-US; Handwriting, Speech and TextToSpeech for en-US only | Coverage is per feature, not per locale. Monitoring at locale granularity would have missed three of these. |
WMIC~~~~ | Installed on a 25H2 device | Documented as not preinstalled from 24H2. Its presence, plus the Setup log staging ...WMIC-FoD-en-US and -en-GB, is the satellite model and content migration both working. |
| Setup log Event 2 FoD packages | ...TerminalServices-AppServer-Client-FOD-en-US, ...WMIC-FoD-en-US, ...WMIC-FoD-en-GB, ...WinOcr-FOD-en-US | Staging succeeded, so a source was reachable. On a device that lost features, these entries are what is missing. |
Servicing.admx | 1,321 bytes, one element: LocalSourcePath | Confirms the 24H2 policy change first-hand — the two Windows Update options really are gone from the shipped template. |
| Repair-source policy key | Key exists, contains only CountryCode | No alternate source configured, and proof the Test-Path trap is real: key presence is not policy configuration. |
| DISM providers | CbsProvider.dll 1,304,040 bytes; IntlProvider.dll 357,864 bytes | Capability work and language work run through different providers behind the same Dism.exe. |
The pattern to take away is small. Optional content is a fetch, not a file. An upgrade that loses it did not break anything — it failed to find a source, quietly, at the one moment it needed one. Capture the census before the ring runs, diff it after, verify every re-add against state rather than exit code, and this stops being a ticket wave and becomes a report you read on Tuesday morning.
References
- Migrating and acquiring optional Windows content - the core document: the in-place upgrade mechanism, "failure to migrate optional content during update", latent acquisition, and all seven acquisition options.
- Features on Demand - satellite versus non-satellite FoDs, the repository requirement, and the source resolution order.
- FoD and language packs for WSUS and Configuration Manager - the 1709 and 1803 WSUS changes, the 22H2 UUP reversal, the 24H2 policy-option removal, and the 2004-to-21H2 policy conflict.
- Add-WindowsCapability (Dism) - the "fails silently; no exceptions are thrown" behaviour and the WSUS note.
- Get-WindowsCapability (Dism) - the read-only census cmdlet and its parameters.
- Language and region Features on Demand - the six language FoD types, their capability names, and the dependency order.
- Available Features on Demand - every non-language capability name including the RSAT set with dependencies, the WMIC note, and the Reserved Storage note.
- Policy CSP - ADMX_Servicing - the registry key name, the ADMX file name, and the alternate-source-path semantics.
- Enable or Disable Windows Features Using DISM - the client-versus-Server payload retention rule.
- .NET Framework 3.5 installation errors - the metadata-versus-payload explanation and the meanings of
0x800F081F,0x800F0906and0x800F0907. - DISM Capabilities Package Servicing Command-Line Options - the capability switch syntax.
- Install-Language (LanguagePackManagement) -
-CopyToSettings,-ExcludeFeaturesand-AsJob. - Get-InstalledLanguage (LanguagePackManagement) - the read-only language inventory cmdlet.
- Get-WindowsPackage (Dism) - the package-level inventory.