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

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.8
File Size Uploaded
without_durability-0.0.8.tar.gz 45.5 kB Details

Built distribution (wheel)

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

Total release size: 97.0 kB

Release files / without_durability-0.0.8.tar.gz

Download URL without_durability-0.0.8.tar.gz
Size 45.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b2b236f0c711c5bf9f01fd0aeddb6575c9416f5e3ece36f9304216ff0007416b
BLAKE2b-256 checksum
How to use checksums
f31916df59cb1db33e9b7825d2132f0e3f147c51a4d7280c0aba94486dc4cef9
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.8-py3-none-any.whl

Download URL without_durability-0.0.8-py3-none-any.whl
Size 51.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd9ab5ed1b77dbf72cd4c0fc9c9757e637b369bb6b3cd0a3ddce0bd522a4deeb
BLAKE2b-256 checksum
How to use checksums
cc003d055bab279c1a8a73fa9d076aa90480240dae7a5122b054113cb4e2e388
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

This release

0.0.8 This release

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