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

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.7
File Size Uploaded
without_durability-0.0.7.tar.gz 42.3 kB Details

Built distribution (wheel)

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

Total release size: 90.5 kB

Release files / without_durability-0.0.7.tar.gz

Download URL without_durability-0.0.7.tar.gz
Size 42.3 kB
Tags Source
SHA-256 checksum
How to use checksums
cc9a57404d669e243956aed0f2fd56898ad928c734469bfe5fce3ca05fd70fc2
BLAKE2b-256 checksum
How to use checksums
6de69d5e3619016ded214ad4a327daa72164cc3a2802f37caa2b5849bd261911
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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.7-py3-none-any.whl

Download URL without_durability-0.0.7-py3-none-any.whl
Size 48.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d6be6b52d0244684d63fabb6595a1d4e91d8a317582b15f013637d75aff57ed7
BLAKE2b-256 checksum
How to use checksums
8bad66bc2b39537af8dd1d4a0a211858299978bc9651898b6f627d988987f32c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","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

0.0.9

2 release files

0.0.8

2 release files

This release

0.0.7 This release

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