Converting crontabs
runwisp import cron turns a crontab into a runwisp.toml you own, annotated
with anything that needed a human’s attention. There are two reasons you’d land
here, and they’re different:
- You already took over and want a job
as hand-written TOML in git. Skip to
Graduating a job into your own config
and
promoteit. Cron is gone, so nothing double-fires — the easy path. - You’re converting by hand — on macOS or a box without systemd, where
takeovercan’t retire cron for you, or because you just want a clean generated TOML from the start.import croncopies your jobs; you then retire the old crontab lines yourself.
The one-liner
Section titled “The one-liner”Pipe your current crontab in, or point it at a file:
crontab -l | runwisp import cron # whatever's installed for yourunwisp import cron /etc/crontab # a specific fileGiven a crontab like:
SHELL=/bin/bashPATH=/usr/local/bin:/usr/bin
# Nightly database backup30 2 * * * /usr/local/bin/backup.sh --full@reboot /opt/app/warmup*/5 * * * * /opt/healthcheck.shyou get an annotated runwisp.toml on stdout:
#:schema https://docs.runwisp.com/config.schema.json# Generated by `runwisp import`. Review the notes below, then# validate with `runwisp validate`.
[tasks.backup]description = "Nightly database backup"cron = "30 2 * * *"shell = "/bin/bash"env_base = "clean"working_dir = "~"catch_up = "skip" # cron never re-fires a missed tickon_overlap = "queue" # cron would overlap instead of queueingrun = "/usr/local/bin/backup.sh --full"
[tasks.backup.env]PATH = "/usr/local/bin:/usr/bin"
# @reboot — runs once each time the daemon starts.[tasks.warmup]run_on_start = trueshell = "/bin/bash"env_base = "clean"working_dir = "~"on_overlap = "queue" # cron would overlap instead of queueingrun = "/opt/app/warmup"
[tasks.warmup.env]PATH = "/usr/local/bin:/usr/bin"
[tasks.healthcheck]cron = "*/5 * * * *"shell = "/bin/bash"env_base = "clean"working_dir = "~"catch_up = "skip" # cron never re-fires a missed tickon_overlap = "queue" # cron would overlap instead of queueingrun = "/opt/healthcheck.sh"
[tasks.healthcheck.env]PATH = "/usr/local/bin:/usr/bin"Each task carries the crontab’s SHELL and PATH itself rather than a shared
[defaults] block, so it stands alone and your own global settings stay
untouched. The env_base, working_dir, catch_up and on_overlap lines
reproduce cron’s execution model, not the daemon’s — see
How cron maps to TOML. (warmup gets no
catch_up: a @reboot job has no ticks to miss.)
The summary
Section titled “The summary”On stderr — so it stays out of the TOML you’re piping somewhere — you get an account of every job:
Imported crontab → 3 tasks ✓ backup 30 2 * * * /usr/local/bin/backup.sh --full ✓ warmup @reboot /opt/app/warmup ✓ healthcheck */5 * * * * /opt/healthcheck.sh
Notes: ! crontab sets [email protected]. crond mails a job's output; RunWisp notifies on the event instead, so the closest equivalent is a sendmail notifier through the same MTA. Add to your root runwisp.toml: [[notifier]] id = "mta" type = "sendmail" from = "runwisp@localhost" to = ["[email protected]"] then put notify_on_failure = ["mta"] on the tasks you want mail from. Note the difference: crond mailed any output at all, this mails failures.
Review the TOML above, save it as runwisp.toml, then run `runwisp validate`.
! cron still runs these jobs. Comment them out (`crontab -e`, or move the file out of /etc/cron.d) before starting RunWisp, or each one runs twice.Every line of your crontab gets a row showing the command that will actually run, wrapped if it’s long and never cut short. The mark tells you how it landed:
| Mark | Means |
|---|---|
✓ |
Mapped cleanly. |
~ |
Imported, but something changed — the row says what. |
! |
Needs a fix from you before this config will load. |
- |
Skipped, because your runwisp.toml already defines it. |
Anything about the file rather than one job — a MAILTO, an unresolvable
[include] — lands in Notes: at the bottom. Nothing is dropped without a line.
The closing ! about double-firing is the one thing this route asks of you; see
Retiring the crontab.
When it looks right, save it and check it parses:
crontab -l | runwisp import cron -o runwisp.tomlrunwisp validateKeeping your own config separate
Section titled “Keeping your own config separate”-o gives you one flat file, perfect when you’re starting fresh. If you already
have a runwisp.toml you’ve been hand-writing, use --write instead — it never
touches what you wrote:
crontab -l | runwisp import cron --write --dry-run # look firstcrontab -l | runwisp import cron --write # then do it--dry-run prints the same summary and names every file a real run would touch,
without touching any of them:
Would stage 3 tasks in /etc/runwisp/runwisp.d/imported.tomlWould create /etc/runwisp/runwisp.toml and wire it to load runwisp.d/*.toml.
Not checked: whether these merge with the tasks you already have.A real run checks that, and rolls both files back if it fails.
! cron still runs these jobs. Comment them out (`crontab -e`, or move the file out of /etc/cron.d) before starting RunWisp, or each one runs twice.
Nothing was written.The real run installs the import in a two-tier layout:
runwisp.toml # yours — hand-authored, keep it in gitrunwisp.d/imported.toml # machine-owned — the importer writes thisYour root file is created (or its
[daemon].include wired) to load runwisp.d/*.toml,
and the imported jobs land in the staging file. The importer only ever owns
runwisp.d/imported.toml, so you can re-run the import without worrying what it’ll
do to your own tasks.
A name clash doesn’t break anything: if your config already defines that name with
the same command, the job is skipped as already-yours (a - row); with a
different command, the import is renamed — backup becomes backup-2 — and the
row says so. Both files are written as one unit, so any failure rolls both back
rather than leaving your daemon half-updated.
Imported tasks report "source": "staged" in runwisp list --json and
status --json — shorthand for “came from an import, not native TOML yet”. It’s a
label, not a limitation: staged tasks are scheduled and run like any other.
Graduating a job into your own config
Section titled “Graduating a job into your own config”promote turns a job RunWisp already runs into a block in your own
runwisp.toml:
runwisp promote backup # this onerunwisp promote --all # all of themIt works the same on a job read live via include_cron and on a staged
import; --all covers both, but what “move” means differs:
- From a live crontab: the definition is copied into your
runwisp.toml, and the exact source line is commented out in the crontab it came from — the same fileinclude_cronread, whether that’s your user spool (crontab -e) or an/etc/cron.ddrop-in. That’s not just tidying. A job is only ever “held” (RunWisp standing down while a live cron daemon runs it) while it’s cron-sourced, so the instant it becomes native RunWisp would schedule it — and a live cron would keep firing the line too. Commenting the line out stops cron from firing it while leaving it visible, with a note pointing at where the job moved. Promote refuses, writing nothing on either side, if that line has changed or gone missing since the config was loaded — try again once the crontab has settled. - From a staged import: staging is a waiting room.
promotemoves the block out ofrunwisp.d/imported.tomlinto yourrunwisp.toml, comments and all — including any# TODOthe import left. Nothing is re-generated, so nothing gets reformatted or lost. When the last staged job is promoted, the empty staging file is cleaned up and you’re left with a single hand-maintainedrunwisp.toml.
From then on the job is yours: it reports no source, and a later re-import won’t
touch it. What a runwisp reload reports depends on which move it was. A staged
promotion — or a live-crontab one where you’d already retired cron, so RunWisp was
scheduling the job anyway — changes nothing about what runs: the reload reports no
task changes and only refreshes the provenance label. Promoting a job while cron
is still live is a real handover — it was held, and now it isn’t — so the reload
reports it as a changed task and RunWisp starts scheduling it. Either way, --reload
does the move and the reload in one step.
Retiring the crontab
Section titled “Retiring the crontab”An import copies your jobs; it never touches your crontab. A copy has no link back
to the crontab line it came from, so — unlike a job read live through
include_cron — RunWisp can’t recognise it as the same one cron runs and stand it
down. Starting the daemon with cron still enabled means two schedulers firing each
job. So take the imported jobs away from cron by hand:
crontab -l > ~/crontab.backup # keep a copy, alwayscrontab -e # comment out the lines you importedFor a system crontab, move the file instead of editing it — cron picks up
/etc/cron.d by directory listing:
sudo mv /etc/cron.d/myjobs /root/myjobs.crontab.backupCutting over gradually works too: comment out one job, let RunWisp own it for a few days, then do the next. Just don’t leave a job enabled in both places — one owner per job keeps the schedules honest.
System crontabs and the user column
Section titled “System crontabs and the user column”/etc/crontab and files in /etc/cron.d put a user between the schedule and
the command. RunWisp maps that column to the task’s user
automatically — from the path or the # ... user command header. Force it either
way with --system / --system=false when a piped crontab is ambiguous; the
mapping reference covers what
happens when a line is missing its user column.
Flag reference
Section titled “Flag reference”runwisp import cron [FILE] reads the file you name, or stdin:
| Flag | What it does |
|---|---|
--output, -o |
Write the TOML to this standalone file instead of stdout. |
--write |
Install into the two-tier layout next to the --config path. |
--force |
Overwrite the -o target without prompting. |
--dry-run |
Print the summary and name every file a real run would touch, then stop. |
--quiet |
Drop the clean rows from the summary, keep anything that needs a fix. |
--system |
Force system-crontab parsing (the user column). --system=false forces per-user; auto-detected when unset. |
--dry-run and --quiet contradict each other and are rejected together — a dry
run writes nothing, so the summary is all it has to show you.
runwisp promote [TASK...] takes:
| Flag | What it does |
|---|---|
--all |
Promote every promotable task — staged and crontab-sourced. |
--reload |
Reload the running daemon afterwards so it picks the move up — clears the provenance marker, and for a job promoted while cron is still live also hands its schedule to RunWisp. |
--dry-run |
Print the blocks that would move and write nothing. |
What it doesn’t do
Section titled “What it doesn’t do”import cronnever modifies the source. Reading and copying your crontab leaves the file untouched, which is why retiring it is yours to do.promoteis the one exception — it comments out the exact cron line of the job it graduates (see Graduating a job).- It never edits your hand-written tasks.
--writeonly ownsrunwisp.d/imported.toml. - It never writes daemon-wide settings. A
MAILTObecomes a notifier block in the summary for you to paste, not a[[notifier]]the importer adds. - It doesn’t guess. Anything that can’t map cleanly gets a
# TODOand a row in the summary — see What needs a human.