PowerShell Tee-Object: Split Pipeline to Screen and File

Long-running automation scripts produce output you want to see live on the screen AND preserve in a log file. Without PowerShell Tee-Object, you must choose one or the other, or duplicate every output line with extra code. Tee-Object splits the pipeline — everything flowing through it goes simultaneously to the console and to a file or variable, with zero impact on downstream pipeline processing.
Quick Answer / TL;DR
Insert Tee-Object -FilePath C:\Logs\output.log anywhere in a pipeline to write to a file while passing objects through unchanged. Add -Append to append rather than overwrite.
Tee-Object to a File
Tee-Object with -FilePath writes the current pipeline objects to a file and passes them through to the next pipeline stage. The file receives objects formatted as they would appear in the console. The file is created if it does not exist.
# Log and display simultaneously
Get-Service | Tee-Object -FilePath C:\Logs\services.txt | Format-Table Name, Status -AutoSize
# Objects continue through — Where-Object receives the original objects
Get-Process | Tee-Object -FilePath C:\Logs\processes.txt |
Where-Object CPU -gt 10 |
Format-Table Name, Id, CPU -AutoSize
Tee-Object to a Variable
Use -Variable to capture pipeline output in a variable while still passing objects downstream. The variable is populated as the pipeline runs. This is useful when you need both the full result set in a variable and the ability to process items downstream in the same statement.
# Capture all services AND filter for display simultaneously
Get-Service |
Tee-Object -Variable allServices |
Where-Object Status -ne Running |
Format-Table Name, Status -AutoSize
# After the pipeline: $allServices has all services
Write-Host "Total services: $($allServices.Count)"
Write-Host "Stopped services shown above"
Append Mode with -Append
By default, Tee-Object overwrites the target file on each run. Use -Append to add output to an existing log file, which is essential for cumulative logs from scripts that run repeatedly on a schedule.
# Append to existing log with timestamp separator
"=== Run: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===" |
Tee-Object -FilePath C:\Logs\daily.log -Append | Out-Null
Get-ChildItem C:\Deploy -Recurse -File |
Tee-Object -FilePath C:\Logs\daily.log -Append |
Measure-Object | ForEach-Object { Write-Host "Files: $($_.Count)" }
Chain Multiple Tees
You can chain multiple Tee-Object calls in a single pipeline. Each one makes a copy of the current data to its respective target, and all objects continue flowing through. This lets you write to multiple log files at different stages of processing.
# Write raw data to one file, filtered data to another, display final results
Get-EventLog -LogName System -Newest 100 |
Tee-Object -FilePath C:\Logs\all_events.txt |
Where-Object EntryType -eq 'Error' |
Tee-Object -FilePath C:\Logs\errors_only.txt |
Select-Object -First 10 |
Format-Table TimeGenerated, Source, Message -Wrap
Use in Long-Running Scripts
For scripts with long execution times, Tee-Object lets operators see progress while automatically building an execution log. Pair it with timestamped output for a professional logging pattern without a complex Write-Log function.
# Long-running migration script with live log
$logFile = "C:\Logs\migration_$(Get-Date -Format 'yyyyMMdd_HHmm').log"
Get-ADUser -Filter * -Properties Department | ForEach-Object {
[PSCustomObject]@{
Time = Get-Date -Format 'HH:mm:ss'
User = $_.SamAccountName
Department = $_.Department
Status = 'Processed'
}
} |
Tee-Object -FilePath $logFile |
Format-Table Time, User, Department, Status -AutoSize
Write-Host "Log saved to $logFile"
Tee-Object vs Out-File
The key difference: Out-File terminates the pipeline — nothing passes through after it. Tee-Object is transparent — objects continue flowing unchanged. Use Out-File at the end of a pipeline when display is not needed. Use Tee-Object when you need both logging and continued pipeline processing in the same statement.
# Out-File: pipeline ends here, nothing downstream
Get-Service | Out-File C:\Logs\services.txt
# Get-Service | Out-File ... | Format-Table ← nothing to format
# Tee-Object: objects continue flowing
Get-Service | Tee-Object -FilePath C:\Logs\services.txt | Format-Table Name, Status -AutoSize
# Both the file AND the formatted console output are produced
Common Errors and Fixes
- Tee-Object passes objects through — Format-* before it converts to strings. If you place
Format-TablebeforeTee-Object, the file receives format strings instead of data. The downstream pipeline also receives format objects, not the original data. Always place Format-* cmdlets after the lastTee-Object, or use a separate pipeline for file output if you need formatted text in the file. - File path must exist or will be created — no error on creation.
Tee-Objectcreates the file automatically if it does not exist. The parent directory must exist though —Tee-Objectdoes not create directories. UseNew-Item -ItemType Directory -Forcebefore writing to a new log directory.
Related Cmdlets / See Also
Wrapping Up
Tee-Object is the right tool whenever you need live console output AND a file log from the same pipeline. Use -FilePath for file logging, -Variable for in-memory capture, and -Append for cumulative logs. Always place it before any Format-* cmdlets so the file receives proper data objects rather than display strings.


