Skip to main content

without-durability

Durable workflows built on the observation that once a workflow's state is a checkpoint any process can read, most of a workflow engine stops being necessary. A workflow is an ordinary async function whose effects are named; resuming means calling it again; each step it reaches hands back what is already recorded instead of running.

from without_durability import Blocked, Completed, MemoryCheckpointer, Run, Sleeping, claimed, resume


def as_text(recorded: object) -> str:
    if not isinstance(recorded, str):
        raise TypeError(f"{recorded!r} is not the reference this step recorded")
    return recorded


async def fulfil(run: Run) -> str:
    charge = await run.step("charged", lambda: gateway.charge(order), as_text)
    await run.sleep("settling", timedelta(days=3))  # survives a crash on day two
    approver = await run.awaiting("approved-by", as_text)  # another process writes this
    return await run.step("paid", lambda: gateway.pay(charge, approver), as_text)


checkpointer = MemoryCheckpointer()
match await resume(await claimed(checkpointer, "order-42"), checkpointer, fulfil):
    case Completed(value=reference):
        ...  # the workflow finished
    case Sleeping(due=due):
        ...  # schedule a wakeup for `due`
    case Blocked(waiting=keys, listening=inboxes):
        ...  # nothing to schedule; `arrive` at any of `keys`, or `deliver` a message

A pass comes back as one of those three rather than raising the suspensions, so what to do next is a match a type checker can tell you is incomplete. Blocked carries every branch that stopped rather than a representative, because a fan-out is blocked on all of them at once and any one of the writes advances it. Inside the workflow a suspension is still an exception, because that is how you stop in the middle of straight-line code; resume is the boundary where it becomes a value.

run.awaiting is one named value from outside, written once. For input a workflow cannot name in advance there is an inbox instead: deliver appends a message and makes the workflow ready, run.receive reads past a cursor and lands in Blocked.listening when there is nothing new, and an entry is an ordinary checkpoint record, so it renders out of load and forks by being copied like anything else.

Each read names a parser because a step hands back what the store holds, not what its effect returned: the value has been through a codec, so a step returning a tuple is handed a list on the pass that ran it. A parser makes the return type something a function proved rather than something a cast asserted.

There is no server here and no engine. What there is instead is an interface, and the interface is where the interesting part lives.

A protocol of load and record is too weak to run a workflow safely at any scale, because it cannot say "only if nobody else is running this" or "only if I am still the one who may write". So Checkpointer states the requirements and an implementation says how it meets them: claim grants at most one live pass and issues strictly increasing fencing tokens, record refuses a write from a superseded pass and never overwrites a recorded step. That is the same problem Temporal answers with a server and DBOS answers by requiring Postgres; here it is stated as an interface, so a deployment brings whatever store it already runs. Scheduler is the other half of a workflow's state, its right to run, and Durable is the pair plus the transitions that have to cross both at once.

Stores live in their own packages, so this one depends on nothing but without and without-dag:

MemoryCheckpointer and MemoryScheduler ship here too, so a test injects a dict rather than starting a container.

The same interface carries a second mechanism: run_durably runs a without-dag CompiledGraph against it, recording each (node key, result) before pulling the next. A saga needs no third one, since a rollback is another graph run through the same call. work(durable, body) turns either into a running service, a pool of passes plus a timer, with backpressure that falls out of pulling one delivery per free slot.

See the without-durability guide (with the API reference) for the full surface: the two mechanisms, the worker, Run.transact and the exactly-once step it buys, what each store can and cannot promise, and the gaps that keep this a substrate rather than a replacement for Temporal or DBOS.

Metadata

Release files for without-durability 0.0.9

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

Source distribution (sdist)

Source distribution for without-durability 0.0.9
File Size Uploaded
without_durability-0.0.9.tar.gz 53.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for without-durability 0.0.9
File Interpreter ABI Platform
without_durability-0.0.9-py3-none-any.whl Python 3 none any Details

Total release size: 113.3 kB

Release files / without_durability-0.0.9.tar.gz

Download URL without_durability-0.0.9.tar.gz
Size 53.4 kB
Tags Source
SHA-256 checksum
How to use checksums
affd5263c049628277bc311a56e0756877c4cc07c1e6c36261dc2fd3af59ecbf
BLAKE2b-256 checksum
How to use checksums
8b3657a7d69de5c8fbbd5fd948d967deb9722eec546b741bba79d8c559db1782
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / without_durability-0.0.9-py3-none-any.whl

Download URL without_durability-0.0.9-py3-none-any.whl
Size 59.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
324920864b169971f0df829c3261f9f0810f771613f6fe723d2495a9653e1f10
BLAKE2b-256 checksum
How to use checksums
36285d2e173daa7cac77c1a2adbc5f7615ca8dcdf856b7352ae419d909262ebe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.0.9 This release

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.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