# Quickstart

> Scaffold a durable workflow, run it, and watch it survive a suspend.

Source: https://docs.pyrula.io/getting-started/quickstart/

Scaffold a workflow, run it on a local SQLite store, open its event stream. No broker, key or account.

```bash
pip install "pyrula[workflows]"
pyrula init --workflow
pyrula dev app.py --open
```

Not on PyPI yet: build from source ([Install](https://docs.pyrula.io/getting-started/install.md)) and prefix the other
two with `uv run`.

## Generated workflow

```
wrote app.py
wrote .gitignore

next:  pyrula dev app.py --open
```

`app.py` is a small checkout workflow:

```python
from pyrula.workflows import WorkflowContext, step, workflow

# Counts real executions so the result payload can show what was not repeated.
# This is a deliberately process-local demonstration counter, not durable
# state: it lives in this worker's memory, not the journal. If a different
# worker process resumes this run (e.g. after this one crashes or restarts),
# the counts reset to zero in the new process even though the run itself
# picks up right where it left off.
CALLS = {"reserve": 0, "charge": 0, "receipt": 0}


@step
def reserve_inventory() -> dict:
    CALLS["reserve"] += 1
    return {"sku": "widget", "qty": 2}


@step
def charge_card() -> dict:
    CALLS["charge"] += 1
    return {"amount": 4200}


@step
def send_receipt(order: str) -> dict:
    CALLS["receipt"] += 1
    return {"order": order, "calls": dict(CALLS)}


@workflow(name="checkout")
async def checkout(ctx: WorkflowContext, order: str = "A-1042") -> dict:
    await reserve_inventory()
    await charge_card()

    # A durable sleep, not asyncio.sleep: the run suspends, the worker slot is
    # freed, and the run resumes later from the journal.
    await ctx.sleep(2)

    return await send_receipt(order)
```

`@step` records a function's result. `ctx.sleep` suspends the run instead of blocking a thread.

## Run the workflow

`pyrula dev app.py --open` starts a local server, runs the workflow once and opens the dev UI:

```
=== Pyrula Dev Server ===
Target: app.py:checkout
Using SQLite store .pyrula/dev.db - runs survive a restart; --fresh starts empty
Cloud: off (no PYRULA_CLOUD_API_KEY) - running locally only
UI: http://127.0.0.1:7978/ui

Press Ctrl+C to stop

  reserve_inventory  done
  charge_card  done
  suspended - worker slot freed
  resumed from journal
  send_receipt  done
  completed - {'order': 'A-1042', 'calls': {'reserve': 1, 'charge': 1, 'receipt': 1}}
Saved the last run as .pyrula/last/checkout.case. It holds the run's inputs, every event and every recorded model and tool result, in full; .pyrula/ is local state, keep it out of version control.
```

The run suspends at `ctx.sleep(2)` and resumes at `send_receipt`. Each step ran once: the card was not charged twice.

## Inspect the event stream

`http://127.0.0.1:7978/ui` lists runs; opening one shows its result and event stream:

```
complete    started 27s ago    0 ms (last attempt)
{'order': 'A-1042', 'calls': {'reserve': 1, 'charge': 1, 'receipt': 1}}

  run:init
  step:pending   reserve_inventory
  step:done      reserve_inventory
  step:pending   charge_card
  step:done      charge_card
  run:sleeping   suspended until 17:54:43
  run:woken      resumed at 17:54:43
  step:pending   send_receipt
  step:done      send_receipt
  run:complete   result: {'order': 'A-1042', 'calls': {...}}
```

Every row expands to its payload. This is the journal the engine replays from. The duration
says `last attempt` because a resumed run's final attempt mostly replays the journal.

## Changing it

`pyrula dev` loads your code once: edit `app.py`, Ctrl-C, start again. Each start submits a fresh run.

## Where the runs are kept

Runs live in `.pyrula/dev.db`, so history survives a restart and a run stopped mid-sleep
finishes when `pyrula dev` starts again.

```bash
pyrula dev --fresh                          # delete the file first
pyrula dev --store memory://                # keep runs in the process only
pyrula dev --store valkey://localhost:6379
pyrula dev --attach --store sqlite:///.pyrula/dev.db   # browse runs, run nothing
```

For a real worker crash see [Surviving worker loss](https://docs.pyrula.io/workflows/guides/surviving-worker-loss.md).

## Further reading

- [Workflows](https://docs.pyrula.io/workflows.md): steps, events, replay.
- [Agents](https://docs.pyrula.io/agents.md): an LLM loop on the same engine.
- [Running a worker](https://docs.pyrula.io/workflows/guides/worker.md) and [Deployment modes](https://docs.pyrula.io/workflows/concepts/deployment-modes.md) for production.
- Core types: [Either](https://docs.pyrula.io/primitives/either.md) and [Option](https://docs.pyrula.io/primitives/option.md).
