Skip to content

From cron

There are two ways to move cron jobs to RunWisp:

  • Take over (Linux with systemd): RunWisp reads your crontabs where they are and replaces the cron service. crontab -e keeps working.
  • Convert by hand (any system): runwisp import cron turns a crontab into TOML, and you disable the old cron lines yourself.

Either way, you can later promote any job into your own runwisp.toml.

Terminal window
sudo runwisp takeover

You don’t need a runwisp.toml first. takeover finds your crontabs, shows a plan, and asks once. Nothing is written until you say yes.

  1. Writes the config: a runwisp.toml that reads your crontabs with include_cron. In an existing config it adds the key and leaves comments and formatting alone.
  2. Installs the service: /etc/systemd/system/runwisp.service, then systemctl daemon-reload.
  3. Stops and masks cron: stopped first, so nothing runs in between.
  4. Starts RunWisp: systemctl enable --now runwisp.service.

Cron isn’t touched until the config and unit are on disk. If RunWisp fails to start, cron is unmasked (and restarted if it was running), and you see the error.

If a crontab sets MAILTO, the plan warns that cron mail stops. Set up notifications to hear about failures; see MAILTO below.

Your crontabs are never rewritten. Running takeover again on a finished take-over does nothing and exits 0. Flags such as --dry-run and --yes are in the CLI reference.

Found 12 cron jobs on this box:
/etc/crontab, /etc/cron.d
1. Write /etc/runwisp/runwisp.toml
reads /etc/crontab, /etc/cron.d live via [daemon] include_cron
2. Install RunWisp as a system service and retire cron.service
write /etc/systemd/system/runwisp.service
systemctl daemon-reload
stop cron.service
mask cron.service
systemctl enable --now runwisp.service
Resolved settings:
Binary: /usr/local/bin/runwisp
Config: /etc/runwisp/runwisp.toml
Data dir: /var/lib/runwisp
Host: 127.0.0.1
Port: 9477
Take over from cron.service? [Y/n]

On a root box with cron jobs, running plain runwisp the first time offers the same take-over. Only a command you run in a terminal ever masks cron; runwisp daemon, systemd, and Docker never do.

While cron is still running, RunWisp holds the jobs it reads from cron’s files: they are listed with their schedule, but RunWisp doesn’t start them, so nothing runs twice. At boot, a HELD: banner names them.

  • Held tasks show ⏸ in runwisp status and the TUI, a held badge in the Web UI, and heldBy: "cron" in the API.
  • runwisp run <name> still runs a held job by hand, so you can try it under RunWisp first.
  • sudo runwisp takeover ends the hold. If you retire cron some other way, stop it and disable it at boot (for example sudo systemctl disable --now cron). RunWisp notices within a minute and starts running the jobs, no reload needed. A stopped cron that is still enabled keeps the jobs held, because it starts again on the next boot.

You never set a hold yourself. RunWisp checks for a running cron daemon on every load.

Terminal window
runwisp service uninstall

