# CLI reference: runs and workers

> The pyrula commands that read or act on a run store and the workers that serve it.

Source: https://docs.pyrula.io/reference/cli/

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.

## Exit codes of `pyrula runs`

| Code | Meaning |
|---|---|
| 0 | Done. An empty `list` is 0. |
| 1 | No such run, or a refusal such as a run that cannot be retried. |
| 2 | Usage 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`. |
| 3 | The store cannot be opened or reached, or the connection was lost while the command ran. |

## Naming a run

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

```bash
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]
```

## pyrula runs list

```bash
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.

| Flag | Meaning |
|---|---|
| `--workflow NAME` | Only this workflow. |
| `--status STATUS` | Only runs in this status. Repeat the flag or comma-separate for several (`--status error,terminated`). |
| `--since DURATION` | Only runs started within this long: a number and one of `s`, `m`, `h`, `d`, `w` (`30m`, `1h`, `7d`). |
| `--limit N` | At most N runs (default 20). |
| `--json` | Print 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`.

```json
[
  {
    "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.

## pyrula runs show

```bash
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:

```json
{
  "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"}`.


## pyrula runs retry, cancel and terminate

These take a run id the same way as `show` and share the exit codes above. See [Cancel and terminate](https://docs.pyrula.io/workflows/guides/cancel-and-terminate.md) and
[Replay and recovery](https://docs.pyrula.io/workflows/concepts/replay-and-recovery.md).

## pyrula worker

```bash
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:

```bash
python -m pyrula.workflows.worker_main --workflow app:process_order
```
