Monitoring Windows Task Scheduler jobs
Task Scheduler is cron for Windows: it runs backups, report exports and maintenance scripts on servers and office machines. It also keeps quiet when something goes wrong. A failed task shows a code like 0x1 in a column nobody looks at, and the built-in “Send an e-mail” action has been deprecated since Windows 8 and Server 2012. Here is how to read the results, avoid the usual traps and get an alert when a task fails or doesn’t run.
What “Last Run Result” means
| Code | Meaning |
|---|---|
0x0 | The program exited with code 0 |
0x1, 0x2… | The program’s own exit code: 1, 2 and so on. For PowerShell, 0x1 often means the script threw an error or couldn’t be loaded |
0x41301 | The task is running right now |
0x41303 | The task has never run |
0x41306 | The task was stopped, by a user or by the time limit |
0x800710E0 | The task was refused, usually because of the account or its rights |
The History tab is empty by default. Turn it on with “Enable All Tasks History” in the Actions pane of Task Scheduler.
The usual traps
- “Run only when user is logged on”. The task doesn’t run on a server nobody is logged in to. Choose “Run whether user is logged on or not”, or run it as
SYSTEM. - Empty “Start in”. The task starts in
C:\Windows\System32, so relative paths in the script point there. Fill in the working folder, or use absolute paths. - Mapped drives.
Z:\exists only in an interactive session. Use UNC paths such as\\nas\backups. - Execution policy. Start PowerShell scripts as
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Jobs\job.ps1, not by the.ps1file alone. - Power settings. On a laptop, “Start the task only if the computer is on AC power” is on by default.
- The time limit. “Stop the task if it runs longer than 3 days” is on by default. A stuck task blocks the next runs until then.
- Exit codes from PowerShell. A script that ends without
exitreports 0 even if commands inside it failed. End it withexit $codeon purpose.
A wrapper that reports to Heartcron
Instead of starting the job directly, start this script. It reports the start, runs the job, and sends the exit code with the end of the output. It works in Windows PowerShell 5.1 and PowerShell 7:
# C:\Jobs\run-backup.ps1
$url = "https://ping.heartcron.com/<uuid>"
try { Invoke-RestMethod "$url/start" -TimeoutSec 10 | Out-Null } catch {}
$out = & "C:\Jobs\backup.exe" --full *>&1 | Out-String
$code = $LASTEXITCODE
if ($null -eq $code) { $code = 0 }
if ($out.Length -gt 100000) { $out = $out.Substring($out.Length - 100000) }
try { Invoke-RestMethod "$url/$code" -Method Post -Body $out -TimeoutSec 10 | Out-Null } catch {}
exit $code
- A non-zero exit code is a failure, and the alert goes out right away, with the output attached.
- If the task doesn’t start at all, because the machine is off or the task was disabled or deleted, no ping arrives and you get an alert after the grace period.
exit $codekeeps the result visible in Task Scheduler too.
If the job is itself a PowerShell script, call it as & powershell.exe -NoProfile -File C:\Jobs\backup.ps1 so that $LASTEXITCODE is set, and end that script with exit.
Create the task
From an elevated PowerShell, run the wrapper every night at 3:00 as SYSTEM:
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Jobs\run-backup.ps1" `
-WorkingDirectory "C:\Jobs"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask -TaskName "Nightly backup" -Action $action -Trigger $trigger `
-User "SYSTEM" -RunLevel Highest
Or the same with schtasks:
schtasks /Create /TN "Nightly backup" /SC DAILY /ST 03:00 /RU SYSTEM /RL HIGHEST ^ /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Jobs\run-backup.ps1"
In Heartcron, create a check with the cron expression 0 3 * * * in the machine’s time zone, and a grace period longer than the backup usually takes. Weekly or monthly triggers translate to cron too; the cron expression explainer shows what an expression means.
Test it
Start-ScheduledTask -TaskName "Nightly backup"
Get-ScheduledTaskInfo -TaskName "Nightly backup" # LastTaskResult should be 0
The check in Heartcron should show the start and the success with the duration. Then make the job fail once to see the alert and the output.
Without a wrapper
The open-source heartcron CLI has a Windows build and does the same as the script above: heartcron.exe run nightly-backup -- C:\Jobs\backup.exe --full. Put that line in the task’s action.