Catch Task Scheduler Failures: Check Your Automated Runs Every Morning
As you set up more automated tasks, the real fear is not that they will fail, but that they will stop running without you noticing. A failure shows up on screen or in logs, but a task that didn't run produces nothing. You only find out days later when you realize, "Wait, that record hasn't updated lately."
In this article, I outline my approach to morning monitoring, which I built when setting up multiple automated tasks on my home PC. I already covered how to actually run tasks in Run Claude Code Automatically Every Day with Windows Task Scheduler, so here I focus strictly on how to verify that tasks haven't stopped.
Monitoring Workflow
- Read task statusGet-ScheduledTask and Get-ScheduledTaskInfo
- Evaluate across three criteriaLast result code / Last run time / Enabled or disabled
- Exclude intentionally stopped tasksFlag tasks as allowed to be stopped in the list
- Record only failuresLimit the same failure to once per day
Three Things to Check
I consider a task to be "running properly" only when all three of the following conditions are met:
- The last result code indicates success. Did it finish without errors the last time it ran?
- The last run time is not too old. If a task scheduled to run daily last ran three days ago, I suspect it has stopped even if the result code says success.
- It is not disabled. A disabled task will not run in the first place.
If you only look at the result code, you will miss tasks where "it succeeded last time, but hasn't run since." In my environment, the overview list primarily determines success or failure based on the result code. Checking the last run time and whether a task is disabled are things I think should be added on top of that.
Minimal PowerShell Example to List Tasks
You can read task definitions with Get-ScheduledTask and run history with Get-ScheduledTaskInfo. According to Microsoft's documentation, the latter gets "the last run-time information for a scheduled task," and you can pipe the output of Get-ScheduledTask directly into it. The output includes LastRunTime (last run time), LastTaskResult (last result), NextRunTime (next run time), and NumberOfMissedRuns (number of missed runs). Whether a task is enabled or disabled appears in the State property of Get-ScheduledTask.
$names = "DailyTasks", "NightlyBackup" # Task names to monitor
Get-ScheduledTask -TaskName $names | ForEach-Object {
$info = $_ | Get-ScheduledTaskInfo
[pscustomobject]@{
Name = $_.TaskName
State = $_.State
Last = $info.LastRunTime
Result = '0x{0:X}' -f [uint32]$info.LastTaskResult
Next = $info.NextRunTime
Missed = $info.NumberOfMissedRuns
}
} | Format-Table -AutoSize
The output displays Result in hexadecimal. If you output LastTaskResult directly, it appears in decimal, making it harder to cross-reference with the table below. In the example output in Microsoft's documentation for Get-ScheduledTask, State is shown as values such as Ready, Disabled, and Running.
Reading Result Codes
Here are the Task Scheduler constants published by Microsoft that you will encounter most often in the overview list.
| Hexadecimal | Decimal | Meaning |
|---|---|---|
| 0x0 | 0 | Completed without errors (success) |
| 0x41300 | 267008 | Ready to run at its next scheduled time |
| 0x41301 | 267009 | Currently running |
| 0x41302 | 267010 | Disabled, so it will not run at its scheduled time |
| 0x41303 | 267011 | Has not yet run |
| 0x41306 | 267014 | The last run was terminated by the user |
| 0x41325 | 267045 | Task Scheduler queued the task to run |
The rows other than 0 are based on descriptions from Microsoft's list of constants. According to Microsoft's documentation, constants whose names begin with SCHED_S_ are success constants, while those starting with SCHED_E_ are error constants. All the nonzero constants in this table are on the SCHED_S_ side. At first glance after setting up the overview list, 0x41301 (running) and 0x41303 (has not run) often look like "failures," but in reality they are not. My initial overview mistakenly displayed 0x41303 on a newly registered task as a failure. I now handle these two separately as "has not run" and "running" rather than "failure."
The description for 0x41306 states "terminated by the user." In my environment, the same value was returned when a task was stopped by the "execution time limit" setting. If a task is designed to be stopped at a specific time every day, this is a normal completion. However, a task that freezes and times out will produce the exact same value, so in my overview list I individually specify "normal only for this task" and treat it as a failure for all others.
When a script exited with a non-zero code such as exit 1, in my environment that value was stored directly in LastTaskResult. If you include exit $code as described in Run Claude Code Automatically Every Day with Windows Task Scheduler, failures become visible here.
Excluding Intentionally Stopped Tasks from "Not Running"
Once you create an overview list, "disabled tasks" will inevitably be mixed in. Some were stopped intentionally by you, while others stopped unexpectedly. If you report every disabled task as an "anomaly," trust in your monitoring will decline. Conversely, if you get used to ignoring stopped tasks, the monitoring becomes pointless.
My rule of thumb is: "Before reporting something as failed or unexecuted, check whether it was intentionally stopped." Keeping a flag and reason for stopped tasks in the overview's configuration file saves the trouble of verifying them manually. When you know why a task was stopped, you won't be confused months later.
There is another caveat when carrying around a laptop. In my environment, tasks configured not to start while running on battery power remained in the Queued state and never ran while unplugged from AC power. Because the last run time becomes outdated in this scenario, the task appears to have "stopped." It isn't broken—it will run once connected back to power—so you need to evaluate it in the context of how you use your PC. This setting is summarized in the table in Run Claude Code Automatically Every Day with Windows Task Scheduler.
A System for Catching Failures
I structure this into three tiers:
- A morning overview. Run a script like the one above periodically (in my case, every 30 minutes) and compile the results into a single page, such as an HTML file. You can see the full picture at a glance.
- Record only failures. Create tasks in Notion only for items evaluated as failed. Because failures appear where you look every day, you will notice them even if you forget to open the overview page. Writing to Notion only required a small Python script using the standard library. If a periodically running monitor invokes an AI each time, it incurs costs on every run.
- Do not notify repeatedly for the same failure. If you run the check every 30 minutes, it will detect the same failure dozens of times in a single day. Notifying every time would create a mountain of tasks. Therefore, I record "which task was notified on which day" in a small file and skip subsequent notifications on the same day. If it remains unresolved the next day, another task is created.
Throttling notifications pays off when a failure persists. If there are too many alerts, you will overlook the one that truly matters.
Summary
- The real danger in automation isn't failure, but not noticing that tasks have stopped running
- Check three things: the last result code, the last run time, and whether the task is enabled or disabled
Get-ScheduledTaskandGet-ScheduledTaskInfolet you list status, last run time, and result codes- 0x41301 (running) and 0x41303 (has not run) are not failures. Result codes also appear in decimal, so cross-reference them against the table
- Before reporting a task as "stopped," verify that it wasn't stopped intentionally
- Record only failures, and limit notifications for the same failure to once per day
As system builders, the next thing to watch out for is automated tasks failing silently because a Python virtual environment or browser disappears unnoticed. The stories about A Python venv Inside OneDrive Can Break Your Scheduled Scripts and Playwright “Executable doesn't exist”: Why It Happens and How to Fix It are also examples of automation failing silently.