Pipeline Input with begin, process, end, and clean
Design advanced functions that bind pipeline objects predictably, process records as a stream, and clean up lifecycle resources safely.
Learning objectives
- Declare pipeline input by value and by property name.
- Use Get-Help and Get-Member to diagnose pipeline binding.
- Place one-time setup, per-record work, and completion logic in begin/process/end.
- Use the PowerShell 7.3+ clean block for lifecycle cleanup and understand its output behavior.
- Preserve streaming rather than collecting the entire pipeline by default.
- Build one function that supports both explicit parameters and pipeline input.
1. Pipeline input means the function can receive objects one at a time
Chapter 03 explained pipeline binding from the caller side. Now you will design the receiving function. A pipeline-capable parameter declares how incoming objects may bind. The function’s process block normally runs once for each successfully bound pipeline input object.
function ConvertTo-TargetRecord {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$Name
)
process {
[pscustomobject]@{ Name=$Name.Trim().ToLowerInvariant() }
}
}
'API-01','WORKER-01' | ConvertTo-TargetRecordEach string arrives separately. This streaming model lets the function start producing results before the entire upstream collection exists.
2. ByValue and ByPropertyName are different binding strategies
ValueFromPipeline allows the incoming object itself to bind to a parameter when the type can match or convert. ValueFromPipelineByPropertyName allows an incoming object property to bind to a parameter with the same name or a supported alias.
function Get-TargetState {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Name
)
process {
[pscustomobject]@{ Name=$Name; State='Observed' }
}
}
# By value: the strings themselves bind to Name.
'api-01','worker-01' | Get-TargetState
# By property name: each object's Name property binds.
[pscustomobject]@{ Name='db-01'; Region='eu' } | Get-TargetStateDo not guess which rule will apply. Inspect the destination parameter with Get-Help and inspect source objects with Get-Member.
3. begin, process, and end divide one-time setup, per-item work, and one-time completion
function Measure-TargetName {
[CmdletBinding()]
param([Parameter(ValueFromPipeline)][string]$Name)
begin {
$count = 0
Write-Verbose 'Starting pipeline processing'
}
process {
$count++
[pscustomobject]@{ Name=$Name; Length=$Name.Length }
}
end {
Write-Verbose "Processed $count target(s)"
}
}begin runs once before pipeline records are processed. process handles each bound input object. end runs once after normal input processing. The count is implementation state; the emitted records are the reusable output contract.
4. clean is a PowerShell 7.3+ cleanup block for pipeline lifecycle resources
The clean block was added in PowerShell 7.3. Microsoft documents it as cleanup that runs when processing finishes normally, terminates with an error, is truncated by a downstream command such as Select-Object -First, or is stopped. It is conceptually similar to a finally around the function lifecycle.
function Read-LabText {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Path)
begin {
$reader = [System.IO.StreamReader]::new($Path)
}
process {
while (-not $reader.EndOfStream) {
$reader.ReadLine()
}
}
clean {
if ($null -ne $reader) { $reader.Dispose() }
}
}clean is not available in Windows PowerShell 5.1 or PowerShell versions before 7.3. This course targets PowerShell 7.6.x, so it is available here.Output written to the Success stream from the clean block is discarded. Use the block for cleanup, not for normal results.
5. Diagnose binding failures systematically instead of reshaping data blindly
A common failure occurs when the destination expects a property name that the source object does not have. Start by inspecting both sides.
function Get-TargetStateByName {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name
)
process { [pscustomobject]@{ Name=$Name; State='Observed' } }
}
$source = [pscustomobject]@{ Server='api-01'; Region='eu' }
$source | Get-Member
Get-Help Get-TargetStateByName -Parameter Name
# This fails because the source has Server, not Name.
# $source | Get-TargetStateByNameRepair the shape explicitly so the contract is visible.
$source |
Select-Object @{Name='Name';Expression={$_.Server}},Region |
Get-TargetStateByNameAnother valid design is to give the destination parameter an alias when Server is intentionally part of the supported input contract. Do not add aliases merely to hide mismatched models.
6. Collecting the whole pipeline defeats streaming and can amplify memory use
A pipeline-aware function should usually process each input item in process. If you append every object into a large in-memory array before doing any work, the caller loses early results and memory requirements grow with the complete input size.
# Streaming pattern
process {
[pscustomobject]@{ Name=$Name; CheckedAt=Get-Date }
}There are legitimate operations that require the whole dataset, such as a global sort or aggregate. Make that requirement explicit instead of accidentally buffering because the function was written like a batch script.
7. One function can support explicit invocation and pipeline input
function ConvertTo-TargetRecord {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Name
)
process {
[pscustomobject]@{
Name = $Name.Trim().ToLowerInvariant()
At = Get-Date
}
}
}
ConvertTo-TargetRecord -Name api-01
'worker-01','db-01' | ConvertTo-TargetRecord
[pscustomobject]@{Name='cache-01'} | ConvertTo-TargetRecordThis is a useful command contract: direct callers can name the parameter explicitly, while pipeline callers can compose the same function into larger workflows.
8. Use a source → rule → destination checklist
| Question | Example |
|---|---|
| What object is arriving? | A string or PSCustomObject |
| What members does it expose? | Inspect with Get-Member |
| What does the destination accept? | Inspect Get-Help -Parameter |
| Which binding rule is declared? | ByValue and/or ByPropertyName |
Does process handle one record? | Normally yes for pipeline-capable commands |
9. Lab: build one command that accepts strings and property-based objects
function Get-LabTargetHealth {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Name
)
begin {
$seen = 0
}
process {
$seen++
[pscustomobject]@{ Name=$Name; Healthy=$true; Sequence=$seen }
}
end {
Write-Verbose "Processed $seen target(s)"
}
clean {
Write-Verbose 'Cleanup phase reached'
}
}
Get-LabTargetHealth -Name api-01
'worker-01','db-01' | Get-LabTargetHealth -Verbose
@([pscustomobject]@{Name='cache-01'},[pscustomobject]@{Name='queue-01'}) | Get-LabTargetHealth- Explicit invocation returns one object.
- Pipeline strings bind by value.
- Objects with a Name property bind by property name.
- The process block emits one record per input.
- Verbose lifecycle messages do not alter the Success-stream object contract.
10. Pipeline-function mistakes and safer fixes
| Mistake | Symptom | Fix |
|---|---|---|
No process for record-by-record work | Pipeline behavior is not what the author expected | Put per-input logic in process |
| Assume property names match | Binding error or missing required value | Inspect source members and destination help |
| Buffer every input object | High memory use and delayed results | Stream unless whole-dataset logic is required |
Emit normal results from clean | Success output is discarded | Use clean only for cleanup |
Use clean while claiming 5.1 compatibility | Parser/runtime incompatibility | Gate the version or use older cleanup patterns |
11. Knowledge check
Question 1. What does ValueFromPipeline mean?
Question 2. What does ValueFromPipelineByPropertyName mean?
Question 3. Which block normally handles each incoming pipeline object?
process.Question 4. When was the clean block added?
Question 5. Why is collecting every pipeline object into an array often a poor default?
12. Summary
Pipeline-capable functions declare binding rules at their parameter boundary. Use ValueFromPipeline for by-value binding and ValueFromPipelineByPropertyName when source properties form part of the contract. Put one-time setup in begin, per-record work in process, normal completion work in end, and PowerShell 7.3+ resource cleanup in clean. Diagnose failures by inspecting the source object and destination parameter instead of guessing. Preserve streaming unless the algorithm genuinely requires the whole dataset.
13. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.