Loams Durable
Durable promises and tasks for agent runs and long operations, served by a Resonate server embedded in the loams binary.
Loams Durable is durable execution inside Loams. A function written with a Resonate SDK records every step it takes as a durable promise. If the process running it dies, another worker picks the work up, replays the function, gets the recorded results of the finished steps back, and carries on from the first step that did not finish. Nothing that already completed runs again.
It is not our own engine. Loams embeds the Resonate server (Apache-2.0) in the loams binary and speaks the Resonate protocol unchanged, so the official Resonate SDKs work against it with only a URL change. The protocol, the SDKs, the storage engines and the formal model behind them are Resonate's work. What Loams adds is the embedding: one binary instead of another service, configuration from Loams' own flags, an in-process path for Loams' own code, and a TiKV storage backend for clusters.
Use it when a run lasts longer than a request and must not redo paid or side-effecting work after a crash: an agent that calls models and tools for minutes, waits for a human, or fans out sub-tasks. It is a good fit when you already run Loams and do not want a sixth system just for workflows. Do not use it yet for multi-tenant or internet-facing workloads: in 0.1.0 the durable API is unauthenticated, serves one namespace and listens on loopback only. If you need a mature workflow platform with a large ecosystem today, Temporal is the safer choice (see the comparison below).
What ships
Loams Durable ships in Loams 0.1.0, and has been in every image since v0.1.0-preview.1: the loams image is built with the cargo feature durable (beside live,live-tikv,graph,tikv,pgwire,mysql-wire,stream-grpc). The feature status table in docs/release/preview.md lists it as Available, and "Durable store on TiKV" as In progress.
| Piece | Status | Where it is |
|---|---|---|
| Embedded Resonate server with SQLite state | Available | In the 0.1.0 loams image (and both previews before it). loams dev and loams standalone start it by default in a binary built with durable |
| Loams' own durable functions on the Rust SDK, in process | Available | DurableRuntime and InProcNetwork in crates/loams-durable (D1 Task 6) |
Linearizability check against loams dev on SQLite | Available | CI job "Durable: SQLite linearizability" |
| Durable state in snapshots and migrations during upgrades | Available | New in 0.1.0: loams upgrade snapshots Durable with every other store, and the compose migrate service runs Durable's own migrations |
Native TiKV backend (--durable-store tikv://…, feature durable-tikv) | Experimental | Built and tested in CI against a TiKV playground; not in the 0.1.0 images, so the feature table lists "Durable store on TiKV" as In progress |
Operations API (/v1/operations/{id}), bulk import, scheduled import | Planned | D1 Tasks 7 to 9 |
| Per-namespace instances, authentication, approval gates | Planned | Milestone D2, after the unified auth plan |
The 0.1.0 CHANGELOG has no Durable entry beyond the upgrade work: the server itself has not changed shape since v0.1.0-preview.1.
Architecture
The crate is loams-durable. It builds a Resonate server from Resonate's own plugins and nothing else:
- Gateway.
resonate-gateway-httpserves the protocol routes and the poll transport's SSE route (/poll/:group/:id) on one listener, port 8001 by default, Resonate's own default port, so the Resonate examples work unchanged. Move it with--durable-listen, turn the server off with--no-durable. - Server.
ResonateServerowns promises, tasks and schedules, over a storage plugin:resonate-server-sqlitefor SQLite, or Loams'resonate-server-tikvfor TiKV. - Workers (transports).
http-pollfor SDK workers that hold an SSE connection;worker_inproc, a Loams plugin for the schemeinproc://any@loams/<node>, for Loams' own functions;http-push(Resonate calls a serverless worker) is linked but off unless--durable-pushis given. - In-process runtime.
DurableRuntimeruns Loams' own durable functions on the Resonate Rust SDK. Its network,InProcNetwork, hands each request straight toResonateServer::processand receives tasks fromworker_inproc. No socket and no HTTP are involved (the testno_http_is_usedchecks it).
The embed is careful not to touch its host. Loams calls Resonate's public composition API (resonate_base::build, then Running::start and Running::stop), never resonate_base::run, which would install a global tracing subscriber and wait for signals itself. Configuration comes only from Loams' flags (--durable-set key=value reaches any plugin setting), never from resonate.toml or RESONATE_* variables. Resonate's abort_on_panic is pinned off, so a handler panic answers 500 instead of aborting the whole loams process. Each of these has a test in crates/loams-durable/tests/embed.rs.
How it works
Promises, tasks and origins
The Resonate protocol has three entities (design 14 §1, from Resonate's Lean specification):
- A promise has an id, a parameter, a value, tags and a timeout. It is
pendinguntil it is settled asresolved,rejected,rejected_canceledorrejected_timedout. Promises can register callbacks (an awaiter waiting on an awaited promise) and listeners. - A task is the unit of work a worker claims. Its id is its promise's id, and its
versionis a fencing token. A task movespending→acquired→suspended,haltedorfulfilled. Workers heartbeat while they hold it. - A schedule creates promises on a cron from a template.
Everything before the first : of an id is its origin. Every operation except search is single-origin: an awaiter and the promise it awaits share an origin, so one atomic write of one origin's state commits any transition. That is why the storage engines can be simple: SQLite commits a transaction, and the blob and TiKV engines commit one document per origin with a compare-and-set.
A crash, and the replay
A durable function is ordinary async code. Each ctx.run(step, args) creates a child promise with a deterministic id and runs the step only if that promise is not already settled. The test crash_between_steps_resumes_without_rerunning_step_1 in tests/inproc.rs shows the whole mechanism:
- Runtime A acquires the task for root promise
crash-1and runscrash_flow(4). Step 1 returns 5; its promise is resolved with 5. - Step 2 is in flight when runtime A is stopped under it, as a crash would do.
- The task is not fulfilled, so the server redelivers it after the task retry timeout (30 seconds by default,
DEFAULT_RETRY_TIMEOUT). Runtime B acquires it with a higher version, so anything A might still try is fenced off. - B replays
crash_flowfrom the top. Step 1's promise is already resolved, soctx.runreturns 5 without calling the step. Step 2 runs and returns 50. - The root resolves with 50. The test asserts that step 1 ran once and step 2 finished once.
The other tests in that file cover the rest of the everyday patterns: failed_branch_alone_retries (a fan-out of four branches where one fails once: only that branch runs twice), sleep_survives_restart (a durable sleep across a runtime restart), and two_step_function_runs.
Replay has the usual rule of every replay-based engine: the code between steps must be deterministic, and side effects belong inside steps. A step that writes somewhere else should use its own promise id as the idempotency key of that write, so a step that crashed after writing but before settling does not write twice (design 21 §6.7). A workflow step and a Loams data write are never one atomic transaction; that is a stated non-goal.
Storage
| Backend | Selected by | Used for | State today |
|---|---|---|---|
| SQLite | the default in loams dev and loams standalone: <data>/durable/default.db | one node | Available. A lock file on the durable directory refuses a second process on the same store (second_process_on_same_store_is_refused) |
| TiKV | --durable-store tikv://<pd-hosts>/<keyspace>, feature durable-tikv | loams cluster, self-hosted clusters, Loams cloud | Built. Each Resonate object is one TiKV key holding a random revision and its body; a pessimistic transaction locks the key before comparing revisions, so two server instances cannot both win a compare-and-set. Random revisions mean a deleted and recreated key cannot make an old ETag valid again |
MySQL protocol (mysql://…, feature durable-mysql) | legacy | dev and tests | Deprecated by D261; kept until the TiKV backend replaces it everywhere |
loams cluster refuses SQLite, because it is single-node; a cluster node without --durable-store serves no durable execution. On TiKV, any node's instance can deliver a timed-out task to its own in-process worker, so any node can resume any of Loams' own operations. The CI test promise_state_survives_two_servers_and_restart runs two servers on one TiKV keyspace and a restart.
For the TiKV layer underneath, see Metadata and TiKV.
Failure handling
| Failure | What happens |
|---|---|
The loams process dies (SQLite) | State is in the file. On restart, timers re-arm from their stored deadlines and pending tasks are redelivered after the retry timeout |
| A node dies (TiKV) | Another node's instance redelivers the task; SDK workers reconnect to any node's poll endpoint |
| A worker is slow | The poll transport drops a message rather than block, and the retry timeout redelivers it. Delivery is best effort in every transport by design |
| The store is unavailable | The server reports not ready and the gateway answers 503 |
| Port 8001 is taken | Startup fails and names --durable-listen and --no-durable (port_in_use_names_flags) |
| A step always fails | The SDK's retry policy gives up and the branch fails; no infinite loop |
Key decisions
The full log is on the decisions page; the design is design 21, which amends design 14.
- D19: durable execution through the Resonate protocol. Agents need durable runs next to their memory. Resonate is Apache-2.0, formally specified, plugin-based and already able to run on object storage, "so this is buy, not build". A Loams-specific durable SDK is a non-goal: users use Resonate's.
- D138: embed the server in the binary. The owner's direction was to link Resonate's plugins into
loamsrather than run a sidecar or a separate role. The linking spike ran it beside Loams' axum 0.8 app with public API only, passed Resonate's Python fan-out example and survivedkill -9of the whole binary in the human-in-the-loop example. D138 also makes the listener refuse non-loopback addresses until authentication exists, the same stricter rule Loams Live uses. - D262: the
durablefeature is opt-in. Toggling it rebuilds about 480 crates, so it is off in default builds and on in release images and CI's durable jobs. A build without it has no Resonate code, and a--durable-*flag only logs a warning. - D139, amended by D261: SQLite in dev, TiKV for clusters. D139 had chosen TiDB through Resonate's MySQL plugin for the cloud. D260 removed TiDB from the engine and D261 replaced it with a native TiKV backend, because Loams runs TiKV anyway. The
mysql://backend is legacy. - D140: the dependencies come from a pinned fork. Resonate's server crates are not on crates.io. The fork
ostrium-labs/resonateis upstream plus a dependency-hygiene commit and two fixes for TiDB, each also proposed upstream (Resonate PRs #1162, #1163 and #1164). - D141: http-poll for SDK workers, push off, the Rust SDK in process for Loams' own code. Using the SDK keeps replay semantics "bought, not rebuilt". Push stays off until the auth plan and a per-organisation outbound allowlist exist.
- D142: one Resonate instance per namespace. Resonate has no tenant concept: groups, schedules, searches and timers are global to a server, so prefixing ids would share them. Isolation needs a whole instance per namespace. 0.1.0 serves only the
defaultnamespace. - D144: conformance by black-box linearizability. Loams does not re-run Resonate's engine differentials (the fork's CI does); it runs Resonate's own
conctraceand porcupine checker against theloamsbinary, because embedding changes configuration, the listener and the in-process path, not the engine. - D143 and D145: patterns become features, on their own track. Async operations, fan-out with partial retry, schedules and idempotency keys come first, with bulk import from object storage as the first real operation. Loams' own short maintenance loops (compaction, merges, GC) deliberately stay lease-fenced worker tasks: a cron would add latency and a dependency and give them nothing.
What is compatible
- Protocol. Resonate server 0.10.1, protocol version
2026-04-01, from the fork at a pinned revision (31ffb201…in the workspaceCargo.toml). SDKs must match the server's protocol: design 21 records that PyPIresonate-sdk0.7.x is refused by server 0.10.1 (400 Promise ID must be prefixed by resonate:origin), while the monorepo Python SDK 0.8.1 works. - Tested in CI. Resonate's Rust SDK in process (
crates/loams-durable/tests/inproc.rs); Resonate's linearizability checker againstloams devon SQLite at 8 clients × 600 operations on every relevant pull request (scripts/durable/conformance.sh); the TiKV backend's integration test in the TiKV job; the legacy TiDB backend nightly against TiDB v8.5.8 at 8 × 600 and 16 × 400 with three seeds. - Tested once, not in CI. The linking spike of 2026-09-27 ran Resonate's Python fan-out/fan-in example (normal and
--crash) and the human-in-the-loop example withkill -9of the binary, with Python SDK 0.8.1. The nightly SDK example suite that D144 describes (Python and TypeScript examples) is not in CI yet, so no TypeScript, Go or Java SDK version is verified against Loams today. The Jobs plan (JB1) pins Resonate Python SDK 0.8.1 and TypeScript SDK 0.11.5 for its work; those are planned, not tested.
Compared with the alternatives
Durable execution is a crowded field. All of these record progress so that work resumes after a failure; they differ in where state lives, how code is replayed, and how much surrounds the core.
| Loams Durable | Temporal | Restate | Inngest | DBOS | |
|---|---|---|---|---|---|
| Model | Resonate's durable promises: each step is a promise, replay reads settled promises | Event history: workflow code is replayed against a recorded history of events | A journal per invocation; a server in front of your handlers | Step functions triggered by events, each step memoized | A library in your process; workflow and step state in Postgres |
| Where it runs | Inside the loams binary | A separate cluster (or Temporal Cloud) | A separate server written in Rust | A hosted platform, or a self-hosted server | In your application, plus a Postgres database |
| State | SQLite file; TiKV for clusters (in progress) | Its own persistence (Cassandra, MySQL, Postgres) | Its own log and storage | Its own | Postgres |
| Maturity and ecosystem | 0.1.0, one namespace, no auth | Mature, widely deployed, many SDKs, large community and tooling | Younger than Temporal, growing | Product focused on serverless and event-driven apps | Library-first, younger |
Where the others are better today: Temporal is far more mature, has years of production use, rich visibility tooling and SDKs for many languages; if you need that today, use it. Restate gives you a dedicated, single-binary Rust server that is built for exactly this job. Inngest is the smoother path if your code is serverless functions triggered by events. DBOS needs nothing beyond Postgres if Postgres is all you run.
Where Loams Durable fits: it is the Resonate protocol, so you get Resonate's SDKs and its formally specified semantics, and it runs inside the system that already holds your agents' memory, collections and logs, with no extra service to operate. It is also the engine Loams itself is moving its long operations onto. If you want Resonate on its own, run Resonate: it is the same protocol, and everything the SDKs do works the same way against either.
What is missing and what is planned
- Authentication and tenancy. The durable API is unauthenticated, loopback only and single-namespace in 0.1.0. Per-namespace instances and the dispatcher that maps a credential to its namespace are milestone D2, after the unified auth plan (D142). In the compose stack, port 8001 stays inside the
loamscontainer and is not forwarded over your tailnet. - TiKV in the images. The backend is built and tested; shipping it is the "Durable store on TiKV" row, In progress.
- The operations API, bulk import and scheduled import. D1 Tasks 7 to 9:
POST …/importanswering202withLocation: /v1/operations/{id}, idempotency keys, per-file fan-out, and a cron schedule that imports only new files. - Retention. The protocol has no delete for settled promises and no backend prunes them, so durable state grows. Retention is open (Q40); pruning is not free either, because a late retry would re-create a pruned id and run the work again.
- Later milestones. Approval gates and tenant provisioning sagas (D2); durable Live actions, the agent runtime and MCP operation tools (D3); a connect-rust transport (D4). Loams Jobs plans schedules and flows on this server (D210), and the desktop agent daemon plans durable turns on
loams-durablewith no HTTP listener (D793).
Try it
Durable is in the 0.1.0 images, so the compose stack already runs it inside the loams service. Because the listener is loopback only, the simplest way to point an SDK at it is a local build. From the engine quick start and the build guides:
# `durable` turns on the embedded Resonate server (port 8001, loopback only).
# It rebuilds roughly 480 crates, so expect a long first build.
cargo build --release -p loams --locked --features durable
./target/release/loams devloams dev serves the Resonate protocol on the loopback interface, port 8001; point a Resonate SDK at that address. Flags: --durable-listen, --durable-store, --durable-push, --durable-set key=value, --no-durable. For the legacy MySQL-protocol store, loams durable migrate runs the schema migrations as an explicit step.
To run the same checks CI runs:
cargo test -p loams-durable --locked
cargo test -p loams --features durable --test durable --bin loams
scripts/durable/conformance.sh --self-test
scripts/durable/conformance.sh --store sqlite --clients 8 --ops 600The conformance script needs Go 1.24 and Python 3 on PATH, and fetches the pinned fork to build Resonate's conctrace.
Source
- Crate:
crates/loams-durable; scripts:scripts/durable - Design: 14, Durable execution (Resonate protocol), 21, Loams Durable: embedded Resonate, 26, Jobs API §8 (how Jobs uses Resonate), 15, Agent workspaces
- Plans: D1, embedded durable execution, the D1 dependency spike, JB1, Jobs
- Upstream: resonatehq/resonate and the fork ostrium-labs/resonate. Thanks to the Resonate team: the protocol, the SDKs and the verification harness Loams relies on are theirs.
Loams Postgres
Serverless Postgres from Loams' hard fork of Neon, with storage in your bucket, copy-on-write branches and computes that start on demand and suspend when idle.
Loams Studio
An agentic development environment in the Loams console: coding agents in their own git worktrees, with review, checkpoints, approvals and parallel runs, all through loams-agentd.