PowerShell Tee-Object: Split Pipeline to Screen and File

PowerShell Tee-Object: Split Pipeline to Screen and File

PowerShell Tips Editor 3 min read
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-Table before Tee-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 last Tee-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-Object creates the file automatically if it does not exist. The parent directory must exist though — Tee-Object does not create directories. Use New-Item -ItemType Directory -Force before 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.

Send-Item -To