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.
Before you upgrade
Section titled “Before you upgrade”Three steps, every time:
- Read the CHANGELOG for every version between yours and the new one. Anything tagged “BREAKING” needs your attention; everything else is safe.
- Back up the data directory — at minimum
runwisp.dbplus its-shmand-walsidecars, and thelogs/tree if you care about history. See the backup section for a one-liner. - Validate the config against the new binary before swapping it
in:
Terminal window /tmp/runwisp-new validate --config /etc/runwisp/runwisp.tomlvalidateparses, 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.
The upgrade itself
Section titled “The upgrade itself”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.
# 1. Drop in the new binary.sudo install -m 0755 ./runwisp /usr/local/bin/runwisp
# 2. Restart the daemon.sudo systemctl restart runwisp # systemddocker compose up -d --force-recreate # docker composeThe 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.
Database migrations
Section titled “Database migrations”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.
Configuration migrations
Section titled “Configuration migrations”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:
0.1 → 0.2: YAML to TOML
Section titled “0.1 → 0.2: YAML to TOML”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.
0.3 → 0.4: default port moved to 9477
Section titled “0.3 → 0.4: default port moved to 9477”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 8080on 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.
Unreleased: notifier IDs cannot contain :
Section titled “Unreleased: notifier IDs cannot contain :”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
Section titled “Rolling back”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.
sudo systemctl stop runwispsudo 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/runwispsudo systemctl start runwispThis is exactly why step 2 of the pre-flight checklist is “back up the data directory.”
What survives an upgrade
Section titled “What survives an upgrade”- 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_PASSWORDto 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.
What doesn’t
Section titled “What doesn’t”- In-flight runs at the moment of restart are cancelled. They
record as
stoppedif they exit within the daemon’s 3-second graceful-shutdown window, orcrashedwith 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.
CI / automation tip
Section titled “CI / automation tip”A useful guard in CI / Ansible / your config-management tool:
runwisp validate --config /etc/runwisp/runwisp.toml || exit 1Pair it with a runwisp status after the restart to confirm the
daemon came back healthy:
systemctl restart runwispsleep 2runwisp 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.
Where to next
Section titled “Where to next”- CHANGELOG — the canonical source of breaking changes.
- Operations: troubleshooting — what to do when the upgrade goes sideways.
- Operations: data directory — what exactly is in the backup.