PowerShell PSCustomObject: Build Custom Output Objects

A function that returns a formatted string is nearly useless in a pipeline — you can’t sort it, filter it, or export it to CSV. A function that returns a PowerShell PSCustomObject works with everything: Sort-Object, Where-Object, Export-Csv, Format-Table, and every other downstream cmdlet in the PowerShell ecosystem. Learning to build custom objects is the single upgrade that makes the most difference to the quality and reusability of your PowerShell functions.
Create a PSCustomObject
The modern syntax for creating a custom object uses the [PSCustomObject] type accelerator with a hashtable. This approach is concise, readable, and available in PowerShell 3.0+.
$server = [PSCustomObject]@{
Name = "server01"
OS = "Windows Server 2022"
IPAddress = "10.0.0.5"
Online = $true
}
$server
Name OS IPAddress Online
---- -- --------- ------
server01 Windows Server 2022 10.0.0.5 True
Access individual properties with dot notation: $server.Name, $server.Online.
Add Properties with [PSCustomObject] Cast
The cast syntax ensures property order is preserved. Properties appear in the output in the order you define them, which matters for Format-Table and Export-Csv column ordering.
$diskInfo = [PSCustomObject]@{
Drive = "C:"
SizeGB = [math]::Round((Get-PSDrive C | Select-Object -Expand Used) / 1GB +
(Get-PSDrive C | Select-Object -Expand Free) / 1GB, 1)
FreeGB = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
FreePct = [math]::Round((Get-PSDrive C).Free / ((Get-PSDrive C).Used + (Get-PSDrive C).Free) * 100, 1)
}
$diskInfo | Format-Table
PSCustomObject vs Hashtable Output
A hashtable looks similar to a PSCustomObject when printed but behaves differently in the pipeline. Hashtables don’t display as table rows — they display as key-value pairs. PSCustomObjects display as table rows with column headers, format properly, and integrate with all pipeline cmdlets.
# Hashtable output (harder to read, doesn't export to CSV nicely)
$ht = @{ Name = "server01"; Status = "Online" }
$ht # Displays as Name/Value list
# PSCustomObject output (clean table, full pipeline compatibility)
$obj = [PSCustomObject]@{ Name = "server01"; Status = "Online" }
$obj # Displays as row with column headers
# PSCustomObject exports to CSV correctly
$obj | Export-Csv -Path "C:\Logs\status.csv" -NoTypeInformation
Add Calculated Properties
Include computed values alongside raw data by including expressions directly in the object definition. This is cleaner than modifying the object after creation.
$proc = Get-Process | Sort-Object CPU -Descending | Select-Object -First 1
$report = [PSCustomObject]@{
ProcessName = $proc.Name
PID = $proc.Id
CPUSec = [math]::Round($proc.CPU, 2)
MemoryMB = [math]::Round($proc.WorkingSet / 1MB, 1)
RunTimeMin = [math]::Round(((Get-Date) - $proc.StartTime).TotalMinutes, 0)
ThreadCount = $proc.Threads.Count
}
$report
Build an Array of PSCustomObjects
The most common pattern is building a collection of custom objects from a loop, then working with the collection as a whole. Use foreach and assign the result to capture all iterations.
$servers = @("server01", "server02", "server03")
$inventory = foreach ($server in $servers) {
$os = Get-CimInstance -ComputerName $server -ClassName Win32_OperatingSystem
$bios = Get-CimInstance -ComputerName $server -ClassName Win32_BIOS
[PSCustomObject]@{
Computer = $server
OS = $os.Caption
Serial = $bios.SerialNumber
LastBoot = $os.LastBootUpTime
}
}
$inventory | Format-Table -AutoSize
Export PSCustomObjects to CSV
PSCustomObjects export to CSV perfectly — each object becomes a row, each property becomes a column. This is the primary reason to use them in reporting scripts.
$report = foreach ($svc in Get-Service) {
[PSCustomObject]@{
Name = $svc.Name
Status = $svc.Status
StartType = $svc.StartType
}
}
$report | Export-Csv -Path "C:\Logs\services.csv" -NoTypeInformation
Write-Output "Exported $($report.Count) services."
Common Errors and Fixes
- Property order not preserved without [ordered]: In PowerShell 2.0, plain hashtables don’t preserve insertion order. In PowerShell 3.0+ with the
[PSCustomObject]cast syntax, order is preserved. If you’re building the object from a plain@{}hashtable and seeing columns in the wrong order, use an ordered hashtable:[ordered]@{ First = 1; Second = 2 }before casting. - Adding properties after creation requires Add-Member: Once a PSCustomObject is created, you cannot simply assign a new property with dot notation. Use
Add-Member:$obj | Add-Member -NotePropertyName "NewProp" -NotePropertyValue "value". For objects built in a loop, include all properties at creation time rather than adding them afterward.
Related Cmdlets / See Also
Wrapping Up
[PSCustomObject] is the building block of good PowerShell output — structured, filterable, sortable, and CSV-exportable without any extra work. As a next step, find a function in your scripts that currently returns a formatted string or a hashtable and refactor it to return a PSCustomObject — the pipeline compatibility improvement alone makes it worthwhile.


