Skip to content

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.

Most of a crontab translates one-to-one.

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.

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].

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 own PATH= line still lands in the task’s env and 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.

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. skip keeps 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 in flock. If yours genuinely relied on running in parallel, change this and reach for max_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.

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 a sendmail notifier addressed to whoever the crontab named, ready to paste into your root runwisp.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 — add notify_on_success if 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 to run_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 # TODO on the run line. Fold it back into the command yourself, usually with a here-doc or a printf … | cmd.

  • A system crontab line with no user column. In /etc/crontab and /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 though echo is 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.shbackup). They’re just labels — rename any that aren’t obvious, and the full command is preserved either way.

  • 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.