Scripts That Run Without You
A script run by a scheduled task has nobody to answer its questions and doesn't start in its own folder, so it takes parameters instead of asking, builds its paths from $PSScriptRoot, leaves a dated log and a Start-Transcript recording, ends with an honest exit code, and runs as the account the task names.
Freelancer · PowerShell · Operating System · Updated
After this lesson you can
- Test a script the way a task runs it, with pwsh -NonInteractive -File from another folder, and fix questions and relative paths
- Record what an unattended script did with a dated log, Start-Transcript, and an exit code that says whether it worked
- Choose the account a task runs as with New-ScheduledTaskPrincipal, from you while signed in, to you at any time, to SYSTEM
Key takeaways
- Test a scheduled script the way the task runs it: pwsh -NoProfile -NonInteractive -File and its full path, from another folder.
- Under -NonInteractive, Read-Host fails, and a script that carries on can still end with exit code 0: "it worked".
- A task with no start-in folder starts in C:\Windows\System32, so build every path from $PSScriptRoot.
- A log function adds a dated line for each event, run after run; Start-Transcript records everything one run printed, errors included.
- A task runs as you while you're signed in, unless its principal says otherwise. Run as yourself unless the job needs more.
The morning after Sam scheduled the evening backup, the backup folder is empty. Task Scheduler says the last run's result was 0: it worked. Both are true. The task ran the script, and the script did nothing, because it was written for a person at the keyboard, and at 6 PM there was no keyboard, no screen, and not even the right folder. A script that runs with nobody watching is an unattended script, and it needs a few habits that a script you run yourself can do without. IT techs learn them the hard way, from a backup that "worked" for a month.
Run it the way the task will
Make a practice folder with a little school work in it:
New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\nightly\school-work\science" | Out-Null
Set-Content -Path "$HOME\terminal-practice\nightly\school-work\history-essay.txt" -Value 'Why the Town Library Matters', 'Second draft, with the quotes fixed.'
Set-Content -Path "$HOME\terminal-practice\nightly\school-work\science\lab-report.txt" -Value 'Lab 4: how fast does ice melt in salt water?'
Save this first try as Backup-SchoolWork.ps1 in the nightly folder. It works fine when you run it yourself from that folder:
# Backup-SchoolWork.ps1, first try: fine at the keyboard
$answer = Read-Host 'Back up school-work now? (y/n)'
if ($answer -eq 'y') {
Copy-Item -Path school-work -Destination backup -Recurse -Force
'Backup done.'
}
Now run it the way the task from Scheduled Tasks will. A task with no start-in folder starts in C:\Windows\System32, so go there (only to look; nothing there changes) and start the script with the task's own command:
Set-Location C:\Windows\System32
pwsh -NoProfile -NonInteractive -File "$HOME\terminal-practice\nightly\Backup-SchoolWork.ps1"
$LASTEXITCODE
Read-Host: C:\Users\Sam\terminal-practice\nightly\Backup-SchoolWork.ps1:2
Line |
2 | $answer = Read-Host 'Back up school-work now? (y/n)'
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.
0
-NonInteractive tells PowerShell nobody is there, so the question fails, $answer stays empty, the copy is skipped, and the script ends normally: exit code 0. Asking Questions: Read-Host gave the fix. A script that runs alone takes parameters, with defaults for everything, and never asks.
Full paths, from the script's own folder
Take the question out, save, and run it the same way:
# Backup-SchoolWork.ps1, second try: no question
Copy-Item -Path school-work -Destination backup -Recurse -Force
'Backup done.'
pwsh -NoProfile -NonInteractive -File "$HOME\terminal-practice\nightly\Backup-SchoolWork.ps1"
$LASTEXITCODE
Copy-Item: C:\Users\Sam\terminal-practice\nightly\Backup-SchoolWork.ps1:2
Line |
2 | Copy-Item -Path school-work -Destination backup -Recurse -Force
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot find path 'C:\Windows\System32\school-work' because it does not exist.
Backup done.
0
Two problems. school-work was looked for in the folder the task started in, and the script then said Backup done. and ended with 0 anyway. The cure for the first is Run It from Anywhere's $PSScriptRoot, the script's own folder, in front of every path. Not $HOME: as you'll see below, a task can run as another account, with another home folder.
Leave a record: a log and Start-Transcript
Nobody sees a scheduled script's screen, so it writes down what happened. Here's the script, rewritten for running alone:
# Backup-SchoolWork.ps1: safe to run with nobody there
param(
[string]$Source = "$PSScriptRoot\school-work",
[string]$Destination = "$PSScriptRoot\backup"
)
Start-Transcript -Path "$PSScriptRoot\last-run.txt" -UseMinimalHeader | Out-Null
function Write-Log {
param([string]$Message)
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message"
Add-Content -Path "$PSScriptRoot\backup.log" -Value $line
$line
}
Write-Log "Backup started: $Source"
try {
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
Copy-Item -Path "$Source\*" -Destination $Destination -Recurse -Force -ErrorAction Stop
Write-Log 'Backup finished'
$code = 0
}
catch {
Write-Log "Backup failed: $_"
$code = 1
}
Stop-Transcript | Out-Null
exit $code
Three records, three jobs. Write-Log adds one dated line per event to backup.log, run after run: the diary. Start-Transcript (read "start transcript") records everything one run prints, errors included, into last-run.txt, like a recording of the window nobody watched; -UseMinimalHeader keeps its heading short. And exit $code makes the exit code, the number Task Scheduler shows, tell the truth. Save it, and run it as the task would, with a typo in -Source, the way it would go on a night the folder had been renamed:
pwsh -NoProfile -NonInteractive -File "$HOME\terminal-practice\nightly\Backup-SchoolWork.ps1" -Source "$HOME\terminal-practice\nightly\school-wrok"
$LASTEXITCODE
Get-Content "$HOME\terminal-practice\nightly\last-run.txt"
2026-11-09 08:13:38 Backup started: C:\Users\Sam\terminal-practice\nightly\school-wrok
2026-11-09 08:13:38 Backup failed: Cannot find path 'C:\Users\Sam\terminal-practice\nightly\school-wrok' because it does not exist.
1
**********************
PowerShell transcript start
Start time: 20261109081338
**********************
2026-11-09 08:13:38 Backup started: C:\Users\Sam\terminal-practice\nightly\school-wrok
PS>TerminatingError(Copy-Item): "The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find path 'C:\Users\Sam\terminal-practice\nightly\school-wrok' because it does not exist."
2026-11-09 08:13:38 Backup failed: Cannot find path 'C:\Users\Sam\terminal-practice\nightly\school-wrok' because it does not exist.
**********************
PowerShell transcript end
End time: 20261109081338
**********************
Exit code 1, so Task Scheduler's last result says something went wrong, and the transcript even caught the error that catch handled. Your times will be your own. Windows PowerShell 5.1 has no -UseMinimalHeader: leave it out there, and the heading lists your user name, computer and versions.
Which account it runs as
A task runs as somebody. With no say from you, it's you, only while you're signed in: it can do what you can do, and no more. New-ScheduledTaskPrincipal (a principal is the account a task runs as) builds the other choices, and Windows' own tasks show the strongest one:
New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U | Select-Object UserId, LogonType, RunLevel
(Get-ScheduledTask -TaskName ScheduledDefrag).Principal | Select-Object UserId, LogonType, RunLevel
UserId LogonType RunLevel
------ --------- --------
Sam S4U Limited
SYSTEM ServiceAccount Highest
S4U runs as you whether you're signed in or not, without saving your password, but it can't reach network drives. SYSTEM is Windows' own account, stronger than an administrator, and its home folder is C:\Windows\System32\config\systemprofile, which is one more reason for $PSScriptRoot. Hand a principal to Register-ScheduledTask with -Principal $principal; on most PCs, a task that runs while you're signed out, or as SYSTEM, has to be registered from an administrator window. The rule IT calls the principle of least privilege says pick the weakest account that can do the job, and for a school-work backup that's you.
Check yourself
2 questions to try on the lesson page.
So what
No questions, full paths, a log, a transcript and an honest exit code: that's the difference between a backup that says it worked and one that did.