A Python SDK for building fault-tolerant multi-step workflows via event sourcing.
When a workflow crashes mid-execution, Waypoint resumes from the last successful step, without re-running expensive operations that already completed. Every step's input and output is logged to an append-only PostgreSQL journal; on recovery, the journal is replayed from the last checkpoint.
- Docker + Docker Compose
- uv (Python 3.13+)
git clone git@github.com:aybruhm/waypoint.git
cd waypoint
make upThis starts the API gateway on http://localhost:9654 and PostgreSQL. The gateway auto-reloads on code changes.
make run_migrations# 3-step search workflow with crash recovery
uv run python -m sdk.examples.simple_agent
# Mocked LLM + crash recovery demo
uv run python -m sdk.examples.agent_with_llm_mock
# Nightly ETL sync: cache=True on expensive external API fetch
uv run python -m sdk.examples.data_pipeline
# E-commerce order pipeline: WaypointSession + cache on payment to prevent double-charge
uv run python -m sdk.examples.order_processing
# RAG pipeline: @waypoint.checkpoint instance decorator + class-based steps
uv run python -m sdk.examples.document_ragSee sdk/examples/ for the full source of each example, including crash recovery demos.
make down| Command | Description |
|---|---|
make up / make start |
Build & start containers (detached) |
make down / make stop |
Stop & remove containers |
make run_migrations |
Apply pending Alembic migrations |
make revert_migrations |
Roll back last migration |
make add_migration MSG="msg" |
Auto-generate new migration |
make show_current_db_head |
Show current migration version |
make show_db_heads |
List all migration heads |
Any multi-step process where re-running from scratch is expensive, slow, or dangerous:
- Wasted computation: Steps that succeeded before the crash get re-run on retry: re-fetching from slow APIs, re-processing records, re-calling LLM endpoints.
- Lost state: No record of which step failed, what the inputs were, or how far execution got.
- Duplicate side effects: Retrying a workflow that already charged a card, sent an email, or wrote to an external system creates duplicates or breaks idempotency.
Waypoint avoids all three by persisting every step's result. On crash, resume from the checkpoint: completed steps return their stored outputs instantly and execution continues from the next uncompleted step.
Workflow Code
↓
@checkpoint decorators (Waypoint SDK)
↓
+----------------+-----------------+------------------+
| Event Journal | Checkpoint Mgr | Replay Engine |
| (append-only) | (progress) | (deterministic) |
+----------------+-----------------+------------------+
↓
PostgreSQL
| Concept | Description |
|---|---|
| Execution | A single run of a workflow, identified by a UUID. |
| Step | A decorated async function (@checkpoint("name")). Each step runs at most once per execution. |
| Checkpoint | A persisted record of a step's input/output + execution position. |
| Event Journal | Append-only log of all steps across all executions (PostgreSQL). |
| Replay | Reconstructing state by reading checkpoints in order, without re-executing steps. |
@checkpoint("step_name", cache=True)
async def my_step(input: dict) -> dict:
return outputThe decorator:
- Checks if a checkpoint exists for this step in the current execution.
- If yes and
cache=True: returns stored output immediately (no re-execution). - If no: runs the function, persists input/output as a checkpoint, returns output.
On crash, create a new Waypoint instance and call resume(execution_id). The SDK rebuilds state from the journal and continues from the next uncompleted step.
Standalone decorator: simplest, steps registered at module level:
waypoint = Waypoint(base_url=..., workflow_id="my_pipeline").use()
@checkpoint("fetch_data", cache=True)
async def fetch_data(url: str) -> dict: ...Instance decorator: steps explicitly bound to a specific Waypoint instance:
waypoint = Waypoint(base_url=..., workflow_id="my_pipeline")
@waypoint.checkpoint("fetch_data", cache=True)
async def fetch_data(url: str) -> dict: ...Session context manager: scopes a series of steps to a single execution, useful when steps are defined as plain functions rather than decorated at import time:
async with waypoint.session(execution_id) as sess:
result = await sess.async_execute("step_name", {"arg": val, "__callable__": fn}, cache=True)- Deterministic replay: Same inputs always produce the same outputs; completed steps never re-execute.
- Step output caching: Outputs marked
cache=Trueare served from the journal on replay (zero re-computation cost). - Non-idempotent step protection: Mark payment charges, email sends, or any one-shot side effect as
cache=Trueto guarantee they run at most once across retries. - Framework-agnostic: Works with LangChain, CrewAI, FastAPI, plain asyncio, or any async Python code.
- Minimal integration: Add
@checkpointto each step. ~3 lines of change per step. - Full history: Query every step, error, duration, and state transition by execution ID.
- Long-running workflows (minutes to hours) where restarting from scratch is expensive.
- Cost-sensitive pipelines where re-running steps wastes money (LLM API calls, paid data sources, cloud processing).
- Financial or transactional workflows where duplicate side effects (double charges, duplicate sends) must be prevented.
- ETL jobs where the extraction phase is slow or rate-limited and re-fetching on failure is unacceptable.
- Teams needing a full audit trail of step inputs, outputs, and errors for debugging.
- Distributed/multi-machine workflows (Waypoint is single-process per execution).
- Stateless, fire-and-forget tasks with no recovery requirement: if a task fails you're happy restarting it from scratch, use Celery, SQS, or similar.
- Simple linear scripts with no expensive steps and no side effects worth protecting.