A program that calls one function from another and survives the worker process crashing mid-call has to record what it did, where it got to, and how to resume — by hand, that's a state machine plus a database plus a queue. Resonate provides durable execution as a primitive: each function call is a checkpoint persisted to the Resonate Server, and a worker that comes back online picks up wherever it left off. This example is the minimum program that exercises the primitive — one workflow function that calls one leaf function, registered against a local server, invoked through the CLI.
The shape of the solution
use resonate::prelude::*;
/// A workflow that orchestrates a greeting.
/// Workflows receive `&Context` and can call sub-tasks.
#[resonate::function]
async fn greet(ctx: &Context, name: String) -> Result<String> {
let greeting = ctx.run(format_greeting, name).await?;
Ok(greeting)
}
/// A leaf function — pure computation, no Context needed.
#[resonate::function]
async fn format_greeting(name: String) -> Result<String> {
Ok(format!("Hello, {name}! Welcome to durable execution."))
}
// from example-hello-world-rs/src/main.rs:1-15The worker process registers both and waits:
#[tokio::main]
async fn main() {
let resonate = Resonate::new(ResonateConfig {
url: Some("http://localhost:8001".into()),
..Default::default()
});
resonate.register(greet).unwrap();
resonate.register(format_greeting).unwrap();
// Keep the process alive to receive work from the server.
println!("Worker started. Waiting for invocations...");
tokio::signal::ctrl_c()
.await
.expect("Failed to listen for ctrl-c");
}
// from example-hello-world-rs/src/main.rs:17-32Invocation is out-of-band from the CLI:
resonate invoke greet-1 --func greet --arg '"World"'
# from example-hello-world-rs/README.md:59The durable primitives in play
#[resonate::function]— attribute macro that generates aDurableimpl for the annotated function. The macro inspects the first parameter type and emits one of three kinds:&Context→ workflow;&Info→ leaf with metadata; anything else (e.g.Stringhere) → pure leaf. Implementation infn detect_kindatresonate-sdk-rs/resonate-macros/src/lib.rs:301-318;FunctionKindenum atlib.rs:94-102. Applied atsrc/main.rs:5(workflow) andsrc/main.rs:12(pure leaf).ctx.run(format_greeting, name)— local durable invocation. Returns aRunTaskbuilder that, when awaited, persists a child checkpoint to the Resonate Server, executes the leaf, and resolves with its result. On replay, an already-resolved checkpoint short-circuits the leaf and returns the recorded value. Source:resonate-sdk-rs/resonate/src/context.rs:244-259. Applied atsrc/main.rs:7.Resonate::new(ResonateConfig { url: ..., ..Default::default() })— constructs the worker, connects to the Resonate Server athttp://localhost:8001. Applied atsrc/main.rs:19-22.resonate.register(func)— makes a function available for the worker to receive invocations of. RequiresDurable<Args, T> + Copy + Send + Sync + 'static. Source:resonate-sdk-rs/resonate/src/resonate.rs:295-303. Applied atsrc/main.rs:24-25.
What the SDK handles vs. what you write
You write two async fns annotated with #[resonate::function], a single ctx.run(...) call to compose them, and a main that constructs the SDK handle, registers the functions, and parks on ctrl_c. The argument and return types of both functions are owned by you — the macro generates only the Durable impl, and that impl is then consumed by register (which requires Args: DeserializeOwned, T: Serialize) and by ctx.run (which requires Args: Serialize), so the consumer crate has to depend on serde directly (Cargo.toml:10). There is no queue declaration, no state-machine wiring, no idempotency key plumbing, and no explicit replay code anywhere in src/main.rs.
The SDK handles function registration with the server, polling for invocations targeted at this worker, serializing the leaf's arguments to a durable promise on ctx.run, deserializing the result back into the caller's typed await, recording the leaf's resolution as a checkpoint keyed by a child id derived from the parent invocation id, and — on a fresh worker process picking up the same invocation id — short-circuiting any leaf whose checkpoint is already resolved. The CLI (resonate invoke greet-1 --func greet --arg '"World"') creates the top-level durable promise; the worker picks it up via its registry.
Failure modes covered
- Worker crashes between the workflow starting and
ctx.runreturning. The top-level promisegreet-1is created by the CLI before the worker touches it. When the worker comes back online, it polls and re-receives the invocation. Because no child checkpoint exists yet,format_greetingruns and a child checkpoint is written. README §"What to observe" (README.md:74) calls this out as the explicit demo: kill the worker mid-execution, restart, it picks up where it left off. - Worker crashes after
format_greetingresolved but beforegreetreturned. On replay,ctx.run(format_greeting, name)finds the already-resolved child checkpoint and returns the recordedStringwithout re-executing the leaf. The workflow then returnsOk(greeting)and the top-level promise resolves.
What this example does not cover: application-level retries, compensating actions, multi-step idempotency on external side effects, timeouts, and id-keyed deduplication semantics (the example exercises crash-and-resume on a single invocation; it does not demonstrate what happens when the same id is invoked twice — see the idempotency / async-rpc examples for that). Those belong to other example posts.
When to reach for this pattern
- If you're spiking on the Resonate Rust SDK and want the smallest program that exercises registration + invocation + checkpoint + replay.
- If you're verifying a local
resonate devserver is reachable from a Rust worker and that the CLI can route an invocation to it. - If you're sanity-checking that
#[resonate::function]compiles cleanly against the SDK version yourCargo.tomlis pinned to. - If you're teaching another agent the shape of a Resonate Rust program before it tackles a real workflow (saga, fan-out, human-in-the-loop, etc.).
If your use-case has more than one leaf, a wait on external input, or a step that must compensate on failure, this is not the pattern — start from a saga, fan-out, or human-in-the-loop example instead.
Sources
- Example repo: resonatehq-examples/example-hello-world-rs
- Rust SDK repo: resonatehq/resonate-sdk-rs (workspace at v0.4.0)
ctx.runsource:resonate/src/context.rs(line 244)Resonate::registersource:resonate/src/resonate.rs(line 295)#[resonate::function]macro source:resonate-macros/src/lib.rs(line 51); kind-detection logic atfn detect_kind(line 301)- Resonate Quickstart docs (Rust tab): docs.resonatehq.io/get-started/quickstart
