CLI
pyrula ships with pyrula-workflows; pyrula-agents adds the agent subcommands.
pyrula --help # case | db | dev | init | reproject | runs | schedules | store | turns | workerpyrula 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). pyrula dev --check prints environment checks and a Cloud probe, then exits.
pyrula dev # discover app.py/agents.py here, run it, open the UIpyrula dev app.py # find the @agent in the modulepyrula dev app.py:support --llm-ref app:llm --port 8000pyrula 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.--freshdeletes 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, thatpyrula case runreruns offline (Record once, iterate free);--no-save-lastturns that off.pyrula initadds.pyrula/to.gitignore. - Attach (
--attach): loads no agent and no worker, and lists every agent and workflow in the store with its runs.--storedefaults 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
Section titled “Dev UI”The UI is at /ui on port 7978 (--port or PYRULA_DEV_PORT; it steps to the next free port if busy).
pyrula dev app.py:support --llm-ref app:llm --open # open the browserpyrula dev app.py:support --llm-ref app:llm --no-ui # API onlyThe 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:
from pyrula.agents.web.router import AgentRouterfrom pyrula.agents.web.ui_ext import register_agent_renderersfrom pyrula.workflows.web import mount_run_ui
router = AgentRouter(worker=worker) # an InlineWorker (pyrula.agents.execution.engine), or a WorkerProxyrouter.attach(app, prefix="/agents")register_agent_renderers() # llm/tool/block transcript renderersmount_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
Section titled “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
Section titled “worker”Runs the worker tier for separate-mode 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.
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. The shard flags (shown at their defaults) are covered in 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, ...).
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.
pyrula db check --store redis://localhost:6379pyrula 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.
Shows, sets or clears the project and environment a store belongs to (One store per project and environment).
pyrula store binding show --store valkey://127.0.0.1:6392pyrula store binding set --project acme --environment production --store valkey://127.0.0.1:6392pyrula store binding clear --store valkey://127.0.0.1:6392project: acmeenvironment: productionbound_by: clibound_at: 2026-10-10T00:23:54+00:00--storedefaults toPYRULA_STORE_URL. It takesvalkey://,redis://(TLS:valkeys://,rediss://) andsqlite:///relative.db/sqlite:////absolute.db.--clusterforces the cluster client for a Valkey cluster that hides its mode.--namespace(Valkey only; defaultPYRULA_VALKEY_NAMESPACE, elsedefault) scopes every key, and every command taking--storeaccepts it. Point all processes in an environment at the same namespace.setis idempotent for the same binding and exits 1 with the existing binding if the store is bound elsewhere.clearexits 1 if another process rebound the store meanwhile. The store’s history goes to whichever environment binds next, so stop its workers first.showreports an unparseable value as malformed (exit 1) with the raw text;clear --malformedremoves 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.
pyrula turns list-quarantined --agent support --limit 50pyrula turns inspect --turn-id <id> --agent supportpyrula turns requeue --turn-id <id> --agent supportpyrula turns gc --older-than 30d --postgres-dsn "$PYRULA_POSTGRES_DSN" --dry-runrequeue puts a turn the worker quarantined as poison back in the queue. gc deletes archived turns past a cutoff; --dry-run counts them first. Scheduled runs: Schedules.
reproject
Section titled “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).
pyrula reproject --agent support # one agentpyrula reproject --agent support --run-id <id> # one run (--run-id requires --agent)pyrula reproject # every archived run