PowerShell · lesson 10 of 12
A System Inventory Report: Putting the Pipeline to Work
Build a real automation script from Get-CimInstance, a function with error handling, and JSON/CSV export — and watch the same nested-object flattening from the CSV lesson show up again, on data that actually matters this time.
By SkillByExample · Published
Every idea from this track lands in one script: an object pipeline, a function with error handling, and the two export formats that don’t treat structured data the same way.
Querying real system data
Get-CimInstance -ClassName Win32_OperatingSystem |
Select-Object Caption, Version, OSArchitecture, @{Name="FreeMemoryMB";Expression={[math]::Round($_.FreePhysicalMemory/1KB,0)}}
Caption Version OSArchitecture FreeMemoryMB
------- ------- -------------- ------------
Microsoft Windows 11 Home Single Language 10.0.26200 64-bit 2279.00
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,1)}}, @{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,1)}}
DeviceID SizeGB FreeGB
-------- ------ ------
C: 460.00 122.40
Get-CimInstance is the modern cmdlet for this — it talks the standard WS-Management
protocol rather than the older, Windows-only WMI plumbing behind Get-WmiObject.
-Filter "DriveType=3" narrows to fixed local disks at the query itself, before any data
comes back.
A function that assembles a report, with real error handling
function Get-SystemReport {
[CmdletBinding()]
param()
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
$disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction Stop |
Select-Object DeviceID, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,1)}}, @{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,1)}}
$topProcess = Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First 1 -Property ProcessName, @{Name="MemoryMB";Expression={[math]::Round($_.WorkingSet64/1MB,1)}}
[pscustomobject]@{
GeneratedAt = Get-Date
OSCaption = $os.Caption
FreeMemoryMB = [math]::Round($os.FreePhysicalMemory/1KB,0)
Disks = $disk
TopProcess = "$($topProcess.ProcessName) ($($topProcess.MemoryMB) MB)"
}
} catch {
Write-Error "Failed to build system report: $($_.Exception.Message)"
}
}
Get-SystemReport
GeneratedAt : 13-09-2026 17:02:33
OSCaption : Microsoft Windows 11 Home Single Language
FreeMemoryMB : 2225
Disks : @{DeviceID=C:; SizeGB=460; FreeGB=122.4}
TopProcess : node (882.9 MB)
-ErrorAction Stop on both CIM calls is what makes the try/catch here actually useful —
without it, per the error-handling lesson earlier in this track, a failed query would just
print a warning and let the function limp forward with a half-built report.
The same flattening problem, on data that matters
$report = Get-SystemReport
$report | ConvertTo-Json
{
"GeneratedAt": "2026-09-13T17:02:53.4884045+05:30",
"OSCaption": "Microsoft Windows 11 Home Single Language",
"FreeMemoryMB": 2209.0,
"Disks": {
"DeviceID": "C:",
"SizeGB": 460.0,
"FreeGB": 122.4
}
}
$report | Export-Csv -Path "$env:TEMP\sysreport.csv" -NoTypeInformation
Get-Content "$env:TEMP\sysreport.csv"
"GeneratedAt","OSCaption","FreeMemoryMB","Disks"
"13-09-2026 17:02:53","Microsoft Windows 11 Home Single Language","2209","@{DeviceID=C:; SizeGB=460; FreeGB=122.4}"
Same lesson as the CSV/JSON track earlier: JSON keeps Disks as a real nested object; CSV
renders it as a single flattened text blob. On a report headed for a dashboard or another
program, that difference decides which format to reach for — and it’s exactly why this script
exports both rather than picking one.
Confirming the error handling actually works
function Get-BadReport {
try {
Get-CimInstance -ClassName Win32_NotARealClass -ErrorAction Stop
} catch {
Write-Error "Failed to build system report: $($_.Exception.Message)"
}
}
Get-BadReport
Get-BadReport: Failed to build system report: Invalid class
A typo’d or nonexistent CIM class name is a real, easy-to-hit failure — confirming it produces a clear, one-line message instead of a raw stack trace is worth doing before this ever runs unattended on a schedule.
Practice
1. Build a lightweight sample function and take two snapshots a second apart.
function Get-QuickSample {
[pscustomobject]@{
Time = Get-Date
FreeMemoryMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory/1KB,0)
}
}
$samples = @()
$samples += Get-QuickSample
Start-Sleep -Seconds 1
$samples += Get-QuickSample
$samples | Export-Csv -Path "$env:TEMP\samples.csv" -NoTypeInformation
Import-Csv "$env:TEMP\samples.csv"
Time FreeMemoryMB
---- ------------
13-09-2026 17:03:08 4122
13-09-2026 17:03:09 4140
This is the shape a real monitoring script takes: append each run’s sample to the same file instead of overwriting it, and a single snapshot becomes a trend.
2. Trigger the bad-class error again and confirm it doesn't throw an unhandled exception.
Get-BadReport
"script kept running after the error"
Get-BadReport: Failed to build system report: Invalid class
script kept running after the error
The catch block turns a failure into a message and lets the calling code continue — a
production script would log that message somewhere durable instead of just printing it.
3. Add process count (grouped by name) to the report, reusing Group-Object from earlier in this track.
$topByCount = Get-Process | Group-Object ProcessName | Sort-Object Count -Descending | Select-Object -First 1
"$($topByCount.Name) has $($topByCount.Count) running instances"
svchost has 98 running instances
Every cmdlet in this final script — Get-CimInstance, Sort-Object, Group-Object,
Select-Object, Export-Csv, ConvertTo-Json — is one this track already covered on its
own; the only thing new here is combining them into something that answers a real question.
That closes the track. The thread through all ten lessons: PowerShell trusts the type of whatever came down the pipeline — which is what makes the object model so capable, and exactly what breaks the moment a value is a string, a null, or a nested structure pretending to be something flatter than it is.