ForEach-Object runs a script block once for every object that comes down the pipeline. Inside the block, $_ is the current object:
'Austin', 'Denver', 'Seattle' | ForEach-Object {
"Sending the weekly report to the $_ office"
}
Output:
Sending the weekly report to the Austin office
Sending the weekly report to the Denver office
Sending the weekly report to the Seattle office
$PSItem is another name for $_, if you find it easier to read. I ran every example in PowerShell 7.6 and Windows PowerShell 5.1, and I note where they differ.
Work with each file, user or row
ForEach-Object is most useful at the end of a pipeline, where each object has properties you can use:
Get-ChildItem -Path C:\psfaqs\Reports -File | ForEach-Object {
'{0,-18} {1,6:N1} KB' -f $_.Name, ($_.Length / 1KB)
}
Output:
budget-2026.xlsx 5.0 KB
notes.txt 0.3 KB
q3-sales.csv 2.0 KB
The same pattern works for rows from Import-Csv, services, users or anything else a cmdlet returns.
Run code before and after the loop with -Begin and -End
-Begin runs once before the first object, -Process runs for each object, and -End runs once at the end. That’s handy for totals:
Get-ChildItem -Path C:\psfaqs\Reports -File | ForEach-Object -Begin {
$total = 0
'Checking files...'
} -Process {
$total += $_.Length
} -End {
"Total size: $([int]($total / 1KB)) KB"
}
Output:
Checking files...
Total size: 7 KB

Shortcut: call a property or method on every object
For a single property or method, skip the script block and pass its name:
'austin', 'denver' | ForEach-Object -MemberName ToUpper
Get-ChildItem -Path C:\psfaqs\Reports -File | ForEach-Object -MemberName Name
Output:
AUSTIN
DENVER
budget-2026.xlsx
notes.txt
q3-sales.csv
This is the same as ForEach-Object { $_.ToUpper() }, just shorter.
Skip an item: use return, not continue
Inside ForEach-Object, return moves on to the next object:
1..5 | ForEach-Object {
if ($_ -eq 3) { return }
"Processing order 100$_"
}
'Done'
Output:
Processing order 1001
Processing order 1002
Processing order 1004
Processing order 1005
Done
continue and break don’t work the way they do in a loop here. They stop the whole script, which the Import-Csv guide shows step by step.
ForEach-Object vs the foreach loop
PowerShell also has a foreach statement. It looks similar, but it works on a list that’s already in memory instead of the pipeline:
# ForEach-Object: works in the pipeline, one object at a time
Get-ChildItem -Path C:\Reports | ForEach-Object { $_.Name }
# foreach statement: loops over a list you already have
$files = Get-ChildItem -Path C:\Reports
foreach ($file in $files) { $file.Name }
To compare speed, I doubled 200,000 numbers three ways:
$numbers = 1..200000
$tests = [ordered]@{
'ForEach-Object' = { $numbers | ForEach-Object { $_ * 2 } }
'foreach loop' = { foreach ($n in $numbers) { $n * 2 } }
'.ForEach()' = { $numbers.ForEach({ $_ * 2 }) }
}
foreach ($name in $tests.Keys) {
'{0,-15} {1,6:N0} ms' -f $name, (Measure-Command { $null = & $tests[$name] }).TotalMilliseconds
}
Output in PowerShell 7 (your times will differ):
ForEach-Object 863 ms
foreach loop 16 ms
.ForEach() 363 ms
Output in Windows PowerShell 5.1:
ForEach-Object 873 ms
foreach loop 21 ms
.ForEach() 567 ms

| ForEach-Object | foreach statement | |
|---|---|---|
| Speed on large lists | Slowest | Fastest |
| Memory | One object at a time | Needs the whole list |