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
If the script hangs instead of printing, check that waymark-executor is
running against the same database. If it raises unable to boot waymark server, the client's bridge did not start: check that Postgres is up,
that this shell has sourced .env, and that waymark-executor has
started once against the same database (it runs the migrations the
bridge checks for). The error does not show the bridge's reason yet
(#820). If it
raises WorkflowFailedError instead, an action failed; the error carries
the action's exception.
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,
Dependsfor 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=trueon the node to serve it.