Skip to content

CLI reference

runwisp is one binary with a handful of subcommands. Run it with no subcommand and it does the friendly thing — attaches to a running daemon, or scaffolds a config and starts one. Everything else is a verb: validate, reload, exec, import, and so on. This page is the full list; each command also prints its own --help. Help pages and error messages are colored when you’re on a terminal and plain otherwise — piping to a file, or setting NO_COLOR, gets you clean text with no escape codes.

These apply to every subcommand. They decide which config gets read, where state lives, where the HTTP server listens, and how the daemon logs.

Flag Default What it does
--config, -c runwisp.toml (root: /etc/runwisp/runwisp.toml) Path to the TOML config file. Env: RUNWISP_CONFIG.
--data .runwisp (root: /var/lib/runwisp) Directory for all persistent state — SQLite DB, per-task logs, PID file, Unix socket. Env: RUNWISP_DATA.
--port, -p 9477 TCP port for the HTTP server (REST API, SSE log stream, Web UI).
--host 127.0.0.1 Bind address. Use 0.0.0.0 to listen on every interface.
--log-level info Verbosity: debug, info, warn, error. Env: RUNWISP_LOG_LEVEL.
--log-format auto Log shape: auto, text, json. Env: RUNWISP_LOG_FORMAT.

--data is the one to pick once and keep: the database (runwisp.db), the socket (runwisp.sock), and every task’s logs all live under it, so moving it later is a plain directory move, not a config change. The two logging flags get the full treatment in Logging.

Running as root changes the defaults, precisely so sudo runwisp reload (or stop / restart / status) finds the same daemon a root runwisp daemon started with no flags: euid 0 defaults --config to /etc/runwisp/runwisp.toml and --data to /var/lib/runwisp instead of the cwd-relative paths every other user gets. RUNWISP_CONFIG / RUNWISP_DATA override that (and everything else) — precedence is flag > env var > euid-derived default.

Terminal window
runwisp # attach a TUI to a running daemon, or scaffold + start one
runwisp daemon # start headless (no TUI) — for Docker, systemd, cron
runwisp tui # attach a TUI to a local daemon over its socket
runwisp tui --url <URL> # attach a TUI to a remote daemon over HTTP

Bare runwisp is the everyday entry point. If a daemon already owns this data dir, it opens the TUI against it. Otherwise — and if there’s no runwisp.toml yet — it offers to scaffold a starter config, spawns the daemon in the background, and attaches.

If the port is already taken by another RunWisp daemon — one started from a different data directory — runwisp tells you which datadir and config that daemon is using and offers to connect to it, or to stop it and launch here instead. (Pick a different port any time with --port.) When the port belongs to something that isn’t RunWisp, you get the plain “port in use” error with hints for finding the culprit.

runwisp daemon is the headless variant for boxes with no terminal: it starts the scheduler, REST API, and Web UI without the TUI, and exits non-zero if there’s no config rather than hanging on a prompt nobody can answer. runwisp tui connects a fresh TUI to a daemon that’s already up, over the local socket — no password needed. Add --url (or set RUNWISP_URL) to attach to a daemon over HTTP — a remote host or a container — logging in with its password; the TUI tour covers how that password is read and cached.

Terminal window
runwisp validate # parse + validate runwisp.toml without starting anything
runwisp list # show configured tasks and their schedules
runwisp status # is the daemon alive right now?

runwisp validate is the CI-and-pre-commit friend: it loads the config, prints a short summary (task and service counts, the resolved timezone), and surfaces the same advisory warnings the daemon would log at boot — all without touching anything live. runwisp list renders the configured tasks as a table. runwisp status pings the daemon over its local socket and prints a short health summary; it also flags when runwisp.toml has changed on disk since the daemon started.

Add --json to validate, list, or status and you get a single JSON document on stdout instead of the human text — handy when an agent or script drives RunWisp headless. Every document carries a schema_version so you can trust its shape across releases (the shape only ever grows — fields are added, never removed or repurposed). A failure still exits non-zero and prints JSON describing what went wrong, so your script never has to grep stderr to learn something broke.

runwisp validate --json tells you whether the config is valid and hands back counts, the resolved timezone, and errors and warnings as arrays:

{
"schema_version": 1,
"valid": true,
"config_path": "runwisp.toml",
"timezone": "UTC",
"timezone_source": "config",
"tasks": 1,
"services": 1,
"warnings": [],
"errors": []
}

