Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

CLI reference: runs and workers

Every command here reads the store from --store URL (default PYRULA_STORE_URL; --namespace for Valkey, and --cluster to override the cluster detection the store does on its own). A store a command cannot open or reach gets the same text pyrula dev --check prints.

On a Valkey store with a Postgres archive, pass --postgres-dsn (default PYRULA_POSTGRES_DSN), the same as for pyrula worker. Without it a run whose live copy has expired is not listed and show, retry, cancel and terminate answer “run not found”. With it, list, show and the run-id lookup read the archive too, and retry of an archived failed run works. The flag is refused (exit 2) on a store that has no archive.

CodeMeaning
0Done. An empty list is 0.
1No such run, or a refusal such as a run that cannot be retried.
2Usage error: a missing --store, a malformed URL, a flag that does not apply, or a run id that exists under more than one workflow and no --workflow.
3The store cannot be opened or reached, or the connection was lost while the command ran.

show, retry, cancel and terminate take the run id and look up its workflow in the store. If the same id exists under two workflows, the command lists them and exits 2; pass --workflow NAME to pick one. The old form, pyrula runs show NAME RUN_ID, is refused with exit 2 and a line showing the new one.

Terminal window
pyrula runs show RUN_ID [--workflow NAME]
pyrula runs retry RUN_ID [--workflow NAME] [--dry-run]
pyrula runs cancel RUN_ID [--workflow NAME] [--reason TEXT]
pyrula runs terminate RUN_ID [--workflow NAME] [--reason TEXT] [--yes]
Terminal window
pyrula runs list [--workflow NAME] [--status STATUS[,STATUS]]... [--since DURATION] [--limit N] [--json]

Runs newest first, with run id, workflow, status, start time (UTC) and duration. Without --workflow it lists every workflow in the store.

FlagMeaning
--workflow NAMEOnly this workflow.
--status STATUSOnly runs in this status. Repeat the flag or comma-separate for several (--status error,terminated).
--since DURATIONOnly runs started within this long: a number and one of s, m, h, d, w (30m, 1h, 7d).
--limit NAt most N runs (default 20).
--jsonPrint a JSON array instead of the table.

A rare --status or an old --since makes the command read further back in the store before it fills --limit. A --status filter with no --since stops after 10,000 runs, counted across all workflows together and walked in name order (with several workflows a rare status can be missed in the later ones: add --workflow or --since), and says so on stderr (stopped after N runs; narrow with --since or --workflow); the exit code is still 0.

A run that completed shows its duration. A run still going shows its age so far with a trailing … (3m12s…) so it is not read as a final figure; in JSON duration_ms stays null until the run has ended. A run that ended without completing shows -.

With --json each run is an object with these keys. Times are ISO-8601 UTC with milliseconds, and a value that does not exist is null.

[
{
"run_id": "r-7f3a",
"workflow": "process_order",
"status": "complete",
"started_at": "2026-10-29T12:00:00.000Z",
"duration_ms": 1500
}
]

An empty result is an empty table (header only) or [], with exit 0.

Terminal window
pyrula runs show RUN_ID [--workflow NAME] [--journal] [--json]

One run. The output has the status, attempt, version, trigger, parent, start time and duration, what the run waits on (a signal name, an interrupt, a sleep deadline or its start time), the start time it was scheduled with, its params, and its result or error. --journal adds the steps with their outcomes (pending, done or error).

Params, results and error text go through the dev UI’s redaction and size cap. Unlike pyrula dev --attach, which shows tracebacks, this command never prints one: an error is its code and a redacted, capped message. The command reads the store directly, so capture settings (PYRULA_CLOUD_CAPTURE, @workflow(capture=...)) do not apply; whatever the run stored is shown after redaction.

With --json the output is one object:

{
"workflow": "process_order",
"run_id": "r-7f3a",
"status": "signal_waiting",
"attempt": 1,
"version": "v3",
"version_behavior": "pinned",
"trigger": "manual",
"parent": null,
"started_at": "2026-10-29T12:00:00.000Z",
"scheduled_start": null,
"duration_ms": null,
"pending": {"kind": "signal", "name": "approve", "wait_id": "w1", "deadline": null},
"params": {"sku": "a"},
"result": null,
"error": null,
"journal": null
}

parent is {"workflow", "run_id"} for a child run. pending is null unless the run waits; its kind is signal, interrupt, sleep, or start for a run submitted with start_at or delay whose wait is armed. started_at is when the run was submitted. scheduled_start is the time a delayed run was told to start, kept after it has; it is null for every other run. error is {"code", "detail"} for a failed run: the error code and its redacted text. journal is null without --journal, else an array of {"step_id", "call_index", "step_kind", "outcome", "error_type", "error_message"}.

These take a run id the same way as show and share the exit codes above. See Cancel and terminate and Replay and recovery.

Terminal window
pyrula worker --workflow app:process_order
pyrula worker --agent app:support --llm-ref app:llm

--workflow serves workflows and works on an install of pyrula-workflows alone. --agent serves agents and comes with pyrula-agents; without it --agent exits 2 with --agent needs pyrula-agents (pip install "pyrula[agents]"). Run one process per kind: they use different stores and claim loops, so mixing the two flags is refused (exit 2), as is a flag that only the other kind reads (--max-concurrent-turns with --workflow, --max-concurrent-runs with --agent).

With --workflow the flags are: module:attr refs (repeatable) or a bare module name, --app-dir, --postgres-dsn, --worker-id, --max-concurrent-runs, --shutdown-grace, --cancel-timeout, --orphan-scan-interval, --shard-count, --shard-lease-ttl, --shard-lease-interval, --health-port, --health-host, --log-level. SIGTERM or SIGINT drains and exits 0, as pyrula dev does.

python -m pyrula.workflows.worker_main is the same worker with the same flags, for a process that cannot use the console script:

Terminal window
python -m pyrula.workflows.worker_main --workflow app:process_order

View this page as Markdown