A Windows Task Scheduler health auditor. It finds scheduled tasks that are failing silently — the ones nobody notices until something downstream breaks — and explains exactly why, in plain English, with a suggested fix.
Read-only, always. SchtaskDoctor never enables, disables, deletes, or modifies a scheduled task. It only inspects and reports. See Read-only guarantee.
Scheduled tasks are one of the few pieces of infrastructure that can fail completely, repeatedly, and totally silently. A task with output redirected nowhere and history disabled can fail every single run for weeks and produce no signal at all — no email, no log, no alert. The task just quietly stops doing its job.
The failure mode that motivated this tool: a scheduled task was registered
with its action pointed at a bare command name — say python — instead of a
fully-qualified path. It ran fine when tested interactively, because the
person testing it had python on their shell's PATH. But Task Scheduler
doesn't run your interactive shell's environment — it runs in its own
default, non-interactive context, and that bare command name didn't resolve.
Every scheduled run failed instantly with 0x80070002 (file not found).
Nobody was watching, because nothing was set up to watch. The task ran, "did
its job" as far as Task Scheduler was concerned (a run happened, on schedule),
and quietly produced nothing for weeks.
That is the shape of bug SchtaskDoctor is built to catch before it costs you anything: a task that looks configured, looks scheduled, and is completely dead.
git clone https://github.com/AgentJDrew/schtask-doctor.git
cd schtask-doctor
Import-Module .\src\SchtaskDoctor\SchtaskDoctor.psd1Or copy src\SchtaskDoctor into a module path (e.g.
$env:UserProfile\Documents\WindowsPowerShell\Modules\SchtaskDoctor) and
Import-Module SchtaskDoctor normally.
Coming soon — Install-Module SchtaskDoctor is not yet published.
# Console report, color-coded by severity, non-Microsoft tasks only
Invoke-TaskAudit
# Markdown report to a file
Invoke-TaskAudit -Format Markdown -OutFile .\audit.md
# Only tasks under a specific folder, as JSON
Invoke-TaskAudit -TaskPath '\MyCompany\*' -Format Json -OutFile .\audit.json
# Include Microsoft's own scheduled tasks too
Invoke-TaskAudit -IncludeMicrosoft
# Deep-dive one task
Get-TaskHealth -TaskName 'Nightly Sync' -TaskPath '\MyApp\'Invoke-TaskAudit runs seven checks against every matched task. Each finding
carries a severity, a plain-English explanation, and a suggested fix.
| ID | Severity | What it catches |
|---|---|---|
FAILING |
Critical | LastTaskResult is a nonzero, non-benign result code. Common HRESULTs are decoded to friendly text: 0x80070002 (file not found), 0x80070005 (access denied), 0x8007010B (bad working directory), 0x800704DD (not logged on), 0x41301 (still running), 0x41303 (never ran), and more. |
UNRESOLVABLE-ACTION |
Critical | The action's executable is a bare name (e.g. python, no path) that would not resolve in Task Scheduler's default, non-interactive environment — or a qualified path that simply doesn't exist. This is the classic silent-0x80070002 shape described above. |
SILENT |
Warning | The action has no output redirection (>>, 2>&1, -LogFile, etc.) and task history is disabled — if it fails, nothing will ever record it. |
NEVER-RAN / STALE |
Warning | An enabled task with active triggers that has never run, or whose last run is far older than its trigger cadence implies (e.g. a daily task that hasn't run in 3+ days). |
OVER-PRIVILEGED |
Warning | RunLevel=Highest where the action doesn't obviously need elevation, or a task running as SYSTEM for what looks like a user-level job (touches a user-profile path). |
MISSING-WORKDIR |
Info | No WorkingDirectory is set while the action's arguments reference relative-looking paths — a common cause of "works when I run it, fails when scheduled." |
ORPHAN-SCHEDULE |
Info | A disabled task that still has a future-dated trigger — informational, but easy to mistake for "this is still active" at a glance. |
# SchtaskDoctor Audit Report
Generated: 2026-01-15 09:00:00
Tasks scanned: **12** | Findings: **4**
## Critical (2)
| Check | Task | Message | Suggested Fix |
|---|---|---|---|
| FAILING | `\MyApp\Nightly Sync` | Last run failed with 0x80070002: ERROR_FILE_NOT_FOUND ... | Open the task's History tab (enable it if off) or Event Viewer... |
| UNRESOLVABLE-ACTION | `\MyApp\Nightly Sync` | Action 'python' is a bare command name that does not resolve... | Replace with the fully-qualified path to the executable... |
## Warning (1)
| Check | Task | Message | Suggested Fix |
|---|---|---|---|
| SILENT | `\MyApp\Nightly Sync` | Action 'python' has no output redirection and task history is disabled... | Enable task history, or wrap the action in a script that redirects stdout/stderr... |
## Info (1)
| Check | Task | Message | Suggested Fix |
|---|---|---|---|
| ORPHAN-SCHEDULE | `\MyApp\Old Cleanup Job` | Task is disabled but still has trigger(s) scheduled in the future... | If permanently retired, delete it or remove its triggers... |(Synthetic data — not a real audit result.)
SchtaskDoctor never calls anything that changes Task Scheduler state. It
only calls Get-ScheduledTask and Get-ScheduledTaskInfo (wrapped so they
can be mocked in tests), plus read-only filesystem and environment-variable
checks to see whether an action's executable would resolve. There is no
Enable-, Disable-, Set-, Unregister-, or Register- anywhere in this
module. Audit away — it cannot break anything.
- Windows PowerShell 5.1 — primary target; all module code is written to run on 5.1 without changes.
- PowerShell 7+ — works as well; the
ScheduledTasksmodule ships with Windows and is available to both hosts. - Requires Windows (Task Scheduler is a Windows-only concept). Not tested on PowerShell for Linux/macOS.
# Run tests (Pester 5)
Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0 -Force
Import-Module Pester -MinimumVersion 5.0
Invoke-Pester -Path .\tests
# Lint
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
Invoke-ScriptAnalyzer -Path .\src\SchtaskDoctor -Recurse -Settings .\PSScriptAnalyzerSettings.psd1Every check function is a pure function over a plain "task record" object
(see src/SchtaskDoctor/Private/ConvertTo-TaskRecord.ps1) — none of them call
Get-ScheduledTask/Get-ScheduledTaskInfo directly. Those two cmdlets are
isolated behind wrapper functions
(src/SchtaskDoctor/Private/Get-TaskScheduleData.ps1) specifically so Pester
can mock them and the whole check suite can run against hand-built synthetic
tasks with no live Task Scheduler dependency.
MIT — Copyright (c) 2026 Andrew Lazzeroni