To monitor Windows Task Scheduler, make each task ping a heartbeat URL at the end of a successful run, and alert when that ping does not arrive on schedule. Task Scheduler cannot do this for you: it records that it launched the action, and in my tests it logged "successfully completed" for a script that failed with exit code 16.
I'm Léo, I build Hyperping. This guide covers how scheduled tasks fail without anyone noticing, what Windows shows you natively, and the PowerShell setup I use with Hyperping healthchecks. I registered and ran every task below on Windows 11 with Windows PowerShell 5.1, as SYSTEM.
Key takeaways
- A task whose action exits non-zero still logs event 201 "successfully completed" and event 102 "successfully finished". Only Last Run Result shows the real code, and nothing alerts on it.
- Missed runs are skipped by default.
-StartWhenAvailableruns them after the machine wakes up, and the cmdlet's defaults refuse to start on battery. - Launch PowerShell with
-File, not-Command, and check$LASTEXITCODEafter native programs. Robocopy needs special care: exit codes 1 to 7 mean success. - Use
Invoke-WebRequest -UseBasicParsingin Windows PowerShell 5.1. Since KB5074596 (December 2025), the prompt it shows otherwise can hang a scheduled task. - A heartbeat sent only on success catches failed runs, skipped runs, hung runs and a machine that is off, with one alert rule.
How Windows scheduled tasks fail silently
"Successfully completed" does not mean it worked
Task Scheduler judges whether it launched the action. I registered a task whose script exited 16 and read the Operational log:
201: Task Scheduler successfully completed task "\HyperpingGuideTest\exit16" , instance "{2efc7225-...}" , action "cmd.exe" with return code 2147942416.
102: Task Scheduler successfully finished "{2efc7225-...}" instance of the "\HyperpingGuideTest\exit16" task for user "NT AUTHORITY\SYSTEM".2147942416 is 0x80070010, the exit code 16 wrapped in an HRESULT. The task list shows Last Run Result: 0x10 and the history shows success. No email, no event with an error level.
PowerShell swallows exit codes
Two details decide whether Task Scheduler sees your script's failure at all:
-Fileversus-Command. The powershell.exe docs say that with-Command, an exit code "other than 0 or 1" is "converted to 1". I checked: a script ending withexit 16returned 16 through-Fileand 1 through-Command.- Native programs.
$ErrorActionPreference = 'Stop'turns PowerShell errors into script-terminating errors, but a native.exereturning 2 is not an error in Windows PowerShell 5.1. You have to read$LASTEXITCODE, which holds "the exit code of the last native program or PowerShell script that ran".
Robocopy is the trap everybody hits: exit codes 0 to 7 mean success (1 means files were copied), 8 and above mean failure. A script that does exit $LASTEXITCODE after a successful robocopy reports a failure every night.
Missed runs are skipped
If the computer is asleep, off, or rebooting for updates at 02:00, the run is skipped. "Run task as soon as possible after a scheduled start is missed" (StartWhenAvailable) is off by default. "Wake the computer to run this task" (WakeToRun) is off by default too.
On laptops, the battery conditions add another silent skip. I printed the defaults of New-ScheduledTaskSettingsSet on Windows 11:
PS> $d = New-ScheduledTaskSettingsSet
PS> $d.DisallowStartIfOnBatteries, $d.StopIfGoingOnBatteries, $d.StartWhenAvailable, $d.ExecutionTimeLimit
True
True
False
PT72HSo a task created with default settings does not start on battery, stops if the laptop is unplugged, and can run for 72 hours before Windows stops it.
A hung run blocks the next ones
The default for a task that is still running when its next trigger fires is IgnoreNew: the new run is dropped, which the history logs as event 322 (launch request ignored, instance already running). A script waiting forever on a network share, or on the Invoke-WebRequest prompt below, can block a daily task for up to three days.
The run-as account loses access
"Run whether user is logged on or not" stores the user's password. When the password changes, the task fails to log on and never starts. Ticking "Do not store password" uses S4U, which "can only use the security context to access local resources", so a backup to a network share fails. For local jobs, running as SYSTEM avoids both.
Invoke-WebRequest can hang the task
Since the December 2025 updates (CVE-2025-54100, KB5074596), Invoke-WebRequest in Windows PowerShell 5.1 shows a "Script Execution Risk" prompt when it parses a response, and Microsoft notes the prompt "could cause the script to hang while waiting for input". -UseBasicParsing skips it. Any heartbeat script for 5.1 needs that switch.
How to check scheduled tasks with PowerShell
# Last run, its result and the next run
Get-ScheduledTaskInfo -TaskName 'Nightly backup' |
Select-Object LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns
# Same information, plus the account and logon mode
schtasks /query /tn 'Nightly backup' /v /fo LIST
# Task history is off on many machines: turn it on
wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true
# Last events for the task
Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-TaskScheduler/Operational'; StartTime = (Get-Date).AddDays(-1) } |
Where-Object Message -like '*Nightly backup*' |
Select-Object TimeCreated, Id, MessageLastTaskResult is the number to read:
| Last Run Result | Meaning |
|---|---|
0x0 |
The action exited 0 |
0x1, 0x10... |
The action's own exit code (1, 16...) |
0x41301 |
The task is running now |
0x41303 |
The task has never run |
0x41306 |
The task was terminated by a user |
0x8004131F |
Refused: an instance is already running |
Task history was disabled on the Windows 11 machine I used. Once enabled, the events I saw for a normal run were 107 (time trigger), 100 (task started), 200 (action launched), 201 (action completed, with its return code) and 102 (task finished). These commands only help if someone runs them.
How to monitor Windows Task Scheduler with Hyperping
A Hyperping healthcheck gives the task a secret URL. When the success ping does not arrive by the scheduled time plus a grace period, Hyperping opens an incident and alerts you, and it resolves on the next successful ping. Healthchecks are included on every plan, Free included.
1. Create a healthcheck with the task's schedule
In Hyperping, open Healthchecks, click Create healthcheck, and pick Cron. Write the trigger as a cron expression: a daily trigger at 2:00 AM is 0 2 * * *, a weekly one on Mondays at 6:00 AM is 0 6 * * 1. For the timezone, run Get-TimeZone on the machine and pick the matching IANA name: Romance Standard Time is Europe/Paris, Eastern Standard Time is America/New_York.
For a trigger that repeats every N minutes, use simple mode with the same interval, or the cron generator's every 15 minutes and every hour pages to check the expression.
2. Store the ping URL where only SYSTEM can read it
New-Item -ItemType Directory -Force 'C:\ProgramData\Hyperping' | Out-Null
icacls 'C:\ProgramData\Hyperping' /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F'
'https://hc.hyperping.io/tok_your_backup_token' | Set-Content 'C:\ProgramData\Hyperping\backup-url.txt'The two SIDs are SYSTEM and the local Administrators group, so the command works whatever the Windows display language. The URL is the healthcheck's only credential.
3. Wrap the job in a PowerShell script that pings on success
# C:\Scripts\backup-with-heartbeat.ps1
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$Source = 'D:\Data'
$Destination = 'E:\Backups\Data'
$HcUrl = (Get-Content -Raw 'C:\ProgramData\Hyperping\backup-url.txt').Trim()
function Send-Heartbeat([string]$Url) {
try {
Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 10 | Out-Null
} catch {
Write-Warning "Hyperping ping failed: $($_.Exception.Message)"
}
}
Send-Heartbeat "$HcUrl/start"
# Robocopy exit codes 0 to 7 mean success, 8 and above mean failure.
robocopy $Source $Destination /MIR /R:2 /W:5 /NP /LOG:C:\ProgramData\Hyperping\backup.log
if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE }
Send-Heartbeat $HcUrl
exit 0Replace the robocopy block with your own job. For most programs, the check becomes if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }. A PowerShell error anywhere else stops the script ($ErrorActionPreference = 'Stop'), which exits 1 under -File, so no success ping is sent.
The TLS 1.2 line only matters on older Windows Server builds where .NET does not enable it by default. The heartbeat function never throws, so a network problem never turns a good backup into a failed task.
4. Register the task with settings that do not skip runs
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\backup-with-heartbeat.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Nightly backup' -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal-ExecutionTimeLimit of one hour replaces the 72 hour default, so a hung run is stopped long before the next night. If the job needs a network share, run it as a domain account with a stored password instead of SYSTEM, and expect to update the task when that password changes.
Here is what I got on Windows 11 with this exact script, registered as SYSTEM, and a local listener in place of Hyperping:
| Run | Pings received | Last Run Result |
|---|---|---|
| Time trigger, robocopy copies a file (exit 1) | /start, then success |
0x0 |
| Source folder missing, robocopy exits 16 | /start only |
0x10 |
| Robocopy succeeds, ping endpoint down | none | 0x0 |
The requests arrived with the user agent WindowsPowerShell/5.1, which is what you will see in the healthcheck's ping list.
5. Size the grace period and route the alerts
In cron mode, Hyperping expects the success ping by the scheduled time plus the grace period. Give it the job's usual run time plus a margin: 30 minutes for a backup that takes 10. After a few days, the healthcheck's ping history shows each run's duration between /start and success.
With -StartWhenAvailable, a run missed while the machine was asleep alerts at 02:00 plus the grace period, then the catch-up run after wake-up sends the ping and closes the incident.
A missed ping alerts every channel connected to the project at once: email and SMS to every member, Slack, Discord, Telegram, PagerDuty and Opsgenie. Healthchecks do not use escalation policies, on-call schedules or Microsoft Teams, so if a failed backup should page the person on call, route it to PagerDuty or Opsgenie.
Test it before the first night
- Run
Start-ScheduledTask -TaskName 'Nightly backup'and check that both pings appear in the healthcheck's last pings. - Point
$Sourceat a folder that does not exist and start the task again. Last Run Result becomes0x10, only/startarrives, and the alert reaches you after the grace period. - Restore the path and run it once more: the incident closes.
The same pattern works for systemd timers on Linux, Kubernetes CronJobs, GitHub Actions scheduled workflows and the Laravel scheduler. For Windows hosts themselves, see the best Windows server monitoring tools. Whatever the host, a nightly backup needs more than an exit code: see monitoring database backups.
FAQ
Why does Task Scheduler say my task completed successfully when it failed? ▼
Task Scheduler reports whether it could launch the action, not whether the action worked. In my test, a task whose script exited 16 logged event 201 "successfully completed" with return code 2147942416 and event 102 "successfully finished". The real exit code only shows in Last Run Result, as 0x10, and nothing alerts on it.
How do I make a scheduled task run after the computer was asleep or off? ▼
Enable "Run task as soon as possible after a scheduled start is missed", which is `-StartWhenAvailable` in `New-ScheduledTaskSettingsSet`. It is off by default, so a missed run is skipped. `-WakeToRun` wakes the computer for the task, and on laptops add `-AllowStartIfOnBatteries` and `-DontStopIfGoingOnBatteries`, since the cmdlet's defaults refuse to start on battery.
How do I return an exit code from a PowerShell scheduled task? ▼
Start the script with `powershell.exe -File script.ps1` and end it with `exit N`. With `-File`, the process exit code is the number you pass. With `-Command`, any code other than 0 or 1 collapses to 1: my test script exiting 16 returned 16 with `-File` and 1 with `-Command`. For native programs, check `$LASTEXITCODE` yourself, because `$ErrorActionPreference = 'Stop'` does not catch their exit codes in Windows PowerShell 5.1.
What does Last Run Result 0x41303 mean in Task Scheduler? ▼
0x41303 is SCHED_S_TASK_HAS_NOT_RUN: the task has never run. 0x41301 means it is running now, 0x0 means the action exited 0, and any other small number is the action's own exit code, such as 0x1 or 0x10. 0x8004131F means a new run was refused because an instance was already running.
Why does my scheduled PowerShell script hang when it calls Invoke-WebRequest? ▼
Since the December 2025 security updates (KB5074596), Invoke-WebRequest in Windows PowerShell 5.1 shows a security prompt when it parses a page, and a scheduled task cannot answer it. Pass `-UseBasicParsing`, which skips the prompt. PowerShell 7 always uses basic parsing.
Is Task Scheduler history enabled by default? ▼
It was disabled on the Windows 11 machine I tested, and I found no Microsoft page that states the default. Enable it with `wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true` or "Enable All Tasks History" in the Task Scheduler console, then read it with Get-WinEvent.




