Quickstart with Python

Run two calculations in parallel, then combine their results. The calculations are deliberately small: you can try a workflow without API keys or an existing application. You'll need Python, a package manager, and Docker for local Postgres.

For an existing deployment, see Production Deployment.

Install

Waymark ships as a single Python package. Add it to your project with whichever package manager you prefer:

uv add waymark

The package includes the Python SDK for writing workflows and the Rust runtime that executes them. Start the runtime with uv run waymark-executor.

Define a workflow

A workflow decides when actions run and how their results feed the next step. Here, it starts factorial and Fibonacci calculations together, then passes both results to a summary action. Save the workflow and the actions below in workflows.py at the root of your project.

import asyncio
from waymark import Workflow, workflow

@workflow
class ParallelMathWorkflow(Workflow):
    async def run(self, number: int) -> str:
        # Fan out: both computations run in parallel.
        factorial_value, fibonacci_value = await asyncio.gather(
            compute_factorial(number),
            compute_fibonacci(number),
            return_exceptions=False,
        )
        # Fan in: one action merges the results.
        return await summarize(
            n=number,
            factorial_value=factorial_value,
            fibonacci_value=fibonacci_value,
        )

Waymark compiles run() into a durable program. asyncio.gather starts the two actions in parallel; the next action waits for both results. The compiler requires return_exceptions=True here.

The calculations themselves are ordinary async Python functions marked with @action. Each one runs in a worker.

from waymark import action

@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

The summary action formats the two results:

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

You can change the workflow and actions together as you develop. The first time a process runs or schedules the workflow, Waymark compiles it and reports unsupported patterns.

Setting up your env

Waymark stores queued work and progress in Postgres. Start a local database with these development credentials:

docker run -d --name waymark-postgres \
  -p 5432:5432 \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=waymark \
  postgres:16

Then write a .env file with the connection string and the module where your actions live:

export WAYMARK_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/waymark
export WAYMARK_USER_MODULE=workflows  # or "yourpkg.workflows"

Start waymark-executor from the project root so its workers can import workflows.py. For an installed package, the module can live anywhere in that environment. WAYMARK_USER_MODULE loads it at worker startup; an import error then stops startup after the 15-second worker attach timeout. Without this setting, the worker imports the module on its first action call.

Boot the node

With the environment in place, the node - the process that runs your workflows and their workers - is one command:

source .env && uv run waymark-executor

Kick off a run

You can launch this workflow from any Python process - a FastAPI route, a script, a notebook. The client process boots its own bridge to Postgres, so it needs the same environment:

source .env && uv run python
import asyncio
from workflows import ParallelMathWorkflow

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

asyncio.run(main())
7! = 5040; fib(7) = 13

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)

Conclusion

To see recovery, add a positive asyncio.sleep() between workflow steps. Stop waymark-executor during the sleep and start it again. The workflow resumes from its saved state, including the original wake-up time.

Where to look next

  • Production Deployment covers shipping this for real: one image, waymark-executor, your app, and the Postgres you already run.
  • Introduction to Python 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 retry policies for failed calls and deadlines for slow ones.
  • Configuration is the full reference for the WAYMARK_* environment variables you can set.
  • Webapp shows your runs as they happen; set WAYMARK_HTTP_ENABLED=true on the node to serve it.