This unmasks and restarts cron, but only if this RunWisp’s unit recorded the take-over (a # runwisp-masked-cron: line in the unit). If something starts cron again later, runwisp service status reports it and RunWisp holds the jobs again within a minute. sudo runwisp takeover fixes the cron side.

sudo runwisp service install alone never retires cron. If cron still runs jobs, it tells you to use takeover.

runwisp import cron converts a crontab to TOML and prints it:

Terminal window
crontab -l | runwisp import cron # your own crontab
runwisp import cron /etc/crontab # a specific file

Given this crontab:

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin
# Nightly database backup
30 2 * * * /usr/local/bin/backup.sh --full

you get:

[tasks.backup]
description = "Nightly database backup"
cron = "30 2 * * *"
shell = "/bin/bash"
env_base = "clean"
working_dir = "~"
catch_up = 0 # cron never re-fires a missed tick
on_overlap = "queue" # cron would overlap instead of queueing
run = "/usr/local/bin/backup.sh --full"
[tasks.backup.env]
PATH = "/usr/local/bin:/usr/bin"

The extra keys keep cron’s behaviour; see How cron maps to RunWisp.

A summary goes to stderr, with one row per job:

Mark Meaning
✓ Mapped cleanly.
~ Imported, but something changed. The row says what.
! Needs a fix from you before the config loads.
- Skipped, because your runwisp.toml already defines it.

Notes about the whole file (such as MAILTO) are listed under Notes:. Anything that can’t be mapped gets a # TODO in the TOML. When it looks right, save and check it:

Terminal window
crontab -l | runwisp import cron -o runwisp.toml
runwisp validate

If you already have a runwisp.toml, use --write instead of -o; see Staging and promoting. All flags are in the CLI reference.

/etc/crontab and files in /etc/cron.d have a user column between the schedule and the command. The importer detects it from the path (or the # ... user command header) and sets the task’s user. For piped input, force it with --system or --system=false.

An import copies your jobs and never touches the crontab. If cron keeps running them, every job runs twice. Before you start RunWisp, remove the imported jobs from cron:

Terminal window
crontab -l > ~/crontab.backup # keep a copy
crontab -e # comment out the imported lines

For a file in /etc/cron.d, move it out of the directory:

Terminal window
sudo mv /etc/cron.d/myjobs /root/myjobs.crontab.backup

You can move jobs one at a time. Just never leave a job enabled in both cron and RunWisp.

import --write adds imported jobs next to a config you already have, without editing your own tasks. It works the same for import supervisord and import systemd.

Terminal window
crontab -l | runwisp import cron --write --dry-run # show what would change
crontab -l | runwisp import cron --write # do it

It uses two files:

runwisp.toml # yours: hand-written, keep it in git
runwisp.d/imported.toml # written by the importer
  • Your runwisp.toml is created, or its include is set to load runwisp.d/*.toml. If you already have your own include list, --write doesn’t change it; it prints the glob to add and writes nothing.
  • The importer only ever writes runwisp.d/imported.toml, so you can run it again safely.
  • A job your config already defines with the same command is skipped (-). A job with the same name but a different command is renamed (backup becomes backup-2).
  • Both files are written together. If anything fails, both are rolled back.
  • If the result wouldn’t load (any ! row), nothing is written and the command exits non-zero. Fix the line and run it again, or use -o to get a file you can edit by hand.

Imported tasks show "source": "staged" in runwisp list --json and runwisp status --json. They run like any other task.

promote moves a job into your own runwisp.toml:

Terminal window
runwisp promote backup # one job
runwisp promote --all # every staged and crontab-read job
  • From staging: the block moves from runwisp.d/imported.toml into your runwisp.toml as it is, comments and # TODO lines included. When the last job is promoted, the empty staging file is removed.
  • From a crontab read by include_cron: the job is copied into runwisp.toml, and its line in the crontab is commented out with a note saying where it went. That stops cron from running it if cron is still active. If the line changed since the config was loaded, promote refuses and writes nothing; try again.

After promoting, the job is yours: it has no source, and a later import doesn’t touch it.

Run runwisp reload afterwards, or pass --reload. If the job was already running under RunWisp (staged, or cron already retired), the reload reports no task changes. If it was held because cron was still running, the reload reports it as changed, and RunWisp starts scheduling it.

These rules apply to runwisp import cron and to jobs read live through include_cron.

crontab runwisp.toml Notes
30 2 * * * cmd cron = "30 2 * * *" + run Copied as is.
@daily, @hourly, @weekly cron = "@daily" … Same names.
@midnight, @annually @daily, @yearly Renamed to RunWisp’s spelling.
@reboot cmd run_on_start = "boot" Once per machine boot.
user column (system crontab) user Detected automatically.
SHELL=/bin/bash shell Must be an absolute path.
PATH=…, TZ=…, other vars env TZ is not a schedule zone in cron.
CRON_TZ=… timezone Only env on Debian/Ubuntu. An unknown zone gets a # TODO.
# comment above a job description
\% in a command a literal %
cmd % input run = "cmd" + a # TODO See below.
MAILTO=… a sendmail notifier, printed for you to add See below.

Crontab-wide settings (SHELL, CRON_TZ, variables) are copied onto each task from that file. An import never changes your [defaults] or [daemon].

Every imported task gets env_base = "clean" and working_dir = "~", because your jobs were written for cron:

  • Cron gives a job an almost empty environment. Without env_base = "clean", the job would get the daemon’s environment. Your crontab’s PATH= is still added on top.
  • Cron runs a job in the user’s home directory. Without working_dir = "~", the job would run in the directory the daemon started in, and relative paths would point somewhere else. ~ is the home of the user the task runs as.

Both are written out, so you can delete the line to get RunWisp’s behaviour:

  • catch_up = 0: cron never runs a job it missed while it was down. RunWisp normally catches up. The missed run is still recorded.
  • on_overlap = "queue": when a job is still running at its next time, cron starts a second copy. RunWisp waits for the first to finish instead. If your job must run in parallel, set max_concurrent.

For jobs read live through include_cron, you can’t edit these lines; promote the job first. RunWisp’s cron syntax also accepts a seconds field and @every 30s; see cron.

  • MAILTO. The summary prints a sendmail notifier for the address, to add to your runwisp.toml. It uses the same local mail setup cron used. Then add it to each task’s notify. Cron mailed any output; RunWisp mails on failure. Without a local MTA, use smtp instead.
  • @reboot. run_on_start = "boot" runs once per boot, but only once the daemon is up. If the daemon starts late, or only after the boot, the job runs then.
  • A % that feeds input. In cron, text after an unescaped % is sent to the job on stdin. RunWisp has no stdin setting, so the command is cut at the % and the input is quoted in a # TODO. Put it back into the command, for example with printf '…' | cmd.
  • A system crontab line without a user. Cron skips it, and so does RunWisp. The sixth field must name a user that exists on this machine, so * * * * * echo "tick" is skipped (there is no user echo). Add the user and import again. A crontab from another machine is checked by its shape only.
  • Task names. Names come from the command (/usr/local/bin/backup.sh becomes backup). Rename any that aren’t clear.