Comparison · vs Jobber
RunWisp vs Jobber: same itch, different reach.
Both replace cron with a Go binary. One stops at scheduled jobs; the other adds services, a web dashboard, and notifications that don't require sendmail.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- You want run history you can browse in a web UI, not just grep in syslog.
- Notifications should hit Slack or Telegram, not depend on a local sendmail.
- You also supervise long-running services on the same box.
- Overlap policies and catch-up after downtime matter.
- One daemon per system beats one daemon per user.
- You need the same binary on macOS, WSL, and Docker.
Stay on Jobber if
- Per-user isolation is a hard requirement (multi-tenant box, each user owns their jobs).
- You want the lightest possible footprint: no embedded SQLite, no web server.
- Your notification story is already sendmail and you don't want to change it.
- One YAML file at ~/.jobber and three error policies is all the model you want.
Yes / no, at a glance
Yes / no, both columns win on something.
| Feature | Jobber | RunWisp |
|---|---|---|
| Run history per firing | ~ jobber log, syslog | SQLite rows + Web UI |
| Captures stdout / stderr | | |
| Retries with backoff | ~ Backoff on error only | constant / linear / exp |
| Failure notifications | ~ sendmail or program | Slack / Telegram / email |
| Coalesced alerts (no notify storms) | | |
| Overlap policy | | skip / queue / kill |
| Catch-up after downtime | | latest / all / skip |
| DST handling | | deduped / next valid |
| Web UI / dashboard | | |
| TUI over SSH | | |
| Long-running services | | [services.*] |
| Per-user daemon isolation | one daemon per user | system-wide |
| Six-field cron (seconds) | | optional seconds field |
| Single static Go binary | | |
| macOS / WSL / Docker | ~ Linux + macOS | |
Pros & cons
What Jobber still does better
- Per-user daemon model: each Unix user owns their jobs with no shared config or shared daemon.
- Lighter footprint: no embedded SQLite, no web server, no TUI. Just runs jobs.
- Simple mental model: one YAML file at
~/.jobber, three error policies, done. - Longer track record in the cron-replacement niche.
What RunWisp adds
- Every firing is a row with status, duration, exit code, and captured stdout/stderr.
- Web dashboard and TUI for browsing run history without SSH + grep.
- Six-field cron with an optional seconds field — Jobber's leading column carries over unchanged.
- Slack, Telegram, Discord, email, and webhook notifications without requiring a local MTA.
- Overlap policies (
skip/queue/kill) are a config field, not your problem. -
catch_up = "latest"replays missed firings after a reboot. - Services (always-on processes) and tasks (scheduled) in one daemon.
- DST fall-back fires once (deduped), spring-forward fires at the next valid minute.
- One binary on Linux, macOS, WSL, and Docker. Same TOML everywhere.
The simple case
Nightly backup at 03:00. Both are a few lines in a config file.
# ~/.jobber
[jobs]
- name: nightly-backup
cmd: /usr/local/bin/backup.sh
time: '0 0 3 * * *'
onError: Stop # runwisp.toml
[tasks.nightly-backup]
cron = "0 3 * * *"
run = "/usr/local/bin/backup.sh" Production-grade
Same backup, hardened. notifyProgram scripts on the left; named fields and built-in notifications on the right.
# ~/.jobber, hardened for production
[jobs]
- name: nightly-backup
cmd: /usr/local/bin/backup.sh
time: '0 0 3 * * *'
onError: Backoff
notifyOnError:
- type: program
path: /usr/local/bin/notify-slack.sh
notifyOnFailure:
- type: program
path: /usr/local/bin/page-ops.sh # runwisp.toml, hardened for production
[tasks.nightly-backup]
description = "Snapshot the data volume to off-site storage"
cron = "0 3 * * *"
on_overlap = "skip" # never two backups at once
timeout = "55m" # die before the next firing
retry_attempts = 2
retry_delay = "30s"
retry_backoff = "exponential"
keep_runs = 60 # two months of history
notify_on_failure = ["slack-ops"]
run = "/usr/local/bin/backup.sh" Migration cheat sheet
Jobber concept on the left, RunWisp equivalent on the right.
| Jobber | RunWisp |
|---|---|
~/.jobber (per-user YAML) | runwisp.toml (system-wide TOML) |
time: '0 0 3 * * *' (6 fields) | cron = "0 0 3 * * *" (6-field accepted) or drop the seconds column |
cmd: | run = |
onError: Stop | retry_attempts = 0 (default) |
onError: Backoff | retry_attempts = N + retry_backoff = "exponential" |
onError: Continue | Default behaviour; failures are logged, next firing proceeds. |
notifyOnError: [{type: program}] | notify_on_failure = ["slack-ops"] |
jobber log | Web UI run list / runwisp tui |
jobber reload | runwisp reload (validates first, no restart) |
| No overlap handling | on_overlap = "skip" |
| No catch-up after reboot | catch_up = "latest" (default) |
Gotchas
Six-field cron carries over directly
Jobber's time field has a leading seconds column: 0 0 3 * * *. RunWisp now accepts the same six-field form, so the expression copies over unchanged; the five-field form (0 3 * * *) and @every 30s work too. Quartz operators (L, W, #) are not parsed.
No per-user daemon model
Jobber runs one daemon per Unix user, each reading ~/.jobber. RunWisp is system-wide. If per-user isolation matters (shared hosting, multi-tenant boxes), you lose that boundary.
Jobber's onError: Backoff is not the same as retry_backoff
Jobber's Backoff error policy delays the next scheduled firing, not the current one. RunWisp's retry_attempts + retry_backoff retries the same firing immediately with increasing delays. They solve different problems.
Tasks run as the daemon's user unless you say otherwise
Jobber jobs run as the user whose ~/.jobber they live in. RunWisp tasks run as the daemon's user by default; when the daemon runs as root, set user = "deploy" per task to run as another uid.
FAQ
Yes. They don't share config files, sockets, or state. Move one job at a time: add the task to runwisp.toml, remove it from ~/.jobber, restart both daemons.
RunWisp is system-wide by design. You can run multiple instances under different users with separate config files and data directories, but it's not the default workflow. If per-user isolation is a hard requirement, Jobber's model is simpler.
Yes. RunWisp accepts an optional leading seconds field, so a Jobber time like 0 0 3 * * * copies over as cron = "0 0 3 * * *". Five-field cron and aliases like @hourly, @daily, @every 30s also work. You can drop the seconds column if you prefer, but you no longer have to.
Stop = retry_attempts 0 (default). Backoff delays the next scheduled firing; the closest RunWisp equivalent is retry_attempts + retry_backoff, but note that RunWisp retries the current firing, not the next one. Continue = the default; failures are logged and the next cron firing proceeds normally.
No. Notifications go through Slack, Telegram, Discord, generic webhooks, or SMTP directly. No local MTA required. Jobber shells out to sendmail or a custom notifyProgram script.
Yes. RunWisp embeds SQLite for run history and an HTTP server for the web dashboard. Idle RAM is around 25 MB vs Jobber's lighter footprint. The trade-off is queryable history and a UI without extra tooling.
Yes. [services.*] blocks define always-on processes with restart backoff, graceful stop, and replicas. Jobber is scheduled jobs only; pair it with supervisord or systemd if you also need services.
Yes. One Go binary, no runtime deps. Same TOML config, same web UI on Linux, macOS, WSL, and Docker. Jobber supports Linux and macOS but not WSL or Docker out of the box.
More comparisons: RunWisp vs cron · RunWisp vs systemd timers · RunWisp vs supervisord .
Outgrown ~/.jobber? One binary, full history.
Install RunWisp, translate your YAML jobs to TOML stanzas, and watch every firing land in the web dashboard.