Skip to content

Upgrading

RunWisp follows Semantic Versioning. The project is pre-1.0, which by SemVer means breaking changes are permitted on any minor bump. In practice, every release ships a CHANGELOG entry that calls out anything that requires action — read it before you upgrade.

Three steps, every time:

  1. Read the CHANGELOG for every version between yours and the new one. Anything tagged “BREAKING” needs your attention; everything else is safe.
  2. Back up the data directory — at minimum runwisp.db plus its -shm and -wal sidecars, and the logs/ tree if you care about history. See the backup section for a one-liner.
  3. Validate the config against the new binary before swapping it in:
    Terminal window
    /tmp/runwisp-new validate --config /etc/runwisp/runwisp.toml
    validate parses, runs the full schema check, and exits without touching the database. A non-zero exit means the new binary will refuse to start with this config — fix the error before the cutover.

There’s no migration tool. The upgrade is “stop the old daemon, replace the binary, start the new daemon.” Schema migrations run automatically on first start.

Terminal window
# 1. Drop in the new binary.
sudo install -m 0755 ./runwisp /usr/local/bin/runwisp
# 2. Restart the daemon.
sudo systemctl restart runwisp # systemd
docker compose up -d --force-recreate # docker compose

The new daemon opens the existing runwisp.db, applies any pending schema migrations, reconciles any rows still marked running (because the old daemon didn’t get to persist them) as crashed with exit code -2, and resumes scheduling.

If anything goes wrong, the daemon exits 1 with a clear message before opening the port. You won’t see a half-running daemon; either it’s up, or it’s down and shouting.

The migration policy:

  • Forward-only. Migrations run on startup and never roll back.
  • Idempotent. Restarting the same binary against the same database is a no-op.
  • Best-effort backwards-tolerant. A new daemon will read rows from a slightly older schema. An older daemon will not necessarily start against a newer database — once you’ve upgraded, downgrade is not supported.

If you need to roll back, restore from the snapshot you took before the upgrade.

Most upgrades need no config changes. When they do, it’s because runwisp.toml is the sole source of truth, and the schema is treated as a public surface even pre-1.0 — so any change to it is called out.

The breaking changes shipped so far:

runwisp.yaml is no longer read. Move every task definition into runwisp.toml, flattened into [tasks.NAME] blocks. The runwisp add and runwisp edit CLI subcommands were removed at the same time — edit runwisp.toml in your editor.

There was no automated converter; the schema rename was thorough enough that an honest hand-port was simpler.

0.2 → 0.3: restart = "always" is rejected on tasks

Section titled “0.2 → 0.3: restart = "always" is rejected on tasks”

Long-running processes used to live under [tasks.*] with restart = "always". They now live under [services.*] exclusively.

[tasks.api-worker]
restart = "always"
run = "/usr/local/bin/worker"
[services.api-worker]
run = "/usr/local/bin/worker"

The daemon rejects the old shape at config load — see the [services.*] reference for the contract. Cron-driven tasks under [tasks.*] are unaffected.

Port 8080 collided with too many other tools. The daemon now defaults to 9477. If you have firewall rules, reverse-proxy configs, or HEALTHCHECK commands hardcoding :8080, either:

  • update them to :9477, or
  • pass --port 8080 on the daemon command line.

Reverse proxies and Docker compositions usually need a one-line update.

0.3 → 0.4: API serialisation of durations and sizes

Section titled “0.3 → 0.4: API serialisation of durations and sizes”

timeout, restart_delay, retry_delay, and keep_for now serialise as nanoseconds (int64) in REST responses; log_max_size as bytes (int64). The TOML surface is unchanged — you still write "30s" and "100mb". If you have a custom REST consumer that parsed strings, switch it to integer math.

notify_on_failure / notify_on_success and [[notification_route]] notify lists now accept "<id>:<target>" tokens — an inline override that reuses a parent notifier’s credentials but redirects the message to a different Slack channel or Telegram chat. The colon is the separator, so existing [[notifier]] id values that contain : are now rejected at config load. Rename them before upgrading:

[[notifier]]
id = "slack:ops"
id = "slack-ops"
type = "slack"

Update any references in notify_on_failure, notify_on_success, and [[notification_route]] notify lists to match. See Per-task: inline target overrides for the new sugar.

Unreleased: log streaming endpoints redesigned

Section titled “Unreleased: log streaming endpoints redesigned”

Coming in the next release: /api/tasks/{name}/runs/{id}/log/stream emits SSE events with absolute line numbers, and the legacy ?start_line / ?tail shape is removed. If you wrote custom code against the SSE log surface, it will need updates — the Unreleased section of the CHANGELOG documents the new shape. Web UI and CLI tooling shipped in the binary update automatically.

Rolling back is restore-from-backup, not “install the older binary.” The schema is forward-only — once a migration has run, older daemons may refuse to open the database.

Terminal window
sudo systemctl stop runwisp
sudo rm -rf /var/lib/runwisp/runwisp.db{,-shm,-wal}
sudo cp -a /backups/runwisp-2026-05-06/runwisp.db{,-shm,-wal} /var/lib/runwisp/
sudo install -m 0755 ./runwisp-old /usr/local/bin/runwisp
sudo systemctl start runwisp

This is exactly why step 2 of the pre-flight checklist is “back up the data directory.”

  • The password file (data/password) is read as-is; no re-authentication needed.
  • The JWT secret lives in SQLite and survives unless you set RUNWISP_PASSWORD to a new value (in which case the daemon rotates it and invalidates outstanding sessions — see Auth).
  • Run history in the database is preserved.
  • On-disk logs under data/logs/ are preserved verbatim.
  • The daemon’s instance fingerprint is preserved.
  • In-flight runs at the moment of restart are cancelled. They record as stopped if they exit within the daemon’s 3-second graceful-shutdown window, or crashed with exit code -2 (via boot reconciliation) if they don’t. Tasks fire fresh on their next scheduled tick; services restart on their normal supervisor loop. This is by design — see Tasks vs Services / crashes.
  • Open Web UI sessions with cookies older than 24 hours need a fresh login (cookie expiry, not an upgrade artefact).
  • Notification queue — anything in flight when the daemon stops is lost. The queue is in-memory and not durable.

A useful guard in CI / Ansible / your config-management tool:

Terminal window
runwisp validate --config /etc/runwisp/runwisp.toml || exit 1

Pair it with a runwisp status after the restart to confirm the daemon came back healthy:

Terminal window
systemctl restart runwisp
sleep 2
runwisp status || (journalctl -u runwisp --since '1 minute ago'; exit 1)

Treat validation as a gate, and you’ll catch every “the new binary rejects this field” issue before the cutover.