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.
Global flags
Section titled “Global flags”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.
Start the daemon and attach
Section titled “Start the daemon and attach”runwisp # attach a TUI to a running daemon, or scaffold + start onerunwisp daemon # start headless (no TUI) — for Docker, systemd, cronrunwisp tui # attach a TUI to a local daemon over its socketrunwisp tui --url <URL> # attach a TUI to a remote daemon over HTTPBare 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.
Look before you leap
Section titled “Look before you leap”runwisp validate # parse + validate runwisp.toml without starting anythingrunwisp list # show configured tasks and their schedulesrunwisp 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.
Machine-readable output (--json)
Section titled “Machine-readable output (--json)”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.
Run a task now
Section titled “Run a task now”runwisp exec <task> # run a task, stream its output, exit with its coderunwisp 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"}Apply config edits
Section titled “Apply config edits”runwisp reload # re-read runwisp.toml and reconcile the live task setrunwisp restart # stop and start fresh (restart-only settings, run_on_start, catch-up)runwisp stop # shut the daemon downFor 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.
Migrate an existing setup
Section titled “Migrate an existing setup”runwisp import cron [FILE] # convert a crontab to runwisp.tomlrunwisp import supervisord [FILE...] # convert a supervisord config to runwisp.tomlBoth 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.
Take ownership of a task with promote
Section titled “Take ownership of a task with promote”runwisp promote backup # put one task's block in runwisp.tomlrunwisp promote backup reindex # several at oncerunwisp promote --all --reload # everything, then reconcile the daemonpromote 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.
Autostart
Section titled “Autostart”sudo runwisp service install # wire up the system-wide systemd servicerunwisp 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?”).
Retire cron
Section titled “Retire cron”sudo runwisp takeover # install the service, mask cron, reload — in that orderThe 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.
Auth and introspection
Section titled “Auth and introspection”runwisp password # print the daemon's ephemeral password (local socket only)runwisp openapi # print the OpenAPI 3.1 spec (JSON) to stdoutrunwisp schema # print the runwisp.toml JSON Schema (JSON) to stdoutrunwisp agent-guide # print a paste-ready AGENTS.md snippet for AI agentsrunwisp 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.
Cloud and demo
Section titled “Cloud and demo”runwisp cloud # connect to the optional control plane instead of scheduling locallyrunwisp demo # boot a throwaway, fully-populated instance to explorerunwisp 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).
Environment variables
Section titled “Environment variables”| 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. |