> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Windows Task Scheduler from PowerShell — Cron for Windows
- URL: https://blog.sudowl.com/windows-task-scheduler-powershell-automation/
- Published: 2026-06-11T10:00:00.000Z
- Updated: 2026-10-11T08:26:51.000Z
- Description: Create and manage scheduled tasks on Windows from PowerShell — New-ScheduledTaskAction, triggers, run as SYSTEM, S4U logon, XML export, and the gotchas.
- Author: Lukas
- Tags: Windows Server

This is cron for Windows, and it's clunkier than cron. The Task Scheduler GUI is a maze of tabs, but the `ScheduledTasks` PowerShell module turns a recurring job into four objects you compose and register — repeatable, scriptable, no clicking.

**TL;DR:**

```powershell
$action  = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\job.ps1" `
    -WorkingDirectory "C:\scripts"
$trigger = New-ScheduledTaskTrigger -Daily -At 3:00am
$princ   = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
Register-ScheduledTask -TaskName "Nightly-Job" -Action $action -Trigger $trigger `
    -Principal $princ -Description "Runs job.ps1 every night at 03:00"

```

Details below.

## The four pieces

A registered task is built from four object types. You make each one, then hand them all to `Register-ScheduledTask`:

- **Action** — what runs (`New-ScheduledTaskAction`)
- **Trigger** — when it runs (`New-ScheduledTaskTrigger`)
- **Principal** — who it runs as (`New-ScheduledTaskPrincipal`)
- **Settings** — optional tuning (`New-ScheduledTaskSettingsSet`)

Compose, register, done. Compared to a one-line crontab entry this is verbose, but the upside is each piece is a real object you can inspect and reuse.

## The action — run a script

Don't point the action at your `.ps1` directly. Point it at `powershell.exe` and pass the script as an argument. That way `-NoProfile` and the execution policy override actually apply:

```powershell
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\backup.ps1" `
    -WorkingDirectory "C:\scripts"

```

`-NoProfile` matters. SYSTEM and service accounts can have profile scripts that hang or change your environment. Skip them. `-WorkingDirectory` matters too — without it the task runs from `C:\Windows\System32`, and every relative path in your script breaks. Set it explicitly or use full paths everywhere. Use both, honestly.

## Triggers — daily, startup, every N minutes

The three I reach for:

```powershell
# Every day at a fixed time
New-ScheduledTaskTrigger -Daily -At 3:00am

# At boot (no logged-in user needed)
New-ScheduledTaskTrigger -AtStartup

# Every 15 minutes, forever — cron's */15 equivalent
$t = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Minutes 15) `
    -RepetitionDuration ([TimeSpan]::MaxValue)

```

The every-N-minutes pattern is the awkward one. There's no `-Every` flag — you start a `-Once` trigger now and attach a repetition interval. `[TimeSpan]::MaxValue` means "run indefinitely." This is the closest thing to a `*/15 * * * *` crontab line, and it takes three parameters to say it.

You can pass multiple triggers to one task — just give `-Trigger` an array.

## The principal — run as SYSTEM, highest privileges

Who the task runs as. For unattended jobs that need to run whether or not anyone is logged in, `SYSTEM` is the cleanest — it's a local account with no password to store or rotate:

```powershell
$princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

```

`-RunLevel Highest` runs elevated (past UAC). Without it, an admin-context script silently fails on anything that needs elevation.

If the job must run as a **specific domain user** — say it touches a network share that SYSTEM can't reach — you have two logon types, and this is the gotcha that bites everyone:

```powershell
# S4U: run whether logged on or not, NO stored password
$princ = New-ScheduledTaskPrincipal -UserId "DOMAIN\svc_backup" `
    -LogonType S4U -RunLevel Highest

# Password: needs creds, can access network resources
Register-ScheduledTask -TaskName "Net-Job" -Action $a -Trigger $t `
    -User "DOMAIN\svc_backup" -Password "P@ss" -RunLevel Highest

```

**The exception:** `S4U` (service-for-user) lets a task run without a stored password — great, because you never have to update the task when the account's password rotates. But S4U tokens **cannot authenticate to other machines**. If your script reads `\\fileserver\share`, S4U gives you access-denied. For network access you need `-LogonType Password` with real credentials, which means storing a password the task vault has to keep and you have to rotate. SYSTEM sidesteps all of this for local-only work, which is why I default to it.

## Register it

Everything comes together here:

```powershell
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -DontStopOnIdleEnd -ExecutionTimeLimit (New-TimeSpan -Hours 2)

Register-ScheduledTask -TaskName "Nightly-Backup" `
    -Action $action -Trigger $trigger -Principal $princ `
    -Settings $settings -Description "restic backup, runs 03:00 daily" -Force

```

`-StartWhenAvailable` runs a missed job once the box comes back up (the closest thing to anacron). `-ExecutionTimeLimit` kills a hung job — set it, or a stuck task blocks the next run forever. `-Force` overwrites an existing task of the same name, which makes the whole script idempotent: re-run it and the task is updated in place.