On a broken config it exits non-zero with "valid": false and a populated errors array. Parse-time errors carry a structured location so tooling can jump straight to the spot instead of parsing the message:

{
"valid": false,
"errors": [
{
"message": "unknown key \"schedule\" at line 4:1",
"key": "tasks.backup.schedule",
"line": 4,
"column": 1
}
]
}

runwisp list --json mirrors the config table. Like the human view it’s offline and reads only runwisp.toml, so there’s no run history here. Services report their instances and omit max_concurrent — a service’s copy count is instances, not an overlap cap, so there’s no value to report:

{
"schema_version": 1,
"tasks": [
{
"name": "backup",
"kind": "task",
"schedule": "0 3 * * *",
"max_concurrent": 1,
"on_overlap": "queue",
"api_trigger": true,
"description": "nightly backup"
}
]
}

A task carries "source" when its definition doesn’t live in your own TOML: "staged" for the machine-owned runwisp.d/imported.toml that import --write manages, "cron" for a real crontab read via include_cron. "source_file" names the exact file. Both are derived from where the definition was loaded, so they’re absent for your own tasks and disappear on their own once you move one into your root config.

runwisp status --json is the live snapshot: daemon health, a system summary, and every task with its last run — the exit code, timing, and failed/missed flags are precomputed so you don’t have to decode RunWisp’s status names yourself:

{
"schema_version": 1,
"healthy": true,
"version": "0.12.0",
"scheduling_active": true,
"config_stale": false,
"system": { "version": "0.12.0", "uptime": "1h2m3s", "cpu_cores": 8, "host": "my-vps" },
"tasks": [
{
"name": "backup",
"kind": "task",
"cron": "0 3 * * *",
"api_trigger": false,
"next_run_at": "2026-07-16 03:00:00",
"last_run": {
"id": "01JZZBACKUP0000000000000000",
"status": "ended",
"end_reason": "failed",
"exit_code": 1,
"start_at": "2026-07-15T03:00:00Z",
"end_at": "2026-07-15T03:00:42Z",
"duration_ms": 42000,
"failed": true,
"missed": false
}
}
]
}

A task that has never run reports "last_run": null. If the daemon isn’t running, status --json exits non-zero with "healthy": false and an error.

Terminal window
runwisp exec <task> # run a task, stream its output, exit with its code
runwisp exec <task> --daemon # require a running daemon (fail fast if none)
runwisp exec <task> --standalone # require in-process (refuse if a daemon owns the dir)
runwisp exec <task> --json # print the run outcome as JSON (log lines to stderr)

runwisp exec runs one task and streams its stdout/stderr straight to your terminal, exiting with the run’s own exit code — so it slots cleanly into a script. By default it auto-detects: if a daemon owns this data dir, the run is dispatched through its REST API and followed live; otherwise the task runs in this process from runwisp.toml. --daemon and --standalone pin the choice and are mutually exclusive.

With --json, the run’s outcome is printed to stdout as one document once it finishes, and the live log lines are diverted to stderr so stdout stays a single JSON value — ideal for an agent that needs the result, not the noise:

{
"schema_version": 1,
"task": "backup",
"run_id": "01JZZBACKUP0000000000000000",
"status": "ended",
"end_reason": "success",
"exit_code": 0,
"duration_ms": 812,
"failed": false
}

If the run can’t even start — an unknown task, an unreachable daemon, a failed login — the document carries an error string instead of a run outcome, and exec still exits non-zero. So a caller parsing stdout always gets JSON, never silence:

{
"schema_version": 1,
"task": "backup",
"error": "task \"backup\" not found"
}
Terminal window
runwisp reload # re-read runwisp.toml and reconcile the live task set
runwisp restart # stop and start fresh (restart-only settings, run_on_start, catch-up)
runwisp stop # shut the daemon down

For the everyday edits — adding, changing, or removing tasks and services, or tweaking [defaults]runwisp reload (the same thing as sending SIGHUP) re-reads the file and reconciles the running task set in place. It’s validate-first: a config that won’t parse, fails validation, or touches a restart-only setting is rejected and your running daemon keeps going untouched. In-flight runs finish under the definition they started with. The Reload page has the full rules.

