-
Notifications
You must be signed in to change notification settings - Fork 0
Importing Task Scheduler
cronstable import-taskscheduler converts Windows Task Scheduler XML
exports into cronstable jobs, so an existing estate does not have to be
retyped.
It is a one-shot converter, not a loader. It writes YAML you read, edit and commit; cronstable never reads Task Scheduler XML at run time. The main reason is that exporting a task does not unregister it, so an export describes tasks Task Scheduler is still running. Loading the output without reviewing it first means both schedulers run the same work.
Whole machine, in one file:
schtasks /query /XML ONE > tasks.xml
cronstable import-taskscheduler tasks.xml -o jobs.yamlOne task, or a folder of them, from PowerShell:
Export-ScheduledTask -TaskName "Nightly Backup" -TaskPath "\Contoso\" |
Out-File -FilePath backup.xml
cronstable import-taskscheduler backup.xml -o jobs.yamlA directory of exports, or a pipe:
cronstable import-taskscheduler C:\exports -o jobs.yaml
schtasks /query /XML ONE | cronstable import-taskscheduler - -o jobs.yamlTwo export quirks are handled for you rather than left to trip you up.
schtasks /query /XML without ONE writes one XML declaration per task
inside a single root, which is not well-formed XML; those declarations are
stripped. And the declaration routinely lies about the encoding, because
Export-ScheduledTask returns a string stamped UTF-16 that PowerShell then
writes as UTF-8; the encoding is decided from the bytes instead.
The converted configuration goes to stdout, or to -o FILE. A report of
everything that could not be carried across goes to stderr, so the two can be
separated:
cronstable import-taskscheduler tasks.xml -o jobs.yaml 2> report.txt
cronstable -v -c jobs.yaml| Flag | Meaning |
|---|---|
-o, --output FILE
|
Write the configuration here instead of stdout. |
--timezone NAME |
Evaluate every converted schedule in this IANA zone. |
Exit 0 when something was converted, 1 when the input could not be read
or nothing usable came out, 2 for a usage error. There is deliberately no
separate code for a partial conversion, because on a whole-machine export a
partial conversion is the normal outcome.
This is the part worth knowing before the first run. Measured on a stock Windows 11 machine, of 195 registered tasks:
- 111 act through a COM handler rather than a command line;
-
97 use
WnfStateChangeTrigger, an internal Windows notification; - 57 have no trigger at all, and are launched on demand or by another task;
- 30 fire on logon, 12 on an event-log event, 11 on a session change.
24 converted. That is not the tool failing; almost all of those tasks are Windows' own internal plumbing, which has no business in a cron scheduler. On a folder of tasks somebody actually wrote, the ratio is very different. Every task that does not convert gets a line in the report saying which element stopped it and, where one exists, what to do instead.
| Task Scheduler | cronstable |
|---|---|
TimeTrigger |
a one-shot M H D Mo * YYYY schedule |
TimeTrigger with Repetition
|
the repetition's minute and hour, with the date columns cleared |
CalendarTrigger / ScheduleByDay, interval 1 |
M H * * * |
| ...interval 7 |
M H * * <weekday>, exact, because seven divides the week |
ScheduleByWeek, interval 1 |
M H * * mon,fri |
ScheduleByMonth |
M H <days> <months> *, with Last becoming L
|
ScheduleByMonthDayOfWeek |
M H * <months> tue#2, with Last becoming L2
|
BootTrigger |
@reboot |
Exec action |
a list-form command, split by the Windows argument rules |
WorkingDirectory |
workingDirectory |
ExecutionTimeLimit |
executionTimeout, in seconds |
MultipleInstancesPolicy |
concurrencyPolicy: IgnoreNew to Forbid, Parallel to Allow, StopExisting to Replace
|
Priority |
priority, by the documented band |
Enabled false, on the task or a trigger |
enabled: false |
A task with more than one trigger becomes one job per trigger, suffixed
-t2, -t3.
Nothing is dropped silently. Everything below is a line in the report, naming the element responsible.
Six trigger types have no cron equivalent at all: logon, idle, event-log, session change, registration, and the internal notification trigger.
Three kinds of calendar schedule cannot be written as a cron expression
either. A day interval other than 1 or 7 is one, because a cron day-of-month
step restarts each month: "every 3 days" would fire on the 1st, the 4th and
so on to the 31st, then the 1st again, leaving a one-day gap. Week intervals
above 1 fail for a related reason, since cron has no week-of-year phase. In
both cases you can run the job daily or weekly and gate it on a durable
cronstable cursor.
Repetitions are the third: a cron minute field and hour field multiply, so a
repetition converts only when its occurrences over a day are exactly that
product. PT1H becomes 0 * * * *, while PT90M does not, because the
product would include times it never fires at, and widening it would double
the job's rate. A repetition with a bounded Duration is refused on the
same grounds.
Actions cronstable cannot run are reported too. A COM handler, an e-mail and a message box have no command line between them. A task with more than one action does convert, but its jobs are written out commented, because Task Scheduler runs a task's actions in sequence inside one instance while separate cronstable jobs on one schedule run at once. Chain them as a DAG if the order matters.
UserId, GroupId and RunLevel are reported and never written as user:
or group:. Those keys are a config-load error on Windows, so emitting them
would produce a file that cannot load on the platform it was converted for.
cronstable runs every job as the account the daemon runs as.
What is left is settings with no counterpart. cronstable schedules on time
and does not test machine state, so run only if idle, only on AC power, only
when a network is available and wake to run are all reported;
RestartOnFailure is the one of those with a near equivalent worth naming,
onFailure.retry. RandomDelay has none.
Hashed schedules spread jobs deterministically, which is
usually what the delay was for, but it is not the same thing. Nor does
EndBoundary, since a cronstable schedule has no end date.
A command line containing %SystemRoot% or any other %VAR% is emitted as a
single string rather than as a command list. Task Scheduler expands
environment variables itself before it launches anything; cronstable does not,
and a list is passed to the OS as an argv, so %windir%\system32\foo.exe
would be looked up as a file with a percent sign in its name and fail on every
run. A string command with no shell set runs through %ComSpec% /c on
Windows, which performs the same expansion, at run time on the target machine
rather than baking this machine's values in. On the 195-task export above, 18
of the 31 emitted jobs took this form, and the report notes each one.
The program is quoted on the way out even when the original was not. Task
Scheduler decides quoting on the literal text it stored, and it calls
CreateProcess rather than a shell, so a path that only gains its space once
the variable expands is stored bare. %ProgramFiles%\App\app.exe handed to
cmd.exe unquoted runs C:\Program.
Two things change with the shell in the middle. &, |, <, > and ^ in
the arguments were text to Task Scheduler and are operators to cmd.exe, so
check any job whose arguments contain them. And the job runs cmd.exe as its
own process with the real program underneath it, which is worth knowing if you
also set priority: Windows only
propagates a lowered priority class to children.
Three labels appear in the report. NOT CONVERTED means no job was emitted
for that task, or that its jobs were written commented out. PARTIAL means
jobs were emitted and are live in the file, but something else about the task
did not carry across, usually one trigger of several. note means the jobs
are complete and something is worth reading anyway.
One task that cannot be converted does not stop the others. A duration or timestamp the converter cannot read becomes a blocking note for that task, so the rest of the export still converts and still gets written.
Seconds are always dropped. A StartBoundary carries a seconds field and a
registration artefact like :38 is common, but a cronstable schedule with a
seconds column makes the whole daemon wake every second, for every other job
on the box as well. That is an expensive thing to import by accident. Add
the column by hand to the one job that genuinely needs it.
ExecutionTimeLimit: PT0S means no limit in Task Scheduler, while cronstable
requires executionTimeout to be greater than zero, so it is emitted as
nothing rather than as zero. Mapping it literally would have made 34 of those
195 tasks fail to load.
Task Scheduler means machine local time unless the boundary says otherwise,
so a naive start time becomes utc: false, and one already in UTC emits
nothing. A stored numeric offset such as -04:00 is reported rather than
converted: it is not an IANA zone name, so it cannot be written as
timezone:, and inferring one would be a guess about daylight saving. Name
the real zone with --timezone if you want one.
A one-shot whose instant has passed still loads. cronstable reports it as
never-fires on /status and /jobs rather than refusing it, so a lapsed
TimeTrigger shows up as a job to delete instead of as a load failure.
A task's URI becomes the job name, with the folder separator becoming . and
spaces becoming -, so \Contoso\Nightly Backup becomes
Contoso.Nightly-Backup, and a collision gets a numeric suffix. One
consequence is worth knowing: a <folder>.<task> name looks exactly like a
DAG task name, so if you later add a DAG named after a Task Scheduler folder,
the load refuses the pair.
A directory scan skips what it does not recognise. .xml is a name half the
tooling on a Windows box writes, so a stray file in a scanned directory is a
report line and a skip. A file you name on the command line is not: that is a
mistake worth stopping for.
- Running on Windows
- Classic Crontabs for the other migration path
- Schedules and Timezones
- Configuration Reference
- CLI Reference
This wiki documents cronstable. See the README and the changelog.
cronstable is a fork of gjcarneiro/yacron.
cronstable™ and the cronstable logo are trademarks of Parker Loflin; the code is MIT-licensed (see TRADEMARKS.md and LICENSE).
- Getting Started
- Configuration
- Job Behavior
-
Integrations
- Reporting (Mail, Sentry, Shell, Webhook)
- Push Notifications
- Windows Event Log
- Metrics with Prometheus
- Metrics with statsd
- HTTP Control API
- LAN Discovery (Bonjour/mDNS)
- Listener TLS
- Calendar Export (iCal)
- Schedule Pressure
- Duplicate Schedule Detection
- Suggest a Slot
- Why Didn't It Run?
- Web Dashboard
- Terminal Dashboard
- MCP Server (Model Context Protocol)
- Reference and Development