RunWisp vs Jobber: same itch, different reach.
Both replace cron with a Go binary. Jobber stops at scheduled jobs and hasn't had a release since 2020; RunWisp adds services, a web dashboard, and notifications that don't need sendmail.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- You'd rather run something maintained: Jobber's last release was v1.4.4 in 2020.
- 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.
- Every job result should pipe into your own script. Jobber's program sink does that; RunWisp's closest is a webhook.
- One YAML file at ~/.jobber and three error policies is all the model you want.
Yes / no, at a glance
Yes / no — and both columns win on something.
| Capability | Jobber | RunWisp |
|---|---|---|
| Actively maintained | ✗last release 2020 | ✓ |
| Run history per firing | ~jobber log, last 100 in memory | ✓SQLite rows + Web UI |
| Captures stdout / stderr | ~via result sinks | ✓every run, in the UI |
| Retries with backoff | ~skips next 1/2/4/8 firings | ✓constant / linear / exp |
| Failure notifications | ~email, file, or your program | ✓Slack / Telegram / ntfy / SMTP / sendmail |
| Pluggable result sinks | ✓program, file, socket | ~webhook |
| Coalesced alerts (no notify storms) | ✗ | ✓ |
| Overlap policy | ✗ | ✓skip / queue / kill |
| Catch-up after downtime | ✗ | ✓catch_up = 0 / 1 / N |
| 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 |
| Few moving parts | ~3 small Go binaries | ✓one binary |
| macOS / WSL / Docker | ~Linux + macOS, Docker image deprecated | ✓ |
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.
- Result sinks: pipe every run's stdout, stderr and exit status to a file, a socket, or your own program.
- Simple mental model: one YAML file at
~/.jobber, three error policies, done. - Frozen and small: it does one thing and hasn't changed since 2020.
What RunWisp adds
- Maintained: active releases, signed builds, a changelog you can read.
- 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.
- Slack, Telegram, Discord, ntfy, email and webhook notifications without a local MTA, with flapping alerts coalesced.
- Retries re-run the same firing;
on_overlapandcatch_upare config fields, not your problem. - Services (always-on processes) and tasks (scheduled) in one daemon.
- DST fall-back fires once, spring-forward fires once, shifted forward by the gap.
- Jobber's six-field time strings carry over unchanged.
The simple case
Nightly backup at 03:00. Both are a few lines in a config file.
# ~/.jobber
version: 1.4
jobs:
nightly-backup:
cmd: /usr/local/bin/backup.sh
time: '0 0 3 * * *'
onError: Continue # runwisp.toml
[tasks.nightly-backup]
cron = "0 3 * * *"
run = "/usr/local/bin/backup.sh" The version that runs in prod
Same backup, hardened. Program sinks (your scripts) on the left; named fields and built-in notifications on the right.
# ~/.jobber, hardened for production
version: 1.4
jobs:
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 = ["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 | No equivalent. Jobber stops scheduling the job after one error; RunWisp keeps firing it. |
onError: Backoff | retry_attempts = N + retry_backoff = "exponential" (retries the same firing) |
onError: Continue | Default behaviour; failures are logged, next firing proceeds. |
notifyOnError: [{type: program}] | notify = ["slack-ops"], or a webhook notifier to reach your own script |
jobber log | Web UI run list / runwisp tui |
jobber reload | runwisp reload (validates first, no restart) |
| No overlap handling (a new goroutine per firing) | on_overlap = "skip" |
| No catch-up after reboot | catch_up = 1 (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 retry_backoff
Jobber's Backoff policy doesn't retry anything. After each error it skips the next 1, 2, 4, 8 scheduled firings, then stops scheduling the job. RunWisp's retry_attempts + retry_backoff re-runs the same firing after retry_delay. onError: Stop has no RunWisp equivalent: a failed task keeps its schedule.
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
Is Jobber still maintained?
Can RunWisp run alongside Jobber while I migrate?
Jobber runs per user. Can RunWisp do that?
Does RunWisp support Jobber's six-field cron syntax?
How do Jobber's error policies map to RunWisp?
Does RunWisp need sendmail for notifications?
Is RunWisp heavier than Jobber?
Can RunWisp supervise long-running services too?
Does the same RunWisp binary work on macOS and WSL?
Outgrown ~/.jobber? One maintained binary, full history.
Install RunWisp, translate your YAML jobs to TOML stanzas, and watch every firing land in the web dashboard.
curl -fsSL https://get.runwisp.com | sh