runwisp restart is the heavier hammer: it stops the daemon and starts a new one, which is what you need for the restart-only settings ([daemon], the scheduler timezone, [storage], [notify], the bind host/port) and when you want a fresh boot to re-fire run_on_start and missed-run catch-up. runwisp stop takes the daemon down without starting a new one. When the daemon is managed by systemd or launchd (via runwisp service install), both restart and stop delegate to the service manager so its view of the unit stays in sync. They find whichever unit is installed on their own; pass --local only when a system unit and a per-user unit both exist and you mean the per-user one.

Terminal window
runwisp import cron [FILE] # convert a crontab to runwisp.toml
runwisp import supervisord [FILE...] # convert a supervisord config to runwisp.toml

Both read from a file or from stdin, print the generated TOML to stdout so you can review it, and write a # TODO comment for anything that doesn’t map cleanly — nothing is silently dropped. Shared flags:

Flag What it does
--output, -o Write the TOML to this standalone file instead of stdout.
--write Install into the two-tier layout (see below) next to the --config path.
--force Overwrite the target file without prompting.
--dry-run Print the summary and name every file a real run would touch, and stop.
--quiet Drop the inventory, keep anything that needs a fix.
--system (cron only) Force system-crontab parsing (the user column).

The summary on stderr gives every job one line, with the command that will actually run and a mark for how it landed: mapped cleanly, ~ something changed, ! needs a fix before this config loads, - skipped because you already have it. --quiet drops the clean rows, not the ones that need you. --dry-run and --quiet are rejected together — a dry run writes nothing, so the summary is all it has to show you.

--write doesn’t append to your runwisp.toml. It installs the import in a two-tier layout: generated tasks land in a machine-owned runwisp.d/imported.toml, and your root runwisp.toml is created — or has its [daemon].include wired — so the daemon loads them. Both files are written and rolled back as one. Staged tasks report "source": "staged" in list --json, status --json, and the API.

An import copies definitions — it never touches the thing you imported from, so starting the daemon on a fresh import means every job has two schedulers firing it. Nothing errors, so every import summary ends with the ! line that says so:

! 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.

Turn the old scheduler off for the jobs you imported before runwisp daemon. Converting crontabs has the commands, along with the two-tier layout, the user column in system crontabs, and the staging-to-promoted workflow; Migrating from supervisord covers the other source.

Terminal window
runwisp promote backup # put one task's block in runwisp.toml
runwisp promote backup reindex # several at once
runwisp promote --all --reload # everything, then reconcile the daemon

promote works on any task RunWisp derived rather than you writing it: one staged in runwisp.d/imported.toml by an import, or one read live out of a crontab via include_cron. Either way its block ends up in your own runwisp.toml and it’s yours — copied as text, comments and # TODO notes intact, in one transaction that changes no file if the result wouldn’t load.

Flag What it does
--all Promote everything promotable — staged and crontab-sourced. Exits 0 when there’s nothing.
--reload Reload the running daemon afterwards so it picks the move up.
--dry-run Print the blocks that would be added and write nothing.

A staged block is moved out of the staging file. A cron-sourced one is copied into runwisp.toml and its source crontab line is commented out in the same transaction, so a still-live cron won’t double-fire it — see Graduating a job. That link also covers what a following runwisp reload reports, which differs between a staged promotion (no change to what runs) and promoting a job while its cron daemon is still live (a real handover RunWisp takes over).

Promote refuses, and writes nothing, when you name a task it can’t move: one that doesn’t exist, one that’s already native (it names the file it found it in), or one generated from a compose project, which has no block of its own.

Terminal window
sudo runwisp service install # wire up the system-wide systemd service
runwisp service uninstall # remove the unit/plist (data dir is preserved)
runwisp service status # is autostart wired up, and has anything drifted?

These wire the daemon into the host init system so it survives a reboot — OS plumbing only, never touching runwisp.toml.

install defaults to the system-wide service (/etc/systemd/system/runwisp.service, Linux, root). --local installs a per-user unit instead — a systemd user unit on Linux, a LaunchAgent on macOS, where it’s currently the only option. Without root and without --local, install refuses rather than guessing.

Other install flags: --yes (-y), --print (render the unit to stdout), --dry-run, --force (overwrite a hand-edited unit), and --binary <path> (override the baked-in binary path).

install never touches cron. If cron is still running jobs on the box when it finishes, it says so and points at sudo runwisp takeover — see Take over from cron.

