Runs as Event Streams
A run of a @workflow is one append-only stream of events in the store. The same
stream does two jobs: it’s the log a worker replays to recover, and it’s the feed a
client reads to follow the run live.
The event stream is the source of truth. If a secondary index disagrees with it, the stream wins.
Events
Section titled “Events”Events are RunEvent records tagged with an EventKind. Lifecycle kinds describe the
run; step kinds describe individual durable operations.
from pyrula.workflows import RunEvent, EventKind, RunStatus| Group | Kinds |
|---|---|
| Lifecycle | run:init, run:complete, run:error, run:cancel, run:continued, run:requeued, run:interrupted, run:resumed, run:sleeping, run:woken, run:pause_requested, run:paused, run:unpaused, run:signal_waiting, run:signal_received |
| Updates | run:update_received, run:update_completed, run:update_failed |
| Step | step:pending, step:done, step:error |
| Progress | run:status (from ctx.emit_status(text)), plus whatever you ctx.emit |
| Internal | heartbeat, stream:overflow, patch_marker |
RunStatus is derived from the stream: pending,
in_progress, complete, cancelled, error, quarantined, interrupted,
sleeping, signal_waiting.
The worker’s replay log and a client’s live feed are the same stream. Read it back with
store.load_run:
@workflow(name="onboard_user")async def onboard_user(ctx, email: str) -> str: account_id = await ctx.step("create_account", lambda: create_account(email)) await ctx.step("send_welcome", lambda: send_welcome_email(account_id)) return account_id
store = MemoryStore()runner = InlineWorkflowRunner(store=store)await run.result()
for event in await run.events(): print(event.kind, event.payload.get("result"))# run:init None# step:pending None# step:done acct_42 <- create_account result# step:pending None# step:done None <- send_welcome result# run:complete acct_42Append-only recovery
Section titled “Append-only recovery”An ordered, append-only log means any worker can rebuild the exact in-memory state of a run by reading it from the start. No separate snapshot, no checkpoint format to keep in sync. That property is what makes replay work.