Quickstart

This walks through the smallest possible Waymark program: one workflow, two actions, run end-to-end against a local Postgres. The full example app is what you should crib from for a real deployment - this is just the shape.

Install

Waymark ships as a single Python package. Install it with whichever package manager you prefer:

uv add waymark

Two binaries land on your PATH:

  • waymark-start-workers - the worker pool that executes actions.
  • waymark-bridge - the gRPC bridge between your Python process and the Rust runtime. The Python SDK boots one of these automatically the first time you await workflow.run(...), so you usually don't run it by hand.

You'll also need a Postgres database. Anything 14+ works; for local development a Docker container is fine:

docker run --rm -p 5432:5432 \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=waymark \
  postgres:16

Define a workflow

Create workflows.py. Two actions, one workflow:

import asyncio
from waymark import Workflow, action, workflow


@action
async def compute_factorial(n: int) -> int:
    total = 1
    for value in range(2, n + 1):
        total *= value
    return total


@action
async def compute_fibonacci(n: int) -> int:
    previous, current = 0, 1
    for _ in range(n):
        previous, current = current, previous + current
    return previous


@action
async def summarize(*, n: int, factorial_value: int, fibonacci_value: int) -> str:
    return f"{n}! = {factorial_value}; fib({n}) = {fibonacci_value}"


@workflow
class ParallelMathWorkflow(Workflow):
    async def run(self, number: int) -> str:
        # Fan out: factorial and fibonacci run in parallel.
        factorial_value, fibonacci_value = await asyncio.gather(
            compute_factorial(number),
            compute_fibonacci(number),
            return_exceptions=True,
        )
        # Fan in: a single summary action returns the result.
        return await summarize(
            n=number,
            factorial_value=factorial_value,
            fibonacci_value=fibonacci_value,
        )

There's nothing magic here. @action marks distributed work - the units that get sent to a worker, retried on failure, and timed out if they overrun. @workflow marks the durable control flow - the part that gets parsed into a DAG. asyncio.gather is recognized by the compiler as a parallel fan-out; the await inside actions is just regular await.

Boot the worker pool

Point the workers at your database and at the module where the actions live:

export WAYMARK_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/waymark
export WAYMARK_USER_MODULE=workflows  # or "yourpkg.workflows"
uv run waymark-start-workers

WAYMARK_USER_MODULE tells each worker which module to preload, so it has the action handlers registered before it starts pulling from the queue.

Kick off a run

From any Python process - a FastAPI route, a script, a notebook:

import asyncio
from workflows import ParallelMathWorkflow


async def main() -> None:
    workflow = ParallelMathWorkflow()
    result = await workflow.run(7)
    print(result)


asyncio.run(main())

The first call boots a singleton waymark-bridge inside the process and queues the workflow into Postgres. The bridge waits for completion and returns the action's result. From here you can plug workflow.run(...) straight into a FastAPI handler:

from fastapi import FastAPI
from workflows import ParallelMathWorkflow

app = FastAPI()


@app.post("/compute/{number}")
async def compute(number: int) -> str:
    return await ParallelMathWorkflow().run(number)

Where to look next

  • The example app bundles a FastAPI UI, Postgres, and the worker daemon as a single docker compose up. It's a complete shape for what production deployment looks like.
  • Python authoring patterns covers the Python-specific surface - pydantic for inputs and outputs, Depends for dependency injection, and how the worker discovers your modules.
  • Workflows & Actions walks through the split between durable control flow and distributed work in more depth.
  • Control Flow covers what AST patterns the compiler supports - loops, conditionals, parallel fan-out, durable sleep, try/except.
  • Retries & Timeouts covers how to configure resilience when actions flake.
  • Configuration is the full reference for the WAYMARK_* environment variables you can set.