Integrating with FastAPI

FastAPI is one of the most popular API libraries in Python. If you're already using it, adopting Waymark should make you feel right at home with its native typehinting, dependency injection, and simple backend.

Waymark is most commonly used to replace logic that you're currently implementing as background tasks. It also is useful for refactoring any POST requests that might be unreliable or you want to view archived progress as they continue. If the work should retry, survive a deploy, show up in run history, or outlive a normal request timeout, hand it to a workflow.

Whenever it's not a super simple database query, format, and return - you might want to consider Waymark.

Project layout

Let's start with the world's simplest FastAPI app. We can ship the code side by side because it's helpful to have your workflow code importable by both processes (the normal uvicorn web process and the waymark runner).

Your FastAPI app imports it to start runs; the Waymark worker imports it to register workflows and actions.

app/
  api.py
  models.py
  workflows.py

Define the contract

Start with the data that crosses the boundary. FastAPI and Waymark both speak Pydantic well, so use the same request and response models on both sides.

# app/models.py
from pydantic import BaseModel

class ReceiptRequest(BaseModel):
    order_id: str
    email: str

class ReceiptResult(BaseModel):
    order_id: str
    email_id: str

This is the first best practice: do not let your route pass loose dicts into a background system. A typed request makes the HTTP contract obvious and gives Waymark something boring to serialize through Postgres.

Write the workflow

The email provider call is the slow, flaky part, so it belongs in an action. Here we call Resend's HTTP API directly. This does network I/O, raises on provider errors, and returns the provider's email id.

# app/workflows.py
import os

import httpx
from waymark import action

from app.models import ReceiptRequest, ReceiptResult

@action
async def send_receipt_email(request: ReceiptRequest) -> ReceiptResult:
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.post(
            "https://api.resend.com/emails",
            headers={
                "Authorization": f"Bearer {os.environ['RESEND_API_KEY']}",
                "User-Agent": "waymark-fastapi-tutorial/1.0",
            },
            json={
                "from": os.environ["RECEIPTS_FROM_EMAIL"],
                "to": [request.email],
                "subject": f"Receipt for order {request.order_id}",
                "html": f"<p>Your receipt for order {request.order_id} is ready.</p>",
            },
        )
    response.raise_for_status()
    data = response.json()
    return ReceiptResult(order_id=request.order_id, email_id=data["id"])

Now wrap that action in a workflow.

from waymark import Workflow, workflow

@workflow
class ReceiptWorkflow(Workflow):
    async def run(self, request: ReceiptRequest) -> ReceiptResult:
        return await send_receipt_email(request)

This is by definition the simplest workflow you can imagine; a single action call with no additional business logic. But even with this little code and a few decorators, you now have firm guarantees that let this function run to completion and flag errors if they occur.

Await short work

If the workflow is short enough for a normal HTTP request budget, await it and return the result directly.

# app/api.py
from fastapi import FastAPI

from app.models import ReceiptRequest, ReceiptResult
from app.workflows import ReceiptWorkflow

app = FastAPI()

@app.post("/receipts", response_model=ReceiptResult)
async def send_receipt(request: ReceiptRequest) -> ReceiptResult:
    return await ReceiptWorkflow().run(request)

This still goes through Waymark. The action can be retried by a worker, the run is visible to the runtime, and a worker crash does not turn into mystery state in your web process.

Queue long work

If the work might take longer than a client should wait, return as soon as the workflow is queued. This is the shape for imports, report generation, slow third party APIs, and agent runs.

from pydantic import BaseModel

class QueuedReceipt(BaseModel):
    workflow_instance_id: str

The route changes by one important flag: _blocking=False.

@app.post("/receipts/async", response_model=QueuedReceipt, status_code=202)
async def queue_receipt(request: ReceiptRequest) -> QueuedReceipt:
    instance_id = await ReceiptWorkflow().run(request, _blocking=False)
    return QueuedReceipt(workflow_instance_id=instance_id)

Do not use FastAPI BackgroundTasks for work you actually care about. Those tasks belong to the web process. A deploy, crash, or process restart can erase them. Waymark queues the run in Postgres and lets workers claim it durably.

Runtime setup

The Resend action uses httpx, so install it with your web dependencies if it is not already in your app.

uv add fastapi uvicorn httpx

Your API process and worker process need the same database, and the worker needs to know which module registers your workflows.

export WAYMARK_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/waymark
export WAYMARK_USER_MODULE=app.workflows
export RESEND_API_KEY=re_xxxxxxxxx
export RECEIPTS_FROM_EMAIL="Acme <onboarding@resend.dev>"

Run workers separately from the web server.

uv run waymark-start-workers

Then run FastAPI however you normally do.

uvicorn app.api:app --reload

The API process creates workflow runs. The worker pool imports app.workflows, claims action work from Postgres, and sends results back to the workflow runtime.

Production habits

  • Keep HTTP-only concerns in FastAPI: auth, headers, status codes, request parsing, and response models.
  • Keep slow I/O in actions: email, billing, imports, file processing, model calls, and anything that needs retries.
  • Keep workflow inputs and outputs as Pydantic models when they are more than a couple of scalars.
  • Keep WAYMARK_USER_MODULE pointed at the module that imports and registers every workflow/action your workers need.
  • Use _blocking=False when a client does not need the final answer in the same request.

That's the whole integration. FastAPI gets to stay a web framework. Waymark gets to own the durable work.