Guides · Scheduled scripts

Task Scheduler Says Success but Your PowerShell Script Failed

Published · Tested with PowerShell 7.6.6 and Windows PowerShell 5.1 (build 26100) on Windows 11 Pro

A script runs every night from Task Scheduler. One morning the report is missing, but the task says the last run was fine: 0x0. Task Scheduler only sees the exit code of the process it started, and a PowerShell script that hits an error often still exits with 0. This guide measures which failures reach Task Scheduler and which do not, then gives a script template that turns every error into exit code 1 and a check that lists the tasks whose last run failed.

Before you start

Which failures reach Task Scheduler

The same scripts, started three ways from the task's action. Windows PowerShell 5.1 and PowerShell 7 gave identical results in every row:

LastTaskResult by what the script did and how the task started it
What the script did-File script.ps1-Command "& 'script.ps1'"-Command "& 'script.ps1'; exit $LASTEXITCODE"
Finished normally000
A cmdlet error (Get-Item on a missing file), then went on000
The same error inside try, with a catch that only writes a warning000
Ran a program that exited with 3, then went on003
The program that exited with 3 was the last line003
The same cmdlet error with $ErrorActionPreference = 'Stop'111
throw 'input file not found'111
exit 1111
exit 3313

The bold zeros are the failures Task Scheduler never hears about. They follow the documented rules of pwsh (Microsoft Learn, about_Pwsh):

When your script never ran

Some results mean the script did not start or did not finish. From the same test runs (three runs each, same result every time, except the time limit case, which ran once per version):

Task action                                              LastTaskResult
pwsh.exe without a path (Microsoft Store install)        2147942402 (0x80070002)
powershell.exe without a path                            0
-File with a wrong script path, Windows PowerShell 5.1   4294770688 (0xFFFD0000)
-File with a wrong script path, PowerShell 7             64
-Command with a wrong script path, both versions         1
Script still running at the task's 1-minute time limit   267014 (0x41306)

A script that fails loudly

The template below makes every error reach Task Scheduler. Put your job between the two marker lines; here it reads a CSV file. Save it as C:\Jobs\Nightly-Job.ps1:

# Nightly-Job.ps1: a script for Task Scheduler that fails loudly.
# Any error stops the script, is written to the log, and ends it with exit code 1,
# which Task Scheduler shows as the task's Last Run Result.
param(
    [string] $LogPath = (Join-Path $PSScriptRoot 'nightly-job.log')
)
$ErrorActionPreference = 'Stop'
# PowerShell 7.4+: a native program that exits non-zero throws as well.
$PSNativeCommandUseErrorActionPreference = $true

function Write-Log {
    param([Parameter(Mandatory)] [string] $Message)
    $line = '{0:yyyy-MM-dd HH:mm:ss} {1}' -f (Get-Date), $Message
    Add-Content -LiteralPath $LogPath -Value $line -Encoding utf8
}

# Runs a native program and throws if it exits non-zero, naming the line that called it.
# Needed in Windows PowerShell 5.1, which ignores the preference above.
function Invoke-Native {
    param(
        [Parameter(Mandatory)] [string] $FilePath,
        [string[]] $ArgumentList = @()
    )
    $PSNativeCommandUseErrorActionPreference = $false  # this function checks the code itself
    & $FilePath @ArgumentList
    if ($LASTEXITCODE -ne 0) {
        throw "$FilePath exited with code $LASTEXITCODE (called at line $($MyInvocation.ScriptLineNumber))."
    }
}

try {
    Write-Log "start (PowerShell $($PSVersionTable.PSVersion))"

    # ---- the job ----
    $rows = Import-Csv -LiteralPath 'C:\Jobs\input.csv'
    Write-Log "read $($rows.Count) rows"
    # ---- end of the job ----

    Write-Log 'done'
    exit 0
}
catch {
    Write-Log "FAILED at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
    exit 1
}

Register the task with the full path to pwsh.exe and -File. -NonInteractive makes prompts fail instead of waiting: “Any attempts to use interactive features, like Read-Host or confirmation prompts, result in statement terminating errors rather than hanging” (Microsoft Learn).

# The full path to pwsh.exe: the MSI default below, or for the Microsoft Store
# package "$env:LOCALAPPDATA\Microsoft\WindowsApps\pwsh.exe" (the one we tested).
$pwsh = 'C:\Program Files\PowerShell\7\pwsh.exe'
$action = New-ScheduledTaskAction -Execute $pwsh `
    -Argument '-NoProfile -NonInteractive -File "C:\Jobs\Nightly-Job.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At '02:00'
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1)
Register-ScheduledTask -TaskName 'Nightly-Job' -TaskPath '\Jobs\' -Action $action `
    -Trigger $trigger -Principal $principal -Settings $settings

The time limit makes a job that hangs end with 0x41306 instead of running for days. Pick a limit well above the job's usual run time.

Results

The template as a task, three runs per case with each version (output of our test script; Native-Job and Native-Checked put a program that exits with 3 in place of the CSV step, without and with Invoke-Native):

Nightly-Job     pwsh  input.csv present               LastTaskResult: 0, 0, 0
Nightly-Job     ps51  input.csv present               LastTaskResult: 0, 0, 0
Nightly-Job     pwsh  input.csv missing               LastTaskResult: 1, 1, 1
Nightly-Job     ps51  input.csv missing               LastTaskResult: 1, 1, 1
Native-Job      pwsh  native exits 3, unchecked       LastTaskResult: 1, 1, 1
Native-Job      ps51  native exits 3, unchecked       LastTaskResult: 0, 0, 0
Native-Checked  pwsh  native exits 3, Invoke-Native   LastTaskResult: 1, 1, 1
Native-Checked  ps51  native exits 3, Invoke-Native   LastTaskResult: 1, 1, 1

