A durable, checkpointed workflow engine for chaining Claude-powered steps, with human-in-the-loop approval gates and a full audit trail.
Agentic automation that runs unattended needs three things a plain prompt loop doesn't give you:
- Durability — if the process dies mid-workflow, you should be able to resume from the last completed step, not from scratch.
- Human checkpoints — some steps (publishing, sending, spending money) shouldn't happen without a person saying yes, and that "yes" might come hours later, from a different process entirely.
- An audit trail — a record of what ran, when, what it produced, and who approved what — independent of whatever logs the LLM calls themselves produce.
waypoint is a small engine built around exactly those three properties, not a general-purpose orchestrator.
A workflow is a YAML list of steps. Two step types:
name: research-brief
steps:
- type: llm
name: draft
system: You are a concise technical writer.
prompt: Write a 3-sentence brief on X.
- type: approval
name: review
message: Review the draft before it gets polished.
- type: llm
name: polish
prompt: |
Polish this for publication:
{{steps.draft.output}}waypoint run examples/research_brief.yaml
# Started run 3f9a2b1c...
# ┌────────┬────────────────────┬─────────────────┐
# │ Step │ Status │ Output / Error │
# ├────────┼────────────────────┼─────────────────┤
# │ draft │ completed │ A durable... │
# │ review │ awaiting_approval │ │
# └────────┴────────────────────┴─────────────────┘
# Run status: awaiting_approval
waypoint approve 3f9a2b1c... examples/research_brief.yaml
# ... polish step runs, run status: completed{{steps.<name>.output}} in a prompt is substituted with that step's completed output, so later steps can build on earlier ones.
models.py — WorkflowSpec, StepSpec (LLMStepSpec | ApprovalStepSpec, discriminated union),
RunState, StepResult, AuditEvent (all pydantic)
store.py — StateStore protocol; SQLiteStateStore (default) + InMemoryStateStore (tests)
claude_client.py — ClaudeClient protocol; AnthropicStepRunner (real API, adaptive thinking, streaming)
engine.py — WorkflowEngine: start / resume / approve, the checkpoint + approval-gate logic
cli.py — Typer CLI over the engine
Checkpointing: after every completed step, WorkflowEngine writes the full run state to the StateStore before moving to the next step. resume() reloads that state and skips any step already marked completed — so a crash between steps loses nothing, and a crash during a step just re-runs that one step.
Approval gates: an approval step doesn't call Claude at all — it marks itself awaiting_approval, persists that, and the engine returns. Nothing continues until a separate call to approve() (a different process, a different day) either completes the gate and resumes the workflow, or rejects it and fails the run. The audit log records the request and the decision independently of the run state.
Audit trail: every state transition (run_started, step_started, step_completed, step_failed, approval_requested, approval_granted, approval_denied, run_completed) is appended to a separate, append-only log keyed by run ID — queryable via waypoint audit <run_id> regardless of the run's current status.
ClaudeClient and StateStore are both protocols with a real implementation and a fake/in-memory one — the engine's tests run entirely against fakes (fast, deterministic, no API key needed), while AnthropicStepRunner is exercised in a real end-to-end run before every push.
Requires an ANTHROPIC_API_KEY in the environment.
pip install -e .
waypoint run examples/research_brief.yaml
waypoint status <run_id>
waypoint approve <run_id> examples/research_brief.yaml
waypoint resume <run_id> examples/research_brief.yaml # after a crash/restart
waypoint audit <run_id>pip install -e ".[dev]"
ruff check .
mypy src
pytest --cov=waypoint- Steps run strictly sequentially — no parallel branches or conditionals. This is intentional scope: the point of this project is durable sequencing and approval gates, not a general DAG scheduler.
- The engine and CLI have been run end-to-end against the real Anthropic API and correctly handle both the request path and API-level failures (verified: a 400 from a real request was caught, the step and run were marked
failed, and the state persisted correctly). A full happy-path run (successful completion through an approval gate) has not yet been exercised against a live account and is the natural next verification step. - Template substitution is a single pattern,
{{steps.<name>.output}}; there's no expression language. - A workflow's structure (the YAML) is not itself versioned in the run state — if you edit the YAML between
start()andresume(), the engine reconciles by step name, not by position, so renaming a step effectively creates a new one.
MIT