uninstall takes --yes, --force, --local, and --purge (also delete the data dir — which makes you type the literal word delete to confirm). status takes --local. Neither needs to be told how the unit was installed: they look for it. It’s all covered in depth on the Autostart page.

Don’t confuse runwisp service status (“will the daemon come back after a reboot?”) with runwisp status (“is it alive right now?”).

Terminal window
sudo runwisp takeover # install the service, mask cron, reload — in that order

The front door for a cron migration, and it works from nothing: no runwisp.toml needed. It finds your crontabs, writes a config that reads them live, installs the system service, masks cron, and reloads a daemon that was already running — one plan, one question, in that order.

While cron is alive, RunWisp holds the jobs it read from cron’s own files rather than firing them alongside cron; takeover is what hands them over. The reload is the part a plain service install can’t do, so held jobs go live immediately instead of waiting for you to reload by hand.

--dry-run always prints a plan, including on a box where the take-over is blocked — there it also exits non-zero, so a provisioning script can ask “would this work?” without doing anything. Re-running a finished take-over is a no-op and exits 0.

Takes --dry-run, --yes (-y), --force (overwrite a hand-edited unit, not skip the prompt), --allow-skipped-cron-jobs, and --binary <path>. Needs root and systemd; see Take over from cron for the refusals, the ordering, and how to undo it.

Terminal window
runwisp password # print the daemon's ephemeral password (local socket only)
runwisp openapi # print the OpenAPI 3.1 spec (JSON) to stdout
runwisp schema # print the runwisp.toml JSON Schema (JSON) to stdout
runwisp agent-guide # print a paste-ready AGENTS.md snippet for AI agents

runwisp password fetches the in-memory ephemeral password over the local socket — handy when you want to log in from another device. It prints in plaintext, so pipe it to a clipboard tool (runwisp password | wl-copy) to keep it out of your scrollback. If the daemon uses a fixed RUNWISP_PASSWORD it refuses (that value is never disclosed); if it runs with RUNWISP_NO_AUTH there’s no password at all and the command exits with a distinct code.

runwisp openapi dumps the OpenAPI 3.1 schema to stdout for feeding into code generators — the same schema a running daemon serves at /openapi.json.

runwisp schema prints the JSON Schema for runwisp.toml (draft 2020-12) — point an editor at it, or check a config before validating. It’s also published at https://docs.runwisp.com/config.schema.json, which is what the #:schema line on a scaffolded config points to. runwisp agent-guide prints a short Markdown block you can append to your project’s AGENTS.md so an AI coding agent knows how to drive RunWisp. Both are covered on the Driving with an AI agent page.

Terminal window
runwisp cloud # connect to the optional control plane instead of scheduling locally
runwisp demo # boot a throwaway, fully-populated instance to explore

runwisp cloud starts the daemon connected to the optional control plane; it needs RUNWISP_CLOUD_TOKEN (via env, .env, or --token) and takes --url and --env-file (default .env), plus --no-tui for headless. runwisp demo boots RunWisp against a temporary config and data dir pre-seeded with hundreds of historical runs, so you can poke around the TUI and Web UI without writing a config — everything lives in a temp directory that’s deleted when the daemon stops. It takes --cloud (and the same --token / --url / --env-file) to point at the control plane instead of seeding local history. Pass --no-tui to leave the daemon running in the background and print its Web UI password to stdout instead of opening the TUI — handy over SSH or in a script where you just want to open the Web UI in a browser; stop it later with runwisp stop --data <dir> (the demo prints the exact data dir alongside the password).

Variable What it does
RUNWISP_PASSWORD Sets the daemon password in memory. Unset, a fresh ephemeral one is minted every boot. See Auth.
RUNWISP_NO_AUTH 1 or true disables authentication entirely — local dev / trusted networks only. Mutually exclusive with RUNWISP_PASSWORD. See Auth.
RUNWISP_TRUSTED_PROXIES Comma-separated CIDR list of reverse proxies whose X-Forwarded-* headers the daemon may honor.
RUNWISP_CLOUD_TOKEN Token for runwisp cloud. Ignored in standalone mode.
RUNWISP_CLOUD_URL Control-plane URL for runwisp cloud (overrides the built-in default).
RUNWISP_LOG_LEVEL Fallback for --log-level (debug/info/warn/error). The flag wins when both are set.
RUNWISP_LOG_FORMAT Fallback for --log-format (auto/text/json). The flag wins when both are set.