A PowerShell advanced function declares `[Parameter(ValueFromPipeline = $true)]` on `$Name` and puts all of its logic directly in the function body with no begin/process/end blocks. Piping ten names into it produces output for only the last one. Why?
answer
- objects arrive one at a time, not as a list
- an unnamed body is not neutral — it is one specific phase
- that phase runs after the stream closes
- the parameter was overwritten on every arrival
- per-item work needs its own block
basics
~20 sA function body with no named blocks is treated entirely as the end block, which runs once after the pipeline is exhausted. The pipeline parameter is rebound for each incoming object, so by the time that code runs it holds only the last one. Per-item logic belongs in a process block.
solid answer
~50 sAn unblocked function body is implicitly the `end` block. `end` runs exactly once, after the upstream command has finished sending. Meanwhile the engine rebinds `$Name` for every object it receives, so the variable is overwritten nine times and holds the tenth value when your code finally executes — hence one line of output. The fix is to declare `process { ... }` and put the per-item work there: `process` runs once per incoming object with the parameter bound to that object. Use `begin` for one-time setup such as opening a connection or zeroing a counter, and `end` for a summary after the stream closes. PowerShell 7.3 added a `clean` block that always runs for teardown. This is also why the same function still works when called directly with `-Name 'web01'` — a single non-pipeline invocation runs begin, process once, then end.
code
powershell · 12 linesfunction Convert-Name {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]$Name
)
begin { $count = 0 }
process { $count++; $Name.ToUpper() }
end { Write-Verbose "Processed $count name(s)" }
}
'web01', 'web02', 'web03' | Convert-Name -Verbosego deeper
Know that a function taking pipeline input needs a process block, and that code written with no blocks at all runs only once at the end.
Explain the three phases precisely — begin once before, process once per object, end once after — and say why the parameter holds the last value in an unblocked body.
Show the streaming consequence: emit from process so downstream work starts immediately and memory stays flat, and reserve end for summaries rather than for dumping an accumulated array.
Treat pipeline participation as part of a tool's public contract — a function that buffers or drops all but the last item cannot be composed by anyone else, which is what makes it a review-blocking defect rather than a style note.
## The pipeline is a stream, and your function is a state machine When a PowerShell command sits in the middle of a pipeline, it does not receive a collection. It receives objects **one at a time**, as the upstream command produces them. A function that wants to participate in that stream has to expose three moments to the engine: something to do before the first object, something to do for each object, and something to do after the last one. Those are the `begin`, `process` and `end` blocks. ```powershell function Convert-Name { [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true)] [string]$Name ) begin { $count = 0 } process { $count++; $Name.ToUpper() } end { Write-Verbose "Processed $count name(s)" } } 'web01', 'web02', 'web03' | Convert-Name ``` ## Why the unblocked version only sees the last item If you write no named blocks, PowerShell does not spread your code across all three phases. It treats the **entire body as the `end` block**. That single decision explains the symptom completely: 1. Upstream sends `web01`. The binder assigns it to `$Name`. There is no `process` block, so nothing runs. 2. Upstream sends `web02`. The binder **overwrites** `$Name`. Still nothing runs. 3. ... nine more times ... 4. Upstream finishes. Now the `end` block — your whole body — runs, once, with `$Name` holding `web10`. You get one result, and it is the last item. The function is not broken in a way that throws; it silently processes a tenth of the data, which is exactly the kind of bug that reaches production. A related detail: in a **simple** function (no `[CmdletBinding()]`, no pipeline parameter) the automatic variable `$input` holds an enumerator over everything that arrived, so `foreach ($item in $input) { ... }` in an unblocked body does see all of them. That is the older idiom, and it is part of why the failure surprises people — the unblocked shape *can* work, but only via `$input`, not via a bound parameter. ## What each block is for **`begin`** runs once, before the first object arrives. Use it for setup that must not repeat per item: opening a database connection, reading configuration, creating the accumulator list, validating that a prerequisite exists. Note that a pipeline-bound parameter is **not yet bound** in `begin` — there is no object yet — so referencing `$Name` there gives you nothing useful. **`process`** runs once per object received, with the pipeline parameter bound to that object. This is where essentially all real work goes. If the function is called without a pipeline (`Convert-Name -Name 'web01'`), `process` still runs — exactly once. **`end`** runs once after the stream is exhausted. Use it for summaries, closing handles, or emitting an aggregate you built up across items. **`clean`** (PowerShell 7.3 and later) runs after everything else *even if the pipeline was stopped or an exception unwound it*, which the `end` block is not guaranteed to do. It is the resource-cleanup block, and it cannot emit to the success stream. One rule to remember: as soon as you use **any** named block, you must put **all** your code in named blocks. Loose statements mixed with a `process` block are a parse error. ## Streaming versus buffering Writing `process` correctly is also what makes your function *stream*. A `process` block emits each result as it is computed, so a downstream command starts working immediately and memory stays flat regardless of how many objects flow through. Code that accumulates everything into an array and emits it from `end` holds the whole result set in memory and delays the first downstream object until the last input arrives. For a hundred items nobody notices; for a million-row export it is the difference between a working tool and one that dies. ## Binding by value versus by property name `ValueFromPipeline = $true` binds the **whole incoming object** to the parameter, subject to type conversion. `ValueFromPipelineByPropertyName = $true` instead looks for a **property on the incoming object whose name matches the parameter name** (or one of its aliases) and binds that. The second is what lets `Get-Process | Stop-MyThing` work when the objects carry an `Id` or `Name` property. A parameter may declare both; the binder tries by value first, then by property name. ## How to recognise this bug in the wild The signature is: the function works perfectly when called with an explicit argument, and quietly produces one result — the last, or sometimes the first if the author used `$input` incorrectly — when used in a pipeline. Anyone reporting "it only processed one row" from a pipeline-capable function should have you looking for a missing `process` block before anything else.
- Why is a pipeline-bound parameter empty inside the `begin` block?Because `begin` runs before the first object is received, so there is nothing to bind yet. Only parameters passed as ordinary arguments are available there. Use `begin` for setup that does not depend on the incoming data — counters, connections, configuration — and read the pipeline parameter in `process`, where it is bound to the current object.
- What does `ValueFromPipelineByPropertyName` do differently from `ValueFromPipeline`?`ValueFromPipeline` binds the entire incoming object to the parameter, converting types if it can. `ValueFromPipelineByPropertyName` instead looks for a property on the incoming object matching the parameter's name or an alias and binds that value. The second is what makes objects from unrelated commands compose, since matching happens by property name rather than by whole-object type.
- Why does emitting from the process block matter for memory rather than just style?A `process` block emits each result the moment it is ready, so the downstream command consumes it and memory stays flat no matter how long the stream is. Accumulating into an array and emitting from `end` holds the entire result set at once and delays the first downstream object until the last input arrives — fine for tens of items, fatal for millions.
saying these in an interview costs you the question
- Thinks an unblocked body runs once per pipeline object
- Believes the pipeline parameter arrives as a full array
- Puts per-item work in begin because it 'comes first'
- Mixes loose statements with a process block and expects it to parse
- Assumes a pipeline-bound parameter is readable in begin