The log tells you why (lines from nightly-job.log, one run of each case):

2026-10-07 14:07:13 start (PowerShell 7.6.6)
2026-10-07 14:07:13 read 3 rows
2026-10-07 14:07:13 done
2026-10-07 14:07:28 start (PowerShell 7.6.6)
2026-10-07 14:07:28 FAILED at line 35: Could not find file 'C:\Jobs\input.csv'.
2026-10-07 14:07:44 start (PowerShell 7.6.6)
2026-10-07 14:07:44 FAILED at line 35: Program "cmd.exe" ended with non-zero exit code: 3 (0x00000003).
2026-10-07 14:07:51 start (PowerShell 5.1.26100.9444)
2026-10-07 14:07:51 after the native command
2026-10-07 14:07:51 done
2026-10-07 14:08:06 start (PowerShell 5.1.26100.9444)
2026-10-07 14:08:06 FAILED at line 27: cmd.exe exited with code 3 (called at line 35).

The 5.1 run that logged “after the native command” and “done” is the trap the template leaves open in Windows PowerShell 5.1: an unchecked program failed and the job finished as a success. Through Invoke-Native, the same program stopped the job in both versions, with the same message; line 27 is the throw inside Invoke-Native, line 35 the job line that called it.

Check the results every day

An exit code only helps if someone reads it. On our machine the Task Scheduler event log (Microsoft-Windows-TaskScheduler/Operational) was disabled, so the History tab was empty; turning it on is a machine-wide setting that needs an administrator, and this guide does not rely on it. Get-ScheduledTaskInfo works without it:

> Get-ScheduledTask -TaskPath '\Jobs\' | Get-ScheduledTaskInfo | Format-Table TaskName, LastRunTime, LastTaskResult

TaskName           LastRunTime            LastTaskResult
--------           -----------            --------------
New-Job-NeverRun   11/30/1999 12:00:00 AM         267011
Nightly-Job-NoFile 10/7/2026 1:57:00 PM                1
Nightly-Job        10/7/2026 1:56:57 PM                0
Old-Job-Swallows   10/7/2026 1:57:02 PM                0

This script turns that into a yes or no. It lists the tasks in one folder whose last run failed, that never ran, or that have not run for longer than a day, and exits 1 when it finds any:

# Get-FailedTask.ps1: list the scheduled tasks in one folder whose last run failed,
# or that have not run recently. Exits 1 when it finds one, so it can feed an alert.
param(
    [string] $TaskPath = '\Jobs\',
    [int] $MaxAgeHours = 26
)
$ErrorActionPreference = 'Stop'
$running   = 0x41301  # SCHED_S_TASK_RUNNING
$notRunYet = 0x41303  # SCHED_S_TASK_HAS_NOT_RUN
$since = (Get-Date).AddHours(-$MaxAgeHours)

$problems = foreach ($task in Get-ScheduledTask -TaskPath $TaskPath) {
    $info = $task | Get-ScheduledTaskInfo
    $result = $info.LastTaskResult
    $problem =
        if ($result -eq $running) { $null }
        elseif ($result -eq $notRunYet) { 'has not run yet' }
        elseif ($result -ne 0) { 'last run failed' }
        elseif ($info.LastRunTime -lt $since) { "no run in $MaxAgeHours h" }
    if ($problem) {
        [pscustomobject]@{
            Task    = $task.TaskName
            LastRun = $info.LastRunTime
            Result  = '0x{0:X}' -f $result
            Problem = $problem
        }
    }
}
if ($problems) {
    $problems | Format-Table -AutoSize | Out-String -Width 200
    exit 1
}
"No problems in $TaskPath"
exit 0

Against the four test tasks above, and then against one task that ran fine (output of the test runs):

> .\Get-FailedTask.ps1

Task               LastRun                Result  Problem
----               -------                ------  -------
New-Job-NeverRun   11/30/1999 12:00:00 AM 0x41303 has not run yet
Nightly-Job-NoFile 10/7/2026 1:57:00 PM   0x1     last run failed

exit code: 1

> .\Get-FailedTask.ps1 -MaxAgeHours 0   # the last run is older than "now"

Task        LastRun              Result Problem
----        -------              ------ -------
Nightly-Job 10/7/2026 1:57:48 PM 0x0    no run in 0 h

exit code: 1

> .\Get-FailedTask.ps1
No problems in \Jobs\
exit code: 0

Old-Job-Swallows is not in the list: its error was caught and dropped inside the script, so its 0 looks like any other success. Only the script can fix that. Run the check where a failure gets seen: from a monitoring tool that reads exit codes, or by hand at the start of the day. Our first version of the check ended without exit 0; called from another script after a failing check, it left the earlier exit code of 1 in $LASTEXITCODE and reported a failure that was not there.

Limits

Just want the scripts? The Scripts Pack · €12 has the code of this guide and the other guides ready to run, with examples and tests. The code on this page stays free.

The code on this page was written for this guide and tested on the versions above. The test copies of the job script also set the UI culture to English, on the same line as $ErrorActionPreference, so that error messages came out in English. The data is invented.