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

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

Terminal window
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). pyrula dev --check prints environment checks and a Cloud probe, then exits.

Terminal window
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); --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.

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

Terminal window
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:

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.

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.

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.

Terminal window
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.

Terminal window
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.

Shows, sets or clears the project and environment a store belongs to (One store per project and environment).

Terminal window
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.

Terminal window
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 back in the queue. gc deletes archived turns past a cutoff; --dry-run counts them first. Scheduled runs: Schedules.

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

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

View this page as Markdown