Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

waymark

Waymark Logo

waymark is a library to let you build durable background tasks that withstand server restarts, task crashes, and long-running jobs. It's built for Python and Postgres without any additional deploy time requirements. More languages are coming soon.

Documentation: waymark.sh

Getting started:

Running Waymark:

Python:

Usage with Python

We ship all client and server binaries in one Python package. Install it via your package manager of choice:

uv add waymark

Let's say you need to send welcome emails to a batch of users, but only the active ones. You want to fetch them all, filter out inactive accounts, then fan out emails in parallel. Actions are the distributed work - plain async functions, sent to your workers:

from typing import Annotated

from waymark import Depends, action

@action
async def fetch_users(
    user_ids: list[str],
    db: Annotated[Database, Depends(get_db)],
) -> list[User]:
    return await db.get_many(User, user_ids)

@action
async def send_email(
    to: str,
    subject: str,
    emailer: Annotated[EmailClient, Depends(get_email_client)],
) -> EmailResult:
    return await emailer.send(to=to, subject=subject)

The workflow is the durable control flow that orchestrates them:

import asyncio

from waymark import Workflow, workflow

@workflow
class WelcomeEmailWorkflow(Workflow):
    async def run(self, user_ids: list[str]) -> list[EmailResult]:
        users = await fetch_users(user_ids)
        active_users = [user for user in users if user.active]

        results = await asyncio.gather(
            *[
                send_email(to=user.email, subject="Welcome")
                for user in active_users
            ],
            return_exceptions=True,
        )

        return results

Run the node - the process that executes workflows and their actions - against your database:

export WAYMARK_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/waymark
export WAYMARK_HTTP_ENABLED=true  # the webapp, on http://localhost:24119
uv run waymark-node

Then kick off the workflow from any Python process, and wait for its result:

async def welcome_users(user_ids: list[str]):
    await WelcomeEmailWorkflow().run(user_ids)

None of this executes inline in your webserver: the run is queued to Postgres and executed by the node.

Actions are the distributed work: network calls, database queries, anything that can fail and should be retried independently.

Workflows are the control flow: loops, conditionals, parallel branches. They orchestrate actions but don't do heavy lifting themselves.

What you can write

Workflows are plain async Python. A few of the things they can do:

  1. Retries and timeouts, per call. By default an action runs once, like a regular function call. Wrap a call in self.run_action(...) to give it a retry policy and a timeout - see Retries & Timeouts.

    from datetime import timedelta
    
    from waymark import RetryPolicy
    
    async def run(self, order_id: str) -> Receipt:
        return await self.run_action(
            charge_card(order_id),
            retry=RetryPolicy(attempts=5, backoff_seconds=10),
            timeout=timedelta(minutes=2),
        )
    
  2. Branches and loops. if/elif/else, for and while compile into the workflow program and are executed by the runtime, just like your actions.

    async def run(self, order_ids: list[str]) -> list[str]:
        shipped = []
        for order_id in order_ids:
            status = await fetch_status(order_id)
            if status == "shipped":
                shipped.append(order_id)
        return shipped
    
  3. Parallel fan-out and durable sleep. asyncio.gather runs actions in parallel; asyncio.sleep pauses the workflow durably, surviving restarts - for a second or for a day.

    import asyncio
    
    async def run(self, user_id: str) -> str:
        profile, history = await asyncio.gather(
            fetch_profile(user_id),
            fetch_purchase_history(user_id),
            return_exceptions=True,
        )
    
        # wait a day before following up
        await asyncio.sleep(24 * 60 * 60)
        return await send_recommendations(profile, history)
    
  4. Schedules. Run a workflow on a cron expression or a fixed interval, with no extra infrastructure - see Scheduled Workflows.

    await schedule_workflow(DataSyncWorkflow, schedule_name="hourly", schedule="0 * * * *")
    
  5. A built-in webapp. waymark-node serves a webapp showing every workflow instance, each one's timeline of calls, and the load on your nodes.

Philosophy

Background jobs in webapps are so frequently used that they should really be a primitive of your fullstack library: database, backend, frontend, and background jobs. Otherwise you're stuck in a situation where users either have to always make blocking requests to an API or you spin up ephemeral tasks that will be killed during re-deployments or an accidental docker crash.

After trying most of the ecosystem in the last 3 years, I believe background jobs should provide a few key features:

  • Easy to write control flow in normal Python
  • Should be both very simple to test locally and very simple to deploy remotely
  • Reasonable default configurations to scale to a reasonable request volume without performance tuning

On the point of control flow, we shouldn't be forced into a DAG definition (decorators, custom syntax). It should be regular control flow just distinguished because the flows are durable and because some portions of the parallelism can be run across machines.

Nothing on the market provides this balance - waymark aims to try. We don't expect ourselves to reach best in class functionality for load performance. Instead we intend for this to scale most applications well past product market fit.

How It Works

Waymark takes a different approach from replay-based workflow engines like Temporal or Vercel Workflow.

Approach How it works Constraint on users
Temporal/Vercel Workflows Replay-based. Your workflow code re-executes from the beginning on each step; completed activities return cached results. Code must be deterministic. No random(), no datetime.now(), no side effects in workflow logic.
Waymark Compile-once. Parse your Python AST → intermediate representation → bytecode. A durable VM executes the bytecode. Your code never re-runs. Code must use supported patterns. But once compiled, the runtime always knows exactly where the workflow is in its execution.

The first time a workflow is run or scheduled, the Python SDK parses its run() method's AST and compiles it to an intermediate representation (IR). This IR captures your control flow - loops, conditionals, parallel branches - and is lowered to bytecode for a durable virtual machine. The Rust runtime executes the bytecode and snapshots the VM's state to Postgres as it goes, so a workflow resumes from where it was after a crash. Your original Python run() definition is never re-executed during workflow recovery.

