I hit this the way most people will hit it: not by reading a deprecation notice, but by watching something break.
I had a small utility script, one of those things that has been sitting in a repo for years quietly doing its job, and it needed the machine’s BIOS serial number. It got that number the way everybody got that number in 2012, with wmic bios get serialnumber. On a freshly imaged Windows 11 box it came back with the least helpful error in the Windows catalogue:
'wmic' is not recognized as an internal or external command,
operable program or batch file.
My first instinct was the reflex every admin has developed over the last two years, which is to add the Feature on Demand back and move on:
DISM /Online /Add-Capability /CapabilityName:WMIC~~~~
That works today. It stops working shortly. Microsoft’s release notes for the August 2026 servicing update remove the escape hatch along with the tool, and once that lands on your machines the reinstall command has nothing left to reinstall.
So I went looking for the rest of the calls instead, which is where this stopped being a five minute fix. A grep found plenty. What the grep would not tell me was which of those hits were real work. Some were a two second swap. At least one was a for /f block where swapping the command would not have been enough. Those are not the same job, and no tool I could find would tell them apart.
This post covers what the replacements are, which parts of the migration are mechanical, which parts will bite you if you treat them as mechanical, and the scanner I ended up writing to tell those apart.
Ten years of notice
WMIC has been on death row for a decade, which is why nobody moved. The full timeline, as Microsoft documents it in KB5067470:
| Year | What changed |
|---|---|
| 2016 | Deprecated in Windows Server 2012 |
| 2021 | Deprecated in Windows 10 (see note below) |
| 2022 | Converted to a Feature on Demand in Windows 11 22H2, still preinstalled and enabled |
| 2024 | Disabled by default in Windows 11 23H2 and 24H2, still installable as a FoD |
| 2025 | Removed on upgrade to 25H2, but you could add it back as a FoD |
| 2026 | Removed completely, no longer available as a FoD |
A small inconsistency, if your sources disagree: Microsoft’s own KB puts the Windows 10 deprecation at version 21H2, while the Win32 SDK reference page and the deprecated features list both say 21H1. It changes nothing practical, but it will make you doubt your notes.
The 2026 step changes your job. Starting with Release Preview builds 26100.9267 for 24H2 and 26200.9267 for 25H2, Windows 11 no longer includes WMIC, and it is no longer available as a Feature on Demand. It arrives through normal servicing on machines you already have. The decision about when you deal with this is not entirely yours.
The scope, because the headlines have been sloppy: WMI itself is not going anywhere. Only the wmic.exe command-line wrapper is being removed. The WMI service, the classes, the providers, and every programmatic interface remain part of Windows. Nothing you can query today becomes unqueryable. You just need a different front door.
The translation table
Most WMIC usage in the wild is a handful of one-liners copied off a forum post. Those map cleanly onto Get-CimInstance:
| WMIC | PowerShell |
|---|---|
wmic bios get serialnumber | (Get-CimInstance Win32_BIOS).SerialNumber |
wmic computersystem get model | (Get-CimInstance Win32_ComputerSystem).Model |
wmic csproduct get uuid | (Get-CimInstance Win32_ComputerSystemProduct).UUID |
wmic os get caption,version | Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version |
wmic logicaldisk get deviceid,freespace | Get-CimInstance Win32_LogicalDisk | Select-Object DeviceID, FreeSpace |
wmic qfe list | Get-HotFix |
wmic process get name,processid | Get-CimInstance Win32_Process | Select-Object Name, ProcessId |
wmic process call create "notepad.exe" | Invoke-CimMethod Win32_Process -MethodName Create -Arguments @{CommandLine='notepad.exe'} |
wmic path win32_service where "state='Running'" get name | Get-CimInstance Win32_Service -Filter "State='Running'" | Select-Object Name |
The class names are identical, which is the good news. A lot of the migration is mechanical: strip the WMIC verb grammar, keep the Win32_ class, move the where clause into -Filter, and select the properties you wanted.
If you would rather keep your WQL, Get-CimInstance takes a raw query:
| |
Finding the class behind the alias
The table above covers the common cases. For anything else you need to know which class an alias was hitting, and the aliases are not always obvious. wmic csproduct is Win32_ComputerSystemProduct. wmic nicconfig is Win32_NetworkAdapterConfiguration. wmic qfe is Win32_QuickFixEngineering. wmic rdtoggle is Win32_TerminalServiceSetting, which nobody guesses.
To find classes by name:
To see what a class exposes:
| |
WMIC aliases frequently exposed a curated subset of the underlying class, so a class usually carries more than its alias ever showed. If a script’s output looked thin, the alias was the limit, and you may find the property you always wanted was there the whole time.
Where a swap stops being enough
Migrations go wrong here. WMIC emitted formatted text. PowerShell emits objects. If a human read the output, swapping the command is enough. If a script parsed that text, swapping the command leaves the parser behind, still reading a layout that no longer exists.
The classic offender is a batch file doing this:
for /f "skip=1 tokens=2 delims==" %%a in ('wmic os get caption /value') do set OSNAME=%%a
That is not a query, it is a text scraper. It is compensating for WMIC’s specific key=value layout, its header row, and the trailing carriage returns WMIC left on every line. Point it at PowerShell output and none of those assumptions hold. Rewrite the whole block rather than the command inside it:
for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command ^
"(Get-CimInstance Win32_OperatingSystem).Caption"`) do set OSNAME=%%a
I always pass -NoProfile. It skips the user profile, which makes the call faster and deterministic on machines where somebody has customised their shell.
More differences that catch people:
Dates change shape. WMIC returned raw DMTF datetime strings, the 20260818103000.000000-420 format, and every script that touched a timestamp had string surgery wrapped around it. Get-CimInstance hands you a real DateTime object instead. That is better, but your substring extraction is now operating on something like 08/18/2026 10:30:00 and will produce nonsense. Uptime calculations are the usual casualty:
| |
Array properties are real arrays. WMIC flattened multi-valued properties into a brace-wrapped string, so wmic nicconfig get ipaddress gave you something like {"192.168.1.5","fe80::1"} and scripts split on quotes and commas. Get-CimInstance returns an actual array. Index it or join it, do not parse it.
Empty results are empty. When a query matched nothing, WMIC printed No Instance(s) Available. to the console. That is text, and plenty of scripts checked for it. Get-CimInstance returns nothing at all, so the check to write is a straight emptiness test on the result.
Methods return an object. wmic process call create printed a return value block you could read or scrape. Invoke-CimMethod gives you an object with a ReturnValue property, and zero means success:
Remote queries
wmic /node:SERVER01 becomes a CIM session, one of the few places where the migration buys you something:
The old WMI path used DCOM, which needs port 135, port 445, and a range of dynamically assigned ports open on the target. CIM sessions use WS-Man over WinRM, the same single port as PowerShell Remoting. If you have a target that only speaks DCOM, New-CimSessionOption -Protocol DCOM gets you back there, but treat that as the documented exception.
Sessions are reusable, so a script that queries five classes from one host should open one session and use it five times. Restrict the properties you pull as well:
By default a WMI query drags back every property whether you use it or not. Locally that is invisible. Across a hundred machines it is not.
/node: sweeps very often carry /user: and /password: on the same command line, which means a credential is sitting in a checked-in script and in the command history of every machine that ran it. Migrating the call is a good moment to fix that too. Track it as its own piece of work, because moving the query to a CIM session does not by itself remove the password.
Traps in the translation
Do not carry wmic product forward as Win32_Product. It is the most common inventory one-liner and the worst class in WMI. Querying it triggers an MSI consistency check against every installed package, which is slow and can cause installers to reconfigure themselves mid-query. Translating it faithfully into Get-CimInstance Win32_Product carries the expense forward, because the class is what makes the query expensive. Read the uninstall keys instead:
Do not migrate to Get-WmiObject. Microsoft’s own migration guidance lists it as a supported alternative alongside Get-CimInstance, which is a strange thing to find in a document about retiring legacy tooling. The WMI cmdlets are themselves deprecated and are not available in PowerShell 6 and later. Rewriting a WMIC call as Get-WmiObject produces a script that works in Windows PowerShell 5.1 and fails the moment anyone runs it in PowerShell 7. Go straight to CIM.
-Filter speaks WQL. It produces confusing errors because the parameter looks like it should take a script block. It does not. Strings are single-quoted, comparison is = rather than -eq, and wildcards are LIKE '%pattern%':
Filtering server-side with -Filter is meaningfully faster than pulling everything and piping to Where-Object, especially remotely.
Is there a tool that does this for me
Short version: nothing rewrites your code, and there is a good reason for that.
For detection there are two existing options. PSScriptAnalyzer ships a rule called AvoidUsingWMICmdlet, the only first-party choice, and it has a gap: it flags the five deprecated cmdlets and never flags wmic.exe. Its scope is the PowerShell cmdlets, so it will happily pass a script full of wmic bios get serialnumber. It also never opens a .bat file, which is where most of the remaining WMIC in the world lives.
The community option is Find-WmiUsage, a single PowerShell 7 function that matches fifteen patterns and does cover the gaps: wmic as an external command, the gwmi/iwmi/swmi/rwmi aliases, the System.Management .NET classes, and the SWbemLocator COM objects. Arbitrary extensions, CSV and JSON output. It is detection only.
Nobody has built the converter, and after spending a while thinking about how one would work, I am fairly sure nobody should. A mechanical rewriter would produce confident garbage. Aliases are not class names, and the alias-to-class mapping is a hand-maintained lookup table that does not exist completely anywhere public. The parsing wrapped around a call is usually harder to fix than the call, so a tool that repaired only the command would leave you with a script that runs and returns wrong values. And some translations are judgment calls: no transpiler is going to decide on its own that wmic product should become a registry read.
Detection is automatable. Translation needs a person who knows what the script was for.
Sorting by effort
Which leaves a gap in the middle, and the cost of this migration sits in it. Everyone already knows WMIC is deprecated. PSScriptAnalyzer answers the yes-or-no question for PowerShell files and a grep answers it for everything else. Neither answers the question a migration gets scheduled against: of these four hundred hits, which one is a two second swap and which one is a day?
I wrote a small module that answers that. WmicTriage is three functions:
| Function | What it does |
|---|---|
Invoke-WmicScan | Scans files and returns one classified finding per call site |
Export-WmicScanReport | Writes those findings as CSV or SARIF |
Get-WmicRule | Lists the rules, so a classification can be argued with |
There is nothing to install. It has no RequiredModules, makes no WMI calls, and is not Windows-only, because it is text and XML parsing over files on disk. Scanning a deployment share from a Linux build agent works fine.
Tiers, ordered by effort
The output classifies every call site it finds. The tiers run in increasing order of how much work the replacement is, so they rank effort where a linter would rank severity:
| Tier | Meaning | Example |
|---|---|---|
| Mechanical | Swap the command, done | wmic bios get serialnumber on its own line |
| Wrapped | Output is captured and parsed, so the wrapper must be rewritten too | for /f ... in ('wmic ...') |
| Semantic | The correct translation is a judgment call | wmic product, anything touching a datetime or multi-value property |
| Environmental | PowerShell may not exist at the call site | WMIC inside startnet.cmd or a WinPE task sequence step |
A block that survives its own fix
No other tool detects this case, and it is where a migration does damage a clean failure would have prevented. A real line out of the example deployment share that ships with the module:
for /f "skip=2 tokens=1,2 delims=," %%A in ('wmic csproduct get vendor^,name /format:csv') do (
set VENDOR=%%A
set MODEL=%%B
)
Swap the command for Get-CimInstance and nothing breaks. The for /f keeps running. It keeps setting VENDOR and MODEL from whatever the token positions now land on, and the CSV that reaches the CMDB is wrong for a quarter before anybody notices.
The skip=2 tokens=1,2 delims=, spec is what has to be rewritten. That spec is not always on the same line as the call, which is why a Wrapped finding spans the whole block and records the for options as their own field:
Wrapped is not only for /f, either. Output redirected to a file, /output: and /append:, a pipe into find or findstr, assignment into a PowerShell variable, and .Exec() in VBScript all mean the same thing: something downstream is reading the WMIC text layout, and that reader has to change when the command does.
When several rules match one call
The finding takes the hardest tier, and every rule that fired is kept in a RuleIds field so nothing is lost. This produces one result that looks wrong until you think about it. A mechanical one-liner sitting in a WinPE script comes back as Environmental:
rem Straight to the console for the technician standing there
wmic computersystem get model
The substitution is trivial. Whether the boot image has PowerShell in it at all is a much larger question, and it has to be answered before the trivial part means anything. Ranking by effort puts the tier where it belongs.
Each file type gets the reader it deserves
.bat and .cmd lead, because that is the gap PSScriptAnalyzer leaves. Then .ps1, .psm1, .vbs, .vbe, .wsf, .js, .py, and .xml. Each gets its own reader:
- Batch joins
^continuations, tracks comment state, and balances parentheses across lines to find where a block ends. - PowerShell uses the real parser. Capture detection is exact, because
$x = wmic,(wmic ...),wmic ... |andwmic ... >are four different syntactic positions that a regex has to guess at and the syntax tree simply knows. - Task sequence XML uses an XML parser, so a finding names the step. Line numbers in an exported task sequence point at something nobody will ever edit. The
runInattribute, or the enclosing group names, decides whether that step is WinPE.
A table of per-language knowledge covers what a comment looks like and what quotes a string. That table needed more care than I expected. An apostrophe delimits a string in PowerShell, JScript and Python, and is just an apostrophe in batch, where treating it as a quote runs every echoed “don’t” into the command on the following line. The result is a command in the report with half the host language stapled to the end of it.
Six of twenty-nine
The module ships with a fake deployment share to point it at: a collector carried forward from an XP rollout, a vendor file nobody is allowed to edit, a WinPE startup script, a task sequence export, and a migration somebody started and abandoned halfway.
| |
Nine files. Twenty-nine deprecation findings plus one security finding, breaking down as 6 Mechanical, 4 Wrapped, 15 Semantic, 4 Environmental.
Six of twenty-nine are the simple swap this job is usually assumed to be. That ratio is why I built the tool. If you scope this migration off a grep count, you will scope it as twenty-nine swaps and deliver six.
Look at the half-migrated file in that example, because it is the state most estates are in:
| |
Somebody did the easy calls and stopped. Both survivors are Wrapped. They look like leftovers. They are the expensive half of the file.
Reports and CI gating
The two output formats exist because two different people read them and want opposite things:
The SARIF carries a partialFingerprints entry built from path, rule and command, with the line number deliberately left out. Add a comment at the top of a batch file and every call below it shifts down. A fingerprint that included the line would close two hundred findings and open two hundred identical ones, and the second report anyone read would be noise.
For CI, exit non-zero on Mechanical and Wrapped, where a machine can judge the work. Zero with a warning on Semantic and Environmental, because both end in a judgment call, and a build that fails on a judgment call is a build people learn to bypass.
The module never calls exit itself. A library that terminates its host is a library you cannot call from anything else, so the verdict rides on the findings instead:
Security findings are the one deliberate disagreement with that rule. A checked-in password is raised as a SARIF error, because it is one by any reading, but it never fails the build, since this gate is about WMIC deprecation and letting one problem close the other helps nobody. They are also raised as separate findings, never folded into the tier, so a line carrying both /node: and /password: appears twice.
Suggestions are advisory, always
Every finding carries a SuggestedReplacement, and nothing in the module will ever apply one. Advisory is $true on every finding in every format.
That restraint is what makes printing them defensible at all. On the Wrapped tier the suggestion reads “restructure this block” and never offers a command to paste, because a one-line replacement for a block would be a lie. On the Semantic tier the tier definition already concedes that no tool can be right.
What it deliberately does not do
It does not resolve variables. A batch file that does this:
set WMICEXE=%SystemRoot%\System32\wbem\wmic.exe
"%WMICEXE%" computersystem get domain,totalphysicalmemory >> %OUTFILE%
gets a Semantic finding on the set line and nothing at all on the call. Chasing call sites is an AST problem in PowerShell and unsolvable in batch, and it would have eaten the whole project. The finding therefore says in as many words that the count is short until somebody greps for that variable by hand. It says so in the report, which is where anyone will read it.
It does not score confidence. Four named tiers that people can filter on beat a 0-100 number nobody calibrates.
It does not skip comments by default. A WMIC command in a rem line is usually documentation that will mislead someone later, and it is the example the next person copies. It never fails a build, because it does not run, and -ExcludeComment drops it entirely if you disagree.
It does not have a -Recurse switch. Directories are walked in full, always. An inventory that covered only the top level of a deployment share is worse than no inventory, because it will be believed.
The rules are data
Every detection is one entry in Data\WmicRules.psd1. The engine holds no WMIC knowledge of its own, so a rule that appears in that file is live with no code change:
Every key in Match must hold, and within one key any listed value will do, so it is an AND of ORs. An empty Match matches everything, which gives the baseline Mechanical rule its behaviour.
The loader validates hard, and that is deliberate. A rule with a typo in a Match key would never fire, and the report would come back short. A short report is the one failure mode a tool like this cannot have, so an unknown match key, an unknown tier, a duplicate id, and a reference to a property group that does not exist are all errors at load time.
Two other data files take edits well. Data\WmicAliases.psd1 maps aliases to classes, with a NonObvious flag on the ones a reader will not guess, which is how the report can tell you on the spot that qfe means Win32_QuickFixEngineering. Data\WmicProperties.psd1 holds the property groups behind the Semantic tier: DateTime, Interval, MultiValue, Boolean. Adding a property there is the cheapest way to make the scanner smarter, because those groups are what let it notice that a particular call is going to change shape as well as syntax.
And if you want to argue with a classification, the tool will tell you why it made it:
| |
Supporting Windows PowerShell 5.1
The manifest allows 5.1 as well as PowerShell 7, against my usual habit for anything new. The reasoning is specific to this problem: the estates that still run WMIC are the same estates whose jump boxes and build servers never got pwsh. A migration tool that cannot run where the migration is happening is not much use.
Finding your callers
Whether or not you use any of the above, the inventory costs more than the rewriting. WMIC calls hide in logon scripts, imaging task sequences, vendor installers, monitoring agents, and Node modules. A recursive grep is the cheap first pass and will tell you whether you have a problem at all:
That catches your own code. It does not catch third-party dependencies, and those are real: the widely used pidtree npm package shells out to wmic and fails on machines without it, which means anything depending on it inherits the problem. The fastest way to surface that category is to stop reading code and start breaking things on purpose:
DISM /Online /Remove-Capability /CapabilityName:WMIC~~~~
Do that on a test box now, while the capability can still be added back, then run your builds, your logon scripts, your imaging sequence, and your monitoring agent against it. You get a free preview of every failure before it arrives on its own schedule.
When PowerShell is not on the table
“Just use PowerShell” fails in a couple of places.
WinPE and WinRE come first. PowerShell is not present in either by default. It is available as an optional component you add through the Windows ADK when building a custom image, and the components have to be added in dependency order. If your deployment sequence used WMIC inside the preinstall environment, budget real time for it, because it is an image engineering task, and image engineering moves at its own pace. Environmental exists as a separate tier for this: the line itself might be a five second fix, but you cannot start it until somebody has answered a question about the boot image, and frequently nobody remembers what went into that boot image.
The second place is anywhere invoking a shell is awkward or the caller is a fixed-format batch context. Several WMIC uses have plain command-line equivalents that have been there all along:
| Need | Command |
|---|---|
| System summary | systeminfo |
| Running processes | tasklist |
| Kill a process | taskkill /PID 1234 /F |
| Service state and control | sc query, sc config |
| Installed updates | dism /online /get-packages |
| Event logs | wevtutil |
| Disk and volume info | diskpart, fsutil |
These are not object-oriented and they are not pleasant to parse, but they are not being removed either.
Scoping the work
This migration is wide, shallow, and unevenly distributed. The commands map close to one for one, the class names do not change, and nothing you could query before becomes unavailable. What makes it painful is that WMIC calls accumulated over twenty years in places nobody owns anymore, and that the hits are not interchangeable. Most of them are nothing. A few are the expensive kind, and a count cannot tell you which.
The removal arrives through servicing, so the window where you can test on your own terms is open right now and closes on Microsoft’s schedule. Pulling the capability off one machine and running everything you own against it costs an afternoon. Finding out from a ticket costs more.
