Automating Windows with Task Scheduler
How Task Scheduler's triggers, actions, and conditions work together, and how to build and inspect scheduled tasks from the command line.
Task Scheduler is Windows’ native answer to cron — but its model is considerably richer, built around independently composable triggers, actions, and conditions rather than a single time expression. Understanding that separation is the key to using it for anything beyond “run this every day at 9am.”
The three building blocks
A scheduled task is defined by combining three things: one or more triggers (what causes the task to run — a time, a logon, a specific event log entry, an idle period), one or more actions (what actually happens — run a program, send an email, in older versions), and conditions (additional gating logic — only run on AC power, only if the network is available, only if idle for N minutes).
$trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM
$action = New-ScheduledTaskAction -Execute "C:\Scripts\backup.ps1"
$settings = New-ScheduledTaskSettingsSet -RunOnlyIfNetworkAvailable
Register-ScheduledTask -TaskName "NightlyBackup" -Trigger $trigger -Action $action -Settings $settings
Trigger types beyond a simple schedule
Time-based triggers (Daily, Weekly, AtLogOn, AtStartup) are the obvious ones, but Task Scheduler can also trigger directly off Event Log entries — a task can fire the moment a specific Event ID appears in any Windows event channel, which is a genuinely powerful automation primitive with no direct cron equivalent:
$class = Get-CimClass MSFT_TaskEventTrigger -Namespace Root/Microsoft/Windows/TaskScheduler
$trigger = New-CimInstance -CimClass $class -ClientOnly -Property @{
Subscription = '<QueryList><Query Id="0"><Select Path="System">*[System[(EventID=7034)]]</Select></Query></QueryList>'
}
That example fires whenever Event ID 7034 (a service crashing unexpectedly) is logged — the basis for a huge range of “detect this specific condition and react automatically” automations.
Where task definitions actually live
Every scheduled task is stored as an XML file under %SystemRoot%\System32\Tasks, mirrored by a registry entry under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache — inspecting the raw XML is often the fastest way to understand exactly what a task does when the GUI’s summary view is too condensed:
Get-ScheduledTask -TaskName "NightlyBackup" | Export-ScheduledTask
Running as a specific account, with or without a logged-in session
A task’s principal determines what account it runs as and whether it runs whether or not a user is logged in — the setting that trips people up most often, since a task configured to “run only when user is logged on” silently does nothing on an unattended server:
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Set-ScheduledTask -TaskName "NightlyBackup" -Principal $principal
Inspecting task history and last-run results
Every execution logs a result code, and Task Scheduler keeps a history view (Event Log-backed) for auditing whether scheduled automation is actually running as expected:
Get-ScheduledTaskInfo -TaskName "NightlyBackup"
Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" -MaxEvents 20
A LastTaskResult of 0 means success; any other value is a Win32 error code worth looking up directly (0x1 is a generic failure, 0x41306 means “task is currently running,” and so on).
Command-line management with schtasks
For scripting environments without PowerShell’s cmdlets available, the older schtasks.exe covers the same ground:
schtasks /create /tn "NightlyBackup" /tr "C:\Scripts\backup.ps1" /sc daily /st 02:00
schtasks /query /tn "NightlyBackup" /v /fo LIST
schtasks /run /tn "NightlyBackup"
Comparing this to cron and launchd
Task Scheduler’s trigger/action/condition separation is genuinely more expressive than a plain cron line or a launchd StartCalendarInterval/WatchPaths pair — it can react to arbitrary event log conditions, chain multiple actions per task, and apply power/network/idle gating without any extra scripting. The trade-off is complexity: a task’s actual behavior is spread across triggers, actions, conditions, and settings independently, which is exactly why exporting a task’s XML (or reading it via Get-ScheduledTask | Select -ExpandProperty Actions/Triggers/Settings) is often faster than trying to reconstruct its full behavior from the GUI’s tabs alone.