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.

Quickstart

Install pyrula-workflows[ui], then scaffold a runnable workflow:

Terminal window
pyrula init --workflow
pyrula dev --open

The packages require a source build until their first public release. See Install for the current commands.

pyrula init --workflow writes app.py: a checkout workflow that reserves inventory, charges a card, suspends for two seconds, and sends a receipt. Every side effect goes through ctx.step, so a resumed run reads back what already happened instead of repeating it. The getting-started quickstart shows the generated file, console output, and event stream.

The workflow body takes a context (ctx). Decorated steps record their results for replay.

import asyncio
from pyrula.workflows import step, workflow, InlineWorkflowRunner, MemoryStore
@step
def build(name: str) -> str:
return f"hello, {name}"
@workflow(name="greet")
async def greet(ctx, name: str) -> str:
return await build(name)
async def main() -> None:
runner = InlineWorkflowRunner(store=MemoryStore())
run = await runner.client.submit("greet", {"name": "ada"})
print(await run.result()) # hello, ada
for event in await run.events():
print(event.kind, event.payload)
asyncio.run(main())

submit queues a run and returns a handle. result() executes it in this process and returns its value. events() returns the lifecycle and step events. MemoryStore keeps the stream in process for tests; a Valkey or Redis store preserves it across restarts.

On replay, a completed step returns its recorded result instead of running again. See Steps for the API and Determinism for the replay contract.

@workflow accepts execution and event-stream limits. timeout bounds one execution attempt; the remaining options prevent unbounded event growth.

@workflow(
name="report",
timeout=600.0, # wall-clock cap per attempt
max_event_count_per_run=10_000,
max_event_payload_bytes=1_000_000,
max_attempt_wall_seconds=3600.0, # also per attempt, NOT a whole-run budget
)
async def report(ctx):
...

Next: Runs as Event Streams.