## Capture output — Task Scheduler history is weak

Task Scheduler's own history is terse and **disabled by default**. Don't rely on it. Make the script log itself:

```powershell
# top of job.ps1
Start-Transcript -Path "C:\logs\job_$(Get-Date -f yyyyMMdd_HHmmss).log"
try {
    # ... work ...
    exit 0
} catch {
    Write-Error $_
    exit 1
} finally {
    Stop-Transcript
}

```

The task records your script's exit code in `LastTaskResult` — but only if the script actually sets one. `exit 0` for success, non-zero for failure. Without explicit exits you get `0x0` for everything and can't tell a real success from a silent crash.

## Manage running tasks

```powershell
# List (filter by folder path)
Get-ScheduledTask -TaskPath "\" | Where-Object State -ne "Disabled"

# Did it run? When's next? What did it return?
Get-ScheduledTaskInfo -TaskName "Nightly-Backup" |
    Select-Object LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns

# Fire it manually, right now
Start-ScheduledTask -TaskName "Nightly-Backup"

# Toggle
Disable-ScheduledTask -TaskName "Nightly-Backup"
Enable-ScheduledTask  -TaskName "Nightly-Backup"

# Remove
Unregister-ScheduledTask -TaskName "Nightly-Backup" -Confirm:$false

```

`Get-ScheduledTaskInfo` is the one I run most. `LastTaskResult` is your exit code; `0` is success. `NextRunTime` confirms the trigger fires when you think it does.

## Export / import as XML

For repeatability across servers, dump a task to XML and re-register it elsewhere. This is how I move a task from a test box to prod without re-typing anything:

```powershell
# Export
Export-ScheduledTask -TaskName "Nightly-Backup" |
    Out-File C:\scripts\nightly-backup.xml -Encoding utf8

# Import on another machine
$xml = Get-Content C:\scripts\nightly-backup.xml -Raw
Register-ScheduledTask -TaskName "Nightly-Backup" -Xml $xml `
    -User "SYSTEM"

```

The XML is the full task definition — triggers, action, principal, settings. Commit it to your config repo and the task becomes infrastructure-as-code, the same way I treat my [server setup checklist](https://blog.sudowl.com/windows-server-initial-setup-checklist/).

## Gotchas

- **`-NoProfile` always.** Service accounts with profile scripts will surprise you.
- **Full paths everywhere.** The task runs from `System32`, not your script's folder. Set `-WorkingDirectory` *and* use absolute paths.
- **Exit code `0x41303`** \= "task has not yet run." Not an error — the trigger just hasn't fired. `Start-ScheduledTask` to test immediately instead of waiting.
- **History is off by default.** Enable it once if you want the event-log trail: `wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true`. But still log in-script.
- **S4U can't hit the network.** Covered above — the single most common "why does it fail only as a task" cause.
- **`-RunLevel Highest`** or your elevated steps fail silently.

## Verify

After registering, confirm the task is real and the trigger is sane:

```powershell
Get-ScheduledTask -TaskName "Nightly-Backup" | Format-List TaskName, State
Get-ScheduledTaskInfo -TaskName "Nightly-Backup" |
    Format-List LastTaskResult, NextRunTime

# Force a run and watch the log
Start-ScheduledTask -TaskName "Nightly-Backup"
Get-Content C:\logs\job_*.log -Tail 20 -Wait

```

If `NextRunTime` is populated and a manual `Start-ScheduledTask` produces a log with `exit 0`, you're done.

## Versus cron and systemd

cron is one line in a crontab and you're finished — terser by far. systemd timers split the *what* (a `.service`) from the *when* (a `.timer`), much like Task Scheduler splits action from trigger; if you've written a [systemd service unit](https://blog.sudowl.com/systemd-service-unit-file/) the two-object model here will feel familiar. Where Task Scheduler genuinely wins is the principal model — SYSTEM, S4U, and run-whether-logged-on-or-not are first-class, where on Linux you're reaching for service accounts and `User=` directives to get the same.

It's more verbose than cron at every step. But it's scriptable, the objects are inspectable, and the XML export makes tasks portable. Wire these cmdlets into your provisioning and Windows scheduling stops being a GUI chore. For the rest of the cmdlets I lean on daily, see my [PowerShell cheatsheet](https://blog.sudowl.com/powershell-sysadmin-cheatsheet/).

---

### Related posts

- [wbadmin — Free Bare-Metal Backup on Windows Server](https://blog.diengdoh.com/wbadmin-windows-server-bare-metal-backup/?ref=blog.sudowl.com)
- [Hosting a Website on IIS — Windows Server Without the Bloat](https://blog.diengdoh.com/iis-host-website-windows-server/?ref=blog.sudowl.com)
- [Hyper-V on Windows Server — Your First VM Without the GUI](https://blog.diengdoh.com/hyper-v-create-first-vm-windows-server/?ref=blog.sudowl.com)