Your backup job runs at 3 a.m. At 9 a.m. your manager asks why last night's export is missing.
You open Task Scheduler and see it: Last Run Result: 0x1. The task failed hours ago and
nothing told you.
Windows used to tell you. Task Scheduler had built-in email alerts - then they were removed
in Server 2016 and never came back. Everyone reinvents the same broken workaround, so here is
the version that actually works in production.
What people usually paste (and why it breaks)
The typical Google result is a script that loops over tasks, calls
Get-ScheduledTaskInfo, and mails you when LastTaskResult -ne 0. Roughly:
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\Windows*' } | ForEach-Object {
$info = $_ | Get-ScheduledTaskInfo
if ($info.LastRunTime -gt (Get-Date).AddHours(-24) -and $info.LastTaskResult -ne 0) {
Send-MailMessage -To me@example.com -From taskwatch@example.com `
-Subject "Task failed: $($_.TaskName)" -SmtpServer smtp.example.com
}
}
It works once. Then it burns you in one of three ways.
Trap 1: the LastRunTime seconds quirk
Get-ScheduledTaskInfo.LastRunTime has a long-standing bug where the seconds value can
mirror the minutes value. It has been reported through Microsoft Feedback Hub more than once
and it is still there. If you compute "how old is this run" with full timestamp precision,
a task that ran 5 minutes and 5 seconds ago can look like it ran 5 minutes and 55 seconds
ago - and anything with a tight window starts flapping.
The fix is boring and reliable: do age math at minute granularity and give yourself a
two-minute tolerance:
$ageMin = ((Get-Date) - $info.LastRunTime).TotalMinutes
if ($ageMin -gt ($maxAgeHours * 60) + 2) {
# stale
}
A rounding quirk can never wake you up at 3 a.m.
Trap 2: the email flood
The naive script has no memory. It runs every 5 minutes, the task is still broken, and you
get 12 identical emails before your coffee. Worse, when the task recovers and fails again
next week, the "already alerted" logic you bolted on suppresses the new incident.
The correct model is a tiny state file keyed per watched item:
- new problem -> alert, store a signature of the problem
- same problem, within the re-alert window (24h) -> stay quiet
- same problem, past the window -> one reminder
- problem gone -> clear the state, so the next incident alerts like new
Signatures must describe the problem, not the moment. Do not include the timestamp of
the failure in the signature or every failed run becomes a fresh alert while the task keeps
failing every cycle.
Trap 3: watching only Task Scheduler
Half the jobs that "run themselves" are not scheduled tasks - they are SQL Agent jobs,
SSIS packages, cron-style loops inside services, or a script on another box. Task Scheduler
monitoring gives you a green light while the real job rots.
The trick that covers everything is a heartbeat file: one line appended by the job on
success, one configured maximum age by the watcher.
# at the end of ANY job, scheduled or not:
Add-Content -Path C:\Scripts\hb.txt -Value (Get-Date -Format o)
If the file is missing or older than N minutes, alert. That converts "is my job alive?"
into a filesystem check that works for anything that can write a file.
What I run in production
After getting tired of rebuilding this per server, I packaged it as TaskWatch - a local,
subscription-free watchdog:
- FAIL - watched task exited non-zero, exit code in the alert
- MISSED RUN - "must run every 25 hours" exceeded
- DISABLED / MISSING - someone turned your backup task off or deleted it
- HEARTBEAT STALE - the heartbeat trick, automated
- alerts via SMTP email, a Teams/Slack-style webhook, or the local log
- an HTML status page per machine, one bookmark
- one alert per problem, re-reminder after 24h, auto-clear on recovery
- nothing leaves your machine: no cloud, no account, no telemetry
One-command setup registers it as its own scheduled task every 5 minutes, and it can send
you a test email during install so you know alerts work before you need them:
.\Install-TaskWatch.ps1 -WatchList 'NightlyBackup','SqlMaintenance' -SmtpServer smtp.example.com -From tw@example.com -To you@example.com
The full version is $15, one-time, no subscription
(30-day refund): xennedelan.gumroad.com/l/taskwatch
Free sample
I kept a free, MIT-licensed version for people who just want the report. It prints every
task that failed on the machine in the last 24 hours with exit codes, and exits 1 if
anything failed - so you can drop it into any existing alerting that understands exit codes:
git clone https://github.com/OpsKit-Dev/powershell-admin-scripts
powershell -ExecutionPolicy Bypass -File .\powershell-admin-scripts\TaskWatch\TaskWatch-mini.ps1
Related
- 10 PowerShell scripts that catch Windows server problems before your users do - the scripts TaskWatch tells you to go run: disk cleanup, backup rotation, service watchdog, TLS expiry checks and more (OpsKit, $7)
- Offboarding in PowerShell - the checklist I run when someone leaves - AD user offboarding ($2)
If TaskWatch saves you one "why is the export missing" conversation, it has paid for itself
several times over.
Top comments (0)