# CLI

> The pyrula command: dev server, workers, store and turn management.

Source: https://docs.pyrula.io/agents/serving/cli/

`pyrula` ships with `pyrula-workflows`; `pyrula-agents` adds the agent subcommands.

```bash
pyrula --help        # case | db | dev | init | reproject | runs | schedules | store | turns | worker
```

`pyrula dev` takes file targets (`module.py:agent_name`). Worker commands and `--llm-ref` / `--deps-ref` take import refs (`your_module:agent_name`). `runs` and `case` are workflow-side commands ([CLI reference](https://docs.pyrula.io/reference/cli.md)). `pyrula dev --check` prints environment checks and a Cloud probe, then exits.

## dev

```bash
pyrula dev                       # discover app.py/agents.py here, run it, open the UI
pyrula dev app.py                # find the @agent in the module
pyrula dev app.py:support --llm-ref app:llm --port 8000
pyrula dev --attach --store valkey://…   # browse every run in a running system
```

- **Embedded** (a target, or auto-discovered): runs the agent in-process on a SQLite store at `.pyrula/dev.db`, so runs survive a restart. `--fresh` deletes the file first; `--store memory://` keeps runs in the process only. It also keeps the newest finished run as a case file, `.pyrula/last/<name>.case`, that `pyrula case run` reruns offline ([Record once, iterate free](https://docs.pyrula.io/workflows/guides/record-once-iterate-free.md)); `--no-save-last` turns that off. `pyrula init` adds `.pyrula/` to `.gitignore`.
- **Attach** (`--attach`): loads no agent and no worker, and lists every agent and workflow in the store with its runs. `--store` defaults to the dev store; `memory://` is refused.

The UI is read-only: start runs from your worker, code or API. To watch several processes on one machine, point them at the same file (`--store sqlite:///.pyrula/dev.db`) and run `pyrula dev --attach`. Valkey does the same across machines.

### Dev UI

The UI is at `/ui` on port 7978 (`--port` or `PYRULA_DEV_PORT`; it steps to the next free port if busy).

```bash
pyrula dev app.py:support --llm-ref app:llm --open   # open the browser
pyrula dev app.py:support --llm-ref app:llm --no-ui  # API only
```

The UI is local-only and unauthenticated. `pyrula dev --host` refuses a non-loopback address unless you pass `--insecure`.

To mount the same UI in your own FastAPI app:

```python
from pyrula.agents.web.router import AgentRouter
from pyrula.agents.web.ui_ext import register_agent_renderers
from pyrula.workflows.web import mount_run_ui

router = AgentRouter(worker=worker)        # an InlineWorker (pyrula.agents.execution.engine), or a WorkerProxy
router.attach(app, prefix="/agents")
register_agent_renderers()                 # llm/tool/block transcript renderers
mount_run_ui(app, prefix="/ui", control=router)
```

With separate workers it needs a durable store (SQLite on one host, or Valkey). `create_app(options=AppOptions(host=...))` refuses a non-loopback host without auth unless `allow_unauthenticated=True`.

### What embedded mode cannot run

An embedded worker (`pyrula dev`, `create_app` with the default `deployment_mode="embedded"`, an `AgentRouter` given a worker) has no timer loop and no claim loop for child runs. A call that would park on one raises `ConfigurationError("... is not supported in embedded mode; run `pyrula worker`")` and the turn ends in `run:error`.

Refused: `ctx.sleep`, `ctx.sleep_until`, `ctx.run_child`, `ctx.spawn_workflow`, `ctx.gather_subagents`, and any wait with a deadline (`ctx.wait_for_signal` or `ctx.interrupt` with `timeout=` / `deadline=`, a tool with `approval_timeout`). Same-process work (`ctx.step`, tools, `ctx.call_agent`) and deadline-free signals, interrupts and approvals still work.

Don't share one store between an embedded app and a separate worker: the embedded router refuses a run that slept under `pyrula worker`. Use `pyrula worker` or `deployment_mode="separate"` for these calls.

## worker

Runs the worker tier for [separate-mode](https://docs.pyrula.io/agents/serving/workers.md) deployments against the API's store (`--store`, or `PYRULA_STORE_URL`). A SQLite file serves several workers on one host; the shard flags apply to Valkey only.

```bash
pyrula worker \
  --agent app:support \
  --store redis://localhost:6379 \
  --llm-ref app:llm \
  --deps-ref app:services \
  --max-concurrent-turns 50 \
  --poison-max-retries 3 \
  --shard-count 64 \
  --shard-lease-ttl 30 \
  --shard-lease-interval 10
```

`--deps-ref` injects a shared dependencies object. Poison, heartbeat and orphan-scan flags map to the `WorkerOptions` knobs in [Workers](https://docs.pyrula.io/agents/serving/workers.md). The shard flags (shown at their defaults) are covered in [Scaling across shards](https://docs.pyrula.io/workflows/guides/worker.md#scaling-across-shards). Every worker serving an agent needs the same `--shard-count`, and `--shard-lease-interval` must be less than `--shard-lease-ttl` or the worker refuses to start. There is no cluster flag: the store detects a cluster from `INFO`. A cluster that hides its mode (behind a proxy, say) needs `WorkerRuntime` embedded with `ValkeyStore(cluster=True, ...)`.

## db

The run store sets itself up. Postgres archival needs its schema applied once; `check` opens the store and, with `--postgres-dsn`, verifies the archive schema revision.

```bash
pyrula db check --store redis://localhost:6379
pyrula db check --store redis://localhost:6379 --postgres-dsn "$PYRULA_POSTGRES_DSN"
pyrula db migrate --postgres-dsn "$PYRULA_POSTGRES_DSN"
```

The archive schema has no upgrade path: an archive from a different pyrula version fails `check`, `migrate` and worker start with the found and expected revisions, and must be recreated.

## store

Shows, sets or clears the project and environment a store belongs to ([One store per project and environment](https://docs.pyrula.io/getting-started/cloud.md#one-store-per-project-and-environment)).

```bash
pyrula store binding show --store valkey://127.0.0.1:6392
pyrula store binding set --project acme --environment production --store valkey://127.0.0.1:6392
pyrula store binding clear --store valkey://127.0.0.1:6392
```

```
project: acme
environment: production
bound_by: cli
bound_at: 2026-10-10T00:23:54+00:00
```

- `--store` defaults to `PYRULA_STORE_URL`. It takes `valkey://`, `redis://` (TLS: `valkeys://`, `rediss://`) and `sqlite:///relative.db` / `sqlite:////absolute.db`. `--cluster` forces the cluster client for a Valkey cluster that hides its mode.
- `--namespace` (Valkey only; default `PYRULA_VALKEY_NAMESPACE`, else `default`) scopes every key, and every command taking `--store` accepts it. Point all processes in an environment at the same namespace.
- `set` is idempotent for the same binding and exits 1 with the existing binding if the store is bound elsewhere.
- `clear` exits 1 if another process rebound the store meanwhile. The store's history goes to whichever environment binds next, so stop its workers first.
- `show` reports an unparseable value as malformed (exit 1) with the raw text; `clear --malformed` removes it.

For SQLite, `pyrula store backup DEST` writes a consistent copy while workers run (copying the `.db` alone isn't a backup; writes can sit in the `-wal` file). `pyrula store info` prints size, format generation, retention, runs by status and the hosts that opened it.

## turns

```bash
pyrula turns list-quarantined --agent support --limit 50
pyrula turns inspect --turn-id <id> --agent support
pyrula turns requeue --turn-id <id> --agent support
pyrula turns gc --older-than 30d --postgres-dsn "$PYRULA_POSTGRES_DSN" --dry-run
```

`requeue` puts a turn the worker [quarantined as poison](https://docs.pyrula.io/agents/serving/workers.md) back in the queue. `gc` deletes archived turns past a cutoff; `--dry-run` counts them first. Scheduled runs: [Schedules](https://docs.pyrula.io/agents/serving/schedules.md).

## reproject

Rebuilds the conversation, usage and cost read tables from the archived event log, to backfill runs archived before projection was enabled or after a price change. Needs Postgres with `PostgresStore(..., project=True)` ([Observability](https://docs.pyrula.io/agents/observability.md#conversations-and-usage-in-sql)).

```bash
pyrula reproject --agent support                 # one agent
pyrula reproject --agent support --run-id <id>   # one run (--run-id requires --agent)
pyrula reproject                                 # every archived run
```
