How cron maps to TOML
This is the reference page for both routes into RunWisp — what
runwisp import cron writes out, and equally how a job read live through
include_cron behaves. For the narrative
version, start with Replacing cron or
Converting crontabs instead.
The mapping table
Section titled “The mapping table”Most of a crontab translates one-to-one.
Syntax
Section titled “Syntax”What the importer reads out of the file, line by line:
| crontab | runwisp.toml | Notes |
|---|---|---|
30 2 * * * cmd |
cron = "30 2 * * *" + run |
The five fields are copied verbatim. |
@daily, @hourly, @weekly |
cron = "@daily" … |
RunWisp speaks the same descriptors. |
@midnight, @annually |
@daily, @yearly |
Normalized to RunWisp’s spelling. |
@reboot cmd |
run_on_start = true + run |
Runs once each time the daemon starts. |
| user column (system crontab) | user = "..." |
Detected automatically, @reboot lines too. |
SHELL=/bin/bash |
per-task shell |
Must be an absolute path. |
PATH=…, other VAR=val |
per-task env |
Copied onto each task from that crontab. |
CRON_TZ=… / TZ=… |
per-task timezone |
Scoped to that crontab; a bad zone gets a TODO. |
# a comment above a job |
description = "..." |
The comment becomes the task’s description. |
\% in a command |
a literal % in run |
Cron’s escape, not the shell’s — unwrapped. |
cmd % input |
run = "cmd" + a # TODO |
Cron piped input on stdin; see below. |
MAILTO=… |
a sendmail notifier |
Handed to you as a paste-able block — see below. |
Execution model
Section titled “Execution model”These aren’t in the file — they’re how cron behaves, written out explicitly so a moved job keeps behaving that way:
| cron’s behaviour | runwisp.toml | Notes |
|---|---|---|
| a nearly empty environment | env_base = "clean" |
Set on every imported task — see below. |
jobs run in $HOME |
working_dir = "~" |
The home of whoever the job runs as. |
| a missed tick is simply gone | catch_up = "skip" |
Cron never re-fires one. Still recorded as missed. |
| overlapping copies of a job | on_overlap = "queue" |
Deliberately not cron’s pile-up — see below. |
Crontab-wide settings become per-task values rather than daemon-wide ones.
A crontab’s SHELL, CRON_TZ/TZ, and top-of-file VAR=val lines are copied
onto each task the importer emits from that file, so importing a second crontab
with different values can’t silently reach back and change the first one’s
tasks — and nothing an import writes touches your global [defaults] or
[scheduler].
Environment and shell
Section titled “Environment and shell”Every imported task gets env_base = "clean"
and working_dir = "~", because
cron and RunWisp default differently and your jobs were written for cron’s rules:
- Environment. Cron hands a job a nearly empty environment (
PATH=/usr/bin:/bin,SHELL,HOME,LOGNAME); RunWisp otherwise inherits the daemon’s.env_base = "clean"keeps cron’s. Your crontab’s ownPATH=line still lands in the task’senvand layers on top, so nothing you set explicitly is lost. - Working directory. Cron runs each job in the running user’s home; RunWisp
otherwise uses the directory it was started in — so a relative path in your script
would write somewhere else.
~means the home of whoever the task runs as: the daemon’s account for a per-user crontab, the user column’s for a system one.
Timing: catch-up and overlap
Section titled “Timing: catch-up and overlap”These two are about when a job fires — the one thing a migration mustn’t change quietly — so both are written out explicitly, each one line to change if you’d rather have RunWisp’s behaviour:
catch_up = "skip". Cron has no concept of a missed tick; one that arrives while nothing is listening is just gone. RunWisp normally catches up — starting the daemon at 15:00 would run last night’s 02:00 backup as a normal run.skipkeeps cron’s behaviour, while still recording the gap as a missed run you can see. Delete the line for catch-up.on_overlap = "queue". When a job outruns its own schedule, cron starts another copy; RunWisp queues the next instead — the unbounded pile-up is why cron jobs end up wrapped inflock. If yours genuinely relied on running in parallel, change this and reach formax_concurrent.
RunWisp’s cron grammar is a superset of vixie-cron’s: the standard five fields,
descriptors like @daily, and a few extras (six-field specs with seconds,
@every 30s). How scheduling works has the details.
What needs a human
Section titled “What needs a human”The importer never drops anything silently — anything it can’t map cleanly
shows up as a # TODO in the file and a note in the summary. The usual ones:
-
MAILTO. The summary hands you asendmailnotifier addressed to whoever the crontab named, ready to paste into your rootrunwisp.toml. It goes through the same local MTA cron used, so there’s no relay host or password to find. It’s a note rather than something the import writes, because a notifier is a daemon-wide setting and an import shouldn’t reach into those.Two things to know. First, you still have to point the tasks you want mail from at it with
notify_on_failure— the notifier alone sends nothing. Second, cron mailed any output at all; RunWisp mails an event. A job that printed a line and exited 0 used to generate mail and now doesn’t. That’s usually the improvement people wanted from cron mail, but it is a different rule — addnotify_on_successif you really did want the chatty version.If the box has no MTA, or you’d rather point RunWisp straight at Gmail/SES/Postmark,
type = "smtp"is the same notifier with relay settings instead. -
@reboot. Mapped torun_on_start, which fires once each time the daemon starts (not the machine). That’s usually what you wanted; double-check if you were relying on true boot timing. -
A
%that fed a job input. An unescaped%ends a crontab command; everything after it is what cron pipes to the job on stdin. RunWisp has no stdin field, so the command is imported cut at the%— exactly what cron ran — and the input it can’t reproduce is quoted in a# TODOon therunline. Fold it back into the command yourself, usually with a here-doc or aprintf … | cmd. -
A system crontab line with no user column. In
/etc/crontaband/etc/cron.d/*the sixth field is the user to run as, and a line written without one is a mistake cron itself skips. RunWisp skips it too rather than guessing — reading the first word of the command as a username would give you a task that runs the wrong command as a user who doesn’t exist. It applies the same test cron does: the sixth field has to name an account that exists on the box, so* * * * * echo "tick"is declined even thoughechois a perfectly good-looking username. The line gets a!row quoting itself; add the user (or create the account) and re-import. A crontab piped in from another machine is judged on shape alone — the accounts it names aren’t this box’s to check. -
The task name. Names are derived from the command (
/usr/local/bin/backup.sh→backup). They’re just labels — rename any that aren’t obvious, and the full command is preserved either way.
What you get that cron never gave you
Section titled “What you get that cron never gave you”- History. Every fire is a run with a start time, duration, exit code, and captured output — browsable in the Web UI and TUI, queryable over REST.
- Failure that’s loud. A non-zero exit (or a missed tick while the daemon was down) can notify you instead of vanishing.
- Overlap control. A slow job that’s still running when its next tick arrives no longer silently double-runs — pick queue, skip, or terminate.
- Manual runs. Trigger any job by hand from the dashboard or
runwisp exec <name>without editing the schedule.