Write a PowerShell script that actually saves you time: proper instructions

A useful PowerShell script removes a repeated, low-risk task while leaving clear evidence of what happened. This guide creates a script that copies files older than a selected age into a dated archive and records a CSV log. It copies rather than deletes, so it is practical for routine organisation without making an irreversible decision during the first automated run.
Prerequisites
- Windows PowerShell 5.1 or PowerShell 7. Check with
$PSVersionTable.PSVersion.
- A test folder with files safe to copy. Do not begin with a live cloud-synchronised folder, production share or irreplaceable documents.
- Permission to read the source and create folders in the archive destination.
- A text editor that can save a UTF-8
.ps1file.
Create a safe archive script
Create C:/Scripts/Archive-OldFiles.ps1 and save this script. It has an explicit test switch, uses literal paths and stops on errors rather than silently continuing.
param(
[Parameter(Mandatory)] [string]$SourcePath,
[Parameter(Mandatory)] [string]$ArchiveRoot,
[ValidateRange(1,3650)] [int]$OlderThanDays = 30,
[switch]$WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$source = (Resolve-Path -LiteralPath $SourcePath).Path
if (-not (Test-Path -LiteralPath $ArchiveRoot -PathType Container)) {
New-Item -ItemType Directory -Path $ArchiveRoot -Force | Out-Null
}
$cutoff = (Get-Date).AddDays(-$OlderThanDays)
$run = Join-Path (Resolve-Path $ArchiveRoot) (Get-Date -Format 'yyyy-MM-dd_HHmmss')
$log = @()
Get-ChildItem -LiteralPath $source -File -Recurse | Where-Object {
$_.LastWriteTime -lt $cutoff
} | ForEach-Object {
$relative = $_.FullName.Substring($source.Length).TrimStart([char]92)
$destination = Join-Path $run $relative
$log += [pscustomobject]@{ Source=$_.FullName; Destination=$destination; Action='Would copy' }
if (-not $WhatIf) {
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
Copy-Item -LiteralPath $_.FullName -Destination $destination -Force
$log[-1].Action = 'Copied'
}
}
if (-not $WhatIf -and $log.Count -gt 0) {
$log | Export-Csv -NoTypeInformation -Path (Join-Path $run 'archive-log.csv')
}
$log | Format-Table -AutoSize
Test before automation
- Create safe test folders and an old test file:
New-Item -ItemType Directory -Force C:/ArchiveTest/Source, C:/ArchiveTest/Archive
Set-Content C:/ArchiveTest/Source/old-note.txt 'test'
(Get-Item C:/ArchiveTest/Source/old-note.txt).LastWriteTime = (Get-Date).AddDays(-45)
- Run a dry test. The one-process execution-policy option avoids changing a machine-wide policy while testing a known local file:
powershell -ExecutionPolicy Bypass -File C:/Scripts/Archive-OldFiles.ps1 -SourcePath C:/ArchiveTest/Source -ArchiveRoot C:/ArchiveTest/Archive -OlderThanDays 30 -WhatIf
- Read the table. It should show
old-note.txtwithWould copy. Confirm the archive has no files. If any source or destination is surprising, fix the parameter or script and repeat the dry test. - Run the identical command without
-WhatIf. Open the dated archive folder, confirm the copied content matches the source and inspectarchive-log.csv. Confirm the source still exists.
Why the details matter
-LiteralPath prevents wildcard interpretation in unusual file names. Strict mode and Stop make undeclared variables and errors visible. The archive preserves the source’s relative structure, preventing same-name files in different folders from overwriting one another. The script uses LastWriteTime, so edit operations can change whether a file matches; inspect dates with Get-ChildItem when the result is unexpected.
Keep the archive root outside the source path. Otherwise later runs can recurse through previous archives. Ensure the archive disk has enough space, and back up the archive if its contents matter. Use full paths and quote paths containing spaces. Review dry-run output whenever you change scope or the age limit. Execution policy is a safety feature, not proof that code is safe: read scripts before running them and do not use bypass for untrusted downloads.
Schedule only after manual proof
After several successful manual runs, create a Task Scheduler task under your own account. Configure the full PowerShell executable, the full script path and explicit source and archive arguments. Run the task manually first, then inspect both the archive and CSV log. A Task Scheduler success status alone does not prove the intended files were copied. Retain a simple run log and periodically check free storage.
Rollback and common mistakes
-
Undo a run: delete unwanted copies, or copy a required archived file back. The original is never removed by this version. -
No files match: confirm the source path, file type and last-write date. -
Access denied: obtain proper read access rather than routinely running automation as Administrator. -
Archive fills up: stop, add capacity or define and test a retention policy before any automated deletion. -
Unexpected result: return to the dry run and narrow the source. Do not broaden the script until the output is understood.
The saved time comes from a narrow scope, a repeatable command and an audit trail, not from making a risky process faster.
Practical review before relying on it
Repeat the complete check once after a restart or on a different day. This catches assumptions caused by cached state, a forgotten setting or an incomplete first run. Record the exact version, device or location involved and keep the successful configuration with the project. When something fails, change one condition at a time, repeat the relevant check and note the result. That approach is slower than random adjustment for one attempt, but much faster when the process must be repeated or explained to someone else.
Do not treat a successful screen message as the only evidence. Check the intended output directly, check that the original data or host system is still healthy, and keep a known-good fallback. A small reversible workflow is more valuable than a complicated one that only works once. When the task becomes routine, keep the same final verification rather than assuming automation removes the need to inspect outcomes.