This is convenient in practice because it means that if your workflow compiles, your workflow will run as advertised. There's no need to hack around stdlib functions that are non-deterministic (like time/uuid/etc) because you'll get an error on compilation to switch these into an explicit @action. The few patterns that currently slip past the compiler are listed under known issues and tracked in #772.

When to use it

When should you use Waymark?

  • You're already using Python & Postgres for the core of your stack, either with Mountaineer or FastAPI
  • You have a lot of async heavy logic that needs to be durable and can be retried if it fails (common with 3rd party API calls, db jobs, etc)
  • You want something that works the same locally as when deployed remotely
  • You want background job code to plug and play with your existing unit test & static analysis stack
  • You are focused on getting to product market fit versus scale

Performance is a top priority of waymark. That's why it's written with a Rust core and runs continuous benchmarks on CI. But it's not the only priority. After all there's only so much we can do with Postgres as an ACID backing store. Once you start to tax Postgres' capabilities you're probably at the scale where you should switch to a more complicated architecture.

When shouldn't you?

  • You have particularly latency sensitive background jobs, where you need <100ms acknowledgement and handling of each task.
  • You have a huge scale of concurrent background jobs, order of magnitude >10k actions being coordinated concurrently.
  • You have tried some existing task coordinators and need to scale your solution to the next 10x worth of traffic.

There is no shortage of robust background queues in Python, including ones like Temporal.io/RabbitMQ that scale to millions of requests a second.

Almost all of these require a dedicated task broker that you host alongside your app. This usually isn't a huge deal during POCs but can get complex as you need to performance tune it for production. Cloud hosting of most of these are billed per-event and can get very expensive depending on how you orchestrate your jobs. They also typically force you to migrate your logic to fit the conventions of the framework.

Open source solutions like RabbitMQ have been battle tested over decades & large companies like Temporal are able to throw a lot of resources towards optimization. Both of these solutions are great choices - just intended to solve for different scopes. Expect an associated higher amount of setup and management complexity.

Project Status

If you have a particular workflow that you think should be working but isn't yet compiling correctly, please file an issue.

Building from source

The webapp is compiled into waymark-node, so building from source requires Node.js (version in .node-version) and npm:

make js-deps
cargo build --bin waymark-node

Contributing

If you want to contribute, check out the contributing guidelines.

Metadata

Release files for waymark 0.30.0.dev14

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for waymark 0.30.0.dev14
File Size Uploaded
waymark-0.30.0.dev14.tar.gz 88.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for waymark 0.30.0.dev14
File
waymark-0.30.0.dev14-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
waymark-0.30.0.dev14-py3-none-manylinux_2_39_x86_64.whl Python 3 none Linux glibc 2.39+ x86-64 Details
waymark-0.30.0.dev14-py3-none-manylinux_2_39_aarch64.whl Python 3 none Linux glibc 2.39+ ARM64 Details
waymark-0.30.0.dev14-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 124.3 MB

Release files / waymark-0.30.0.dev14.tar.gz

Download URL waymark-0.30.0.dev14.tar.gz
Size 88.2 kB
Tags Source
SHA-256 checksum
How to use checksums
907e9381002c5498398dac59aba5ec29eadb864724b6b6ac551c1f43b05db108
BLAKE2b-256 checksum
How to use checksums
174ab2f9c7b170f1fa64b13a22538d04dd454a256bab6c799a30c425aeef2903
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / waymark-0.30.0.dev14-py3-none-win_amd64.whl

Download URL waymark-0.30.0.dev14-py3-none-win_amd64.whl
Size 35.8 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
ab552e3e032788bb623f9a6d755ce13c9974e097f2798a1a8a831eb8d978996f
BLAKE2b-256 checksum
How to use checksums
b0af79b288f2481b2cf72a9b32fb5080306952f5fe4fbf3b62db7e308a658348
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / waymark-0.30.0.dev14-py3-none-manylinux_2_39_x86_64.whl

Download URL waymark-0.30.0.dev14-py3-none-manylinux_2_39_x86_64.whl
Size 23.6 MB
Tags Linux glibc 2.39+ x86-64 Python 3
SHA-256 checksum
How to use checksums
cc4845f3b00b084b40873989e65adb72f09a37db2e9b921d49ec91f128de5795
BLAKE2b-256 checksum
How to use checksums
04df7d2d14631890f75ea47186eb2042fae3256f157ec7a524fe941919bcec8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / waymark-0.30.0.dev14-py3-none-manylinux_2_39_aarch64.whl

Download URL waymark-0.30.0.dev14-py3-none-manylinux_2_39_aarch64.whl
Size 23.5 MB
Tags Linux glibc 2.39+ ARM64 Python 3
SHA-256 checksum
How to use checksums
185b66c3f07356fb4c231c678b39938d1fa40fdd84b415713be3923e64f778ef
BLAKE2b-256 checksum
How to use checksums
2c0a58388c6805759b1c4ffe057ac70e530d5a9f71a53d301f85028394e53ab6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / waymark-0.30.0.dev14-py3-none-macosx_11_0_arm64.whl

Download URL waymark-0.30.0.dev14-py3-none-macosx_11_0_arm64.whl
Size 41.3 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
ce4bf46f661e20cccf5229e2827d7b5a43289858d377d63835b40e1037201d54
BLAKE2b-256 checksum
How to use checksums
2e502656bbc949bcd4f6cdcedd4a521f6ba2bc993b4102c8d78ce72f9787deee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.30.0.dev14 This release

5 release files

0.22.0

5 release files

0.20.5

5 release files

0.20.4

5 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page