How the PowerShell Pipeline Passes Objects
PowerShell’s pipeline hands live .NET objects from one cmdlet to the next, one object at a time, and understanding exactly how that binding happens is what separates scripts that work by luck from scripts that work by design.
| operator in PowerShell hands a live object to the next cmdlet, not a line of text, and each cmdlet’s process block runs once per object as it arrives, not once for the whole batch.
Every pipeline stage sends full .NET objects downstream, complete with properties and methods.
The two ways a parameter can accept piped input, and the source of almost every binding surprise.
The automatic variable holding the current object inside a ForEach-Object or Where-Object script block.
What Is the Pipeline?
A PowerShell pipeline is a series of commands connected with the | operator, where the output of one command becomes the input of the next. Command-1 | Command-2 | Command-3 runs left to right, and PowerShell processes the whole chain as a single operation rather than three separate steps with temporary files in between.
Think of it less like a Unix pipe and more like a conveyor belt in a warehouse. A POSIX shell pipe passes a fax: a flat sheet of text that the next program has to read and re-parse to figure out what it means. PowerShell’s pipeline passes the actual labelled crate, still packed, with every compartment (property) and every tool bolted to it (method) intact. Get-Process notepad | Stop-Process works with no -Name or -Id parameter anywhere in sight, because the crate that arrives at Stop-Process already is the process.
Get-Process creates is the exact same object Stop-Process receives.
Why Objects, Not Text?
Text pipelines force every receiving program to invent its own parsing rules: column widths, delimiters, quoting, locale-dependent date formats. ps aux | awk only works if the columns line up the way awk expects this week. An object pipeline skips that entire layer, because the receiving cmdlet asks the object for a named property instead of guessing where it sits in a line of text.
A cmdlet reads $_.Name directly instead of splitting a formatted string.
A [datetime] property stays a [datetime] all the way down the chain, not a locale-formatted string.
The object keeps its methods, so $_.Stop() or $_.ToUpper() is available downstream without re-fetching anything.
This is also why formatting cmdlets go last. Format-Table and Format-List convert live objects into formatted output objects meant for display, and piping their output into anything else almost always breaks, because there is nothing structured left to bind to.
Why This Matters in Production Scripts
Most operational PowerShell is a chain of three or four cmdlets: get something, filter it, transform it, act on it. If the binding between any two links in that chain is wrong, the script does not usually crash. It just quietly processes zero objects, or the wrong parameter, and reports success. That failure mode is worse than an error, because nothing in the console tells you it happened.
A script that pulls domain controllers, checks a service, and remediates the unhealthy ones — the same shape as the checks in domain controller baseline monitoring or an Active Directory forest health check — depends on every stage of the pipeline binding to the parameter you actually intended. Get that wrong on the remediation step and the script can report “0 objects processed” while looking, at a glance, like it ran cleanly.
Get-Member or Trace-Command.
How Parameter Binding Actually Works
When you pipe an object to a cmdlet, PowerShell’s parameter binding component decides which parameter receives it. A parameter is only a candidate if it meets three conditions: it accepts pipeline input, it accepts the type of object being sent (or a type that can be converted to it), and it was not already supplied on the command line.
There are exactly two ways a parameter can accept pipeline input, and every binding mystery in PowerShell traces back to one of them:
| Mode | How It Binds | Failure Mode |
|---|---|---|
| ByValue | The parameter accepts the whole piped object if its .NET type matches, or can be converted to, the parameter’s declared type. | An object of the wrong type is silently skipped for that parameter, unless another parameter can take it. |
| ByPropertyName | The parameter accepts a value only when the piped object has a property whose name (or alias) matches the parameter name. | A property name that is close but not exact — ComputerName vs Name — never binds, with no warning. |
Start-Service is the textbook example. Its Name parameter accepts pipeline input both ByValue and ByPropertyName, so it can take a bare string or an object with a Name property. Its InputObject parameter accepts only ByValue, and only for actual ServiceController objects. Piping a custom object with a Name property works; piping one with a ServiceName property does not, because ServiceName is not the parameter’s name or a declared alias.
There is a second, easy-to-miss consequence of piping versus passing a parameter directly. When you pipe a collection, PowerShell enumerates it and sends each item through the pipeline one at a time — the receiving cmdlet’s process block runs once per item. When you pass the same collection through a parameter like -InputObject, it arrives as a single array object instead, and a cmdlet’s process block only fires once for that array as a whole. Hashtables are a further exception: PowerShell does not automatically enumerate a hashtable’s key-value pairs down the pipeline the way it enumerates an array, so @{One=1;Two=2} | Measure-Object reports a count of 1, not 2.
If you are writing the receiving function yourself, this is exactly what ValueFromPipeline and ValueFromPipelineByPropertyName declare on a parameter, and why an advanced function needs a process block to handle more than the first piped object. The advanced functions guide covers how to declare both attributes together so a parameter accepts input either way.
Inspecting What’s Actually Flowing
You rarely need to guess. PowerShell exposes the exact type and binding rules for anything in the pipeline.
Get-Help <cmdlet> -Parameter * (or -Full) prints every parameter’s Accept pipeline input? line, showing True (ByValue), True (ByPropertyName), True (ByPropertyName, ByValue), or False. That single line answers “will this even bind?” before you run anything.
Get-Member answers the other half: what type is this object, and what properties and methods does it actually have. Piping a collection to Get-Member shows the members of each individual object; passing the same collection through -InputObject shows the members of the array instead — a small but reliable way to confirm you are piping, not parameterising.
Get-TypeData goes one layer deeper: it shows extended type data added by Types.ps1xml files or Update-TypeData — the script properties and alias properties PowerShell bolts onto a type after the fact, such as the Name alias on process objects.
Trace-Command -Name ParameterBinding -PSHost -Expression { <pipeline> } prints exactly which parameter PowerShell tried to bind each object to, and why it was rejected.
Worked Examples
The following block walks the same ideas as commands: what accepts pipeline input, how ByValue and ByPropertyName differ in practice, and how to shape an object before it hits the next stage.
# See which parameters of a cmdlet accept pipeline input, and how
Get-Help Start-Service -Parameter *
# Inspect the live .NET type and members flowing through the pipeline
Get-Process | Get-Member
# ByValue binding: Stop-Process takes the whole process object directly
Get-Process notepad | Stop-Process
# ByPropertyName binding: a custom object with a Name property also works
[pscustomobject]@{ Name = 'Spooler' } | Start-Service -WhatIf
# Piping the same array through -InputObject treats it as one object,
# not one object per item - compare the two outputs
Get-Member -InputObject (Get-Process)
# A hashtable is not enumerated automatically down the pipeline
@{ One = 1; Two = 2 } | Measure-Object
# Trace exactly which parameter a piped object bound to
Trace-Command -Name ParameterBinding -PSHost -Expression {
Get-Service wmi | Start-Service
}
# Shape output for the next stage with Select-Object, including
# a calculated property built from the current object ($_)
Get-Process |
Select-Object -Property ProcessName, Id,
@{ Name = 'WorkingSetMB'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } } |
Sort-Object -Property WorkingSetMB -Descending |
Select-Object -First 5
# Build a clean report row from scratch with [pscustomobject]
Get-Service | ForEach-Object {
[pscustomobject]@{
ServiceName = $_.Name
Status = $_.Status
CanStop = $_.CanStop
}
}
Select-Object calculated property above uses a hashtable with Name/Expression keys (Label/Expression also work). Inside the script block, $_ refers to the current object, exactly as it does inside ForEach-Object and Where-Object.
[pscustomobject] is the standard way to build a clean object to hand downstream, rather than reusing whatever shape the source cmdlet happened to produce. Casting a literal hashtable — not a variable holding one — to [pscustomobject] preserves key order, which matters when the object is about to be exported to CSV or displayed in a table.
Pipeline Binding Cheat Sheet
| Accept Pipeline Input? | Meaning | Practical Note |
|---|---|---|
True (ByValue) | Binds the whole object if its type matches or converts. | Check the object’s type with Get-Member first. |
True (ByPropertyName) | Binds a value only if the object has a matching property name or alias. | Rename with Select-Object if the source property name differs. |
True (ByPropertyName, ByValue) | Tries ByValue first, falls back to ByPropertyName. | The most forgiving pattern; use it on your own advanced functions. |
False | The parameter ignores anything piped to it. | Must be supplied explicitly on the command line. |
$_ / $PSItem | Holds the current object inside a script block. | Identical variables; $PSItem reads better in nested blocks where $_ could be ambiguous. |
Troubleshooting Cheat Sheet
| Symptom | Likely Cause | Fix |
|---|---|---|
| “The input object cannot be bound to any parameters” | No parameter on the receiving cmdlet accepts pipeline input for that object’s type or property set. | Run Get-Help <cmdlet> -Full, find a parameter with a matching Accept pipeline input? value, and either pipe a compatible object or supply the parameter explicitly. |
| Piped values are silently ignored; a default is used instead | Binding succeeded against the wrong parameter, or failed to bind at all with no error. | Confirm the real binding with Trace-Command -Name ParameterBinding -PSHost -Expression { ... } before assuming the pipeline worked. |
| A custom function only acts on the first pipeline object | The function is missing a process block, so the body only runs once, in the implicit end block. |
Add [Parameter(ValueFromPipeline)] to the parameter and move the per-item logic into an explicit process { } block. |
ValueFromPipelineByPropertyName parameter never receives a value |
The piped object’s property name does not exactly match the parameter name or one of its aliases. | Pipe through Select-Object @{Name='TargetParam';Expression={$_.SourceProp}} to rename the property first, or add an [Alias()] to the parameter. |
A hashtable piped to Measure-Object reports a count of 1 |
PowerShell does not automatically enumerate hashtables (or IDictionary types) down the pipeline the way it enumerates arrays. |
Call .GetEnumerator() on the hashtable before piping it, or convert it to an array of key/value objects first. |
Get-Member -InputObject (Get-Process) shows array members, not process members |
Passing a collection through a parameter treats it as one object; only piping enumerates it item by item. | Pipe the collection instead of passing it through -InputObject when you want per-item behaviour. |
Output from Format-Table cannot be piped further |
Formatting cmdlets convert live objects into display-only formatting objects, discarding the original type. | Do the real work first, and put Format-Table or Format-List last in the pipeline, never in the middle. |
Final Thoughts
The pipeline is the one PowerShell feature every script leans on, and also the one most scripts get quietly wrong. The mechanism is not complicated once it is explicit: a parameter either accepts the whole object, or a named property of it, or neither, and PowerShell tells you which before you ever run a command.
Treat Get-Help -Parameter * and Get-Member as the first two commands you run against any cmdlet you have not piped into before, the same way you would check a service’s status before restarting it. The five minutes that costs is cheaper than a remediation script that reports success while quietly touching nothing.
Get-Help <cmdlet> -Parameter * for Accept pipeline input?, then confirm with Get-Member or Trace-Command -Name ParameterBinding — do not assume the binding worked just because nothing threw an error.
Next, we can look at what happens to these same live objects when they cross a PowerShell Remoting session: why properties survive but methods disappear once an object is deserialized on the other side of a New-PSSession.