Loams v0.1.0 is out: run the whole stack yourself with Docker Compose. See the roadmap

Blog/The Loams platform

Durable execution: Resonate, embedded

Loams links the Resonate server into its own binary, so the official Resonate SDKs get durable promises, retries, schedules and human approvals from the system that holds the agent's memory. Here is how, and the patterns we build on it.

Durable execution: Resonate, embedded
On this page
  1. The model: durable functions and durable promises
  2. Why embed it
  3. How the embedding works
  4. Storage
  5. The patterns, and what Loams uses them for
  6. What we deliberately did not move
  7. Limits we know about
  8. Where this stands

Updated 2026-10-18 for Loams 0.1.0

Changed: Loams Durable (embedded Resonate on SQLite) is available in 0.1.0; the TiDB store is legacy since D261 and durable state for clusters moves to TiKV (in progress, not in the images); the operations API and import patterns are planned, not in progress; linked the Learn page.

An agent run is a long program that calls expensive, flaky things. A ten-step research agent that crashes at step seven either repeats six paid model calls or loses the run. The usual fix is a workflow engine beside everything else: Temporal, or a queue plus cron plus a table of states. That is one more stateful system with its own database.

Loams’ answer is to embed one. The Resonate server is linked into the Loams binary, speaks the standard Resonate protocol, and stores its state next to the rest of the namespace. Agent code uses Resonate's own SDKs, unchanged. This post covers the programming model, how the embedding works, and the patterns Loams builds on it. In Loams 0.1.0 this ships as Loams Durable (available); the Durable page in Learn is the current reference.

The model: durable functions and durable promises

Resonate's model is distributed async/await. A durable function is ordinary code that calls other functions through a context. Each call's result is recorded as a durable promise on the server. When the process dies and the function is invoked again, it replays from the top, and every call that already completed returns its recorded result instead of running again.

from resonate import Resonate

resonate = Resonate(url="http://127.0.0.1:8001")          # Loams’ durable listener

@resonate.register(name="research", version=1)
async def research(ctx, question: str):
    sources = await ctx.run(search_the_web, question)      # recorded once
    notes = await ctx.run(summarize, sources)              # a paid model call, recorded once
    return await ctx.run(write_report, notes)

If the worker is killed after summarize returns, the retry replays research, gets sources and notes from their promises, and resumes at write_report. (The example uses the Python SDK that ships in Resonate's monorepo, 0.8.1, which matches the server version Loams pins.) The model call is never paid for twice. Because the function replays, it must be deterministic between calls; anything non-deterministic (time, randomness, I/O) goes inside a recorded call.

Promises are the whole state. A workflow is a tree of promises under one root, its origin, and the server commits each transition of an origin atomically. Tasks with fenced leases hand work to workers, and a task whose worker vanishes is redelivered after a retry timeout. Schedules create promises from a template on a cron. The protocol is formally specified, with an executable model in Lean 4, a TLA+ model and a trace checker that replays a real server's traffic against the model. The Resonate deep dive goes further into it.

Why embed it

We looked at running Resonate (or Temporal) as a sidecar and chose to link it in:

  • One binary, one deployment. loams dev gets durable execution with no extra process, container or role.
  • In-process calls. Loams’ own Rust code talks to the server without HTTP, which matters when Loams’ own long operations become durable functions.
  • The same tenancy, storage and, later, auth. Durable state is per namespace, like everything else.
  • Buy, not build. Loams writes glue and workflows, not a durable-execution engine.

Resonate made that possible because its new server is Rust, built from plugins, with a public composition API. A spike linked it into an axum 0.8 application, ran Resonate's Python fan-out example against it (including its crash mode, where only the failed branch re-ran), and killed the whole binary with kill -9 while a human-in-the-loop workflow was suspended. After the restart, resolving the approval completed the workflow from its stored state. No upstream change was needed.

How the embedding works

Callers
Resonate SDKs: TypeScript, Python, Go, Java, RustLoams clients (operations API, landing)
Loams binary
Resonate HTTP gateway on 127.0.0.1:8001ResonateServerhttp-poll transport (SSE)In-process networkLoams durable runtime (Rust SDK)
Durable store
SQLite file (dev, single node)TiKV (clusters, in progress)MySQL plugin (legacy, D261)
Durable execution inside the Loams binary. Loams’ own workflows reach the server in process; SDK workers reach it over HTTP on a loopback listener.
  • Composition, not main. A crate, loams-durable, builds a registry of only the plugins Loams carries and starts the server with Resonate's build, start and stop calls. It never calls Resonate's own run, which installs a global tracing subscriber and waits for signals itself.
  • Configuration from Loams’ flags. Loams builds Resonate's configuration in code. It reads no resonate.toml and no RESONATE_* variables, so a standalone Resonate setup on the same machine cannot leak in.
  • No process-wide surprises. Resonate's HTTP gateway can abort the process on a handler panic. Embedded, that would take down all of Loams, so it is pinned off and a panic answers 500.
  • Loopback only. The durable API has no authentication yet, and it lets a caller create schedules and, through the push transport, make the server send HTTP requests to addresses named in a promise. So the listener accepts only loopback addresses and refuses anything else at startup, and the push transport is off by default.
  • Loams’ own workflows run in process. The Resonate Rust SDK accepts a custom network. Loams implements it by calling the server directly and delivering tasks to an in-process worker, so Loams’ operations are durable functions with the SDK's replay semantics, and with a shared store any node can resume any of them.

The server crates are not on crates.io yet, so Loams depends on a pinned revision of a fork (ostrium-labs/resonate). The fork is upstream plus a small set of commits, each also offered upstream: a dependency-hygiene change (newer sqlx and rusqlite, rustls instead of OpenSSL, which cleared seven of eight RustSec advisories against our cargo deny policy), TiDB support in the MySQL plugin, and error classification for retries.

Storage

StoreWhereVerification
SQLite, one file per tenantloams dev and single-node installsUpstream's differential tests and porcupine; Loams runs Resonate's linearizability check against the embedded binary in CI
TiDB, one database per tenant, through Resonate's MySQL pluginLegacy, dev and tests only (deprecated by D261)Upstream's engine and port differentials and porcupine all passed on TiDB v8.5.8 in our research run
Native TiKV store for Resonate's blob serverClusters, --durable-store tikv://… with the durable-tikv featureBuilt and tested in CI against a TiKV playground; not in the 0.1.0 images yet (in progress)

Why native TiKV: Resonate's blob server keeps one compare-and-swapped document per origin. When this post was written TiDB was the cluster store; D260 removed TiDB from Loams' own state and D261 replaced it with this native TiKV backend, which removes a SQL layer and puts durable state on the same storage substrate as Live. Tenant dispatch remains a separate integration step.

The patterns, and what Loams uses them for

PatternLoams featureStatus
Submit, then poll (async HTTP API)/v1/operations/{id}: every long Loams operation becomes a durable promise with progress and cancel; bulk import from object storage firstPlanned
Fan-out and fan-inImport fans out per file and per slice; after a failure only the failed branches re-runPlanned
SchedulesScheduled incremental import ("every hour, load new files under this prefix")Planned
Idempotency keysAn Idempotency-Key header maps to an operation id; a step's promise id is the idempotency key of the write it makesPlanned
Sagas with compensationGDPR erasure across every store that holds a subject's data; tenant provisioning with rollbackPlanned
Human in the loopApproval gates on destructive operations and on agent actionsPlanned
Durable agentsLive actions as durable functions, multi-agent handoffs, deep research with sub-agents, long-running MCP tools, agent traces into LoamsPlanned

Submit, then poll. A bulk import is POST …/collections/{c}/import, which answers 202 Accepted with Location: /v1/operations/op-…. The operation id is the root promise id, and its state maps directly from the promise: pending, running, succeeded, failed, canceled. Progress needs no second store: branch promises are tagged with the operation id and counted. The result carries the consistency token that covers every write, so a client can read its own import.

Fan-out. The import plans once (list the source, fix the file set with ETags), then starts one branch per file and one step per Parquet row group or 64 MiB NDJSON slice. A crash re-runs the slice it was in. Upserts by primary key make that converge, and documents without a key get ids derived from the operation, file and row, so a re-run never duplicates.

Schedules. Each run of a scheduled import starts one durable call per file with an id derived from the schedule, key and ETag. Resonate deduplicates on the id, so a file imported earlier is found already resolved and is not read again, and a changed file (new ETag) is imported again. The durable store is the dedup ledger.

Sagas. Tenant provisioning is a sequence of steps, each with a compensation: a directory entry, a TiKV keyspace, a bucket prefix and envelope key, quotas, an authorization store, the tenant's durable store. On failure the workflow runs the compensations in reverse. Because the workflow is durable, a crash halfway through compensating resumes the compensation instead of leaving a half-provisioned tenant.

Human in the loop. A destructive request becomes an operation whose first step waits on an approval promise. An approver resolves or rejects it; without one, it times out and is rejected. The same gate serves agents whose policy marks an action as needing approval.

What we deliberately did not move

Loams’ maintenance loops (split merges, Lance compaction, HNSW builds, garbage collection) stay lease-fenced worker tasks. They are short, idempotent and re-derived from state on every poll. A crash loses at most one run, and the next poll proposes it again. A cron would add latency and a dependency to the most important loops and give them nothing. Durability pays where one logical job outlives one task run or crosses systems: imports, erasure, re-embedding.

Limits we know about

  • Retention. The protocol has no delete for settled promises, so durable state grows without bound. Pruning is not free either: if a late retry re-creates a pruned id, the work runs again. Loams prunes only its own finished operations after a retention period; a general setting is proposed upstream.
  • Tenancy. Resonate has no tenant concept: groups, schedules and searches are global to a server. Today the embedded server is one instance with one store. The planned isolation is one server instance per namespace, over the namespace's own store, which needs a small upstream change to serve per-tenant routes behind Loams’ dispatcher, and needs the auth plan to know which tenant is calling.
  • No cross-system transactions. A workflow step and a Loams write are not atomic. Steps are idempotent instead.
  • Protocol versions. SDKs must match the server's protocol version (2026-04-01 for the pinned server). We document which SDK versions pass our conformance run.

Where this stands

PartStatus
Resonate embedded behind the durable build feature, loopback listener, SQLite storeAvailable
TiDB store through the MySQL plugin (legacy since D261)Available
In-process network and Loams durable runtimeAvailable
Resonate's linearizability check against the embedded server, in CIAvailable
Native TiKV store, built and tested; shipping it in the release imagesIn progress
Operations API, bulk import, scheduled import, the SDK example suitePlanned
Authentication, per-namespace instances, approval gates, provisioning sagas and push policyPlanned
Durable Live actions, the agent runtime, MCP operation tools, agent tracesPlanned

The durable feature is opt-in in source builds because it adds several hundred crates (D262); the 0.1.0 release images are built with it. The durable API is still unauthenticated, loopback only and single-namespace in 0.1.0. The next post uses durable promises for something else: making waiting free.

More from the blog