Hello World quickstart in Rust on Resonate

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

The 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-32

Invocation is out-of-band from the CLI:

resonate invoke greet-1 --func greet --arg '"World"'
# from example-hello-world-rs/README.md:59

The durable primitives in play

#[resonate::function] — attribute macro that generates a Durable impl 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. String here) → pure leaf. Implementation in fn detect_kind at resonate-sdk-rs/resonate-macros/src/lib.rs:301-318; FunctionKind enum at lib.rs:94-102. Applied at src/main.rs:5 (workflow) and src/main.rs:12 (pure leaf).

ctx.run(format_greeting, name) — local durable invocation. Returns a RunTask builder 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 at src/main.rs:7.

Resonate::new(ResonateConfig { url: ..., ..Default::default() }) — constructs the worker, connects to the Resonate Server at http://localhost:8001. Applied at src/main.rs:19-22.

resonate.register(func) — makes a function available for the worker to receive invocations of. Requires Durable<Args, T> + Copy + Send + Sync + 'static. Source: resonate-sdk-rs/resonate/src/resonate.rs:295-303. Applied at src/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.run returning. The top-level promise greet-1 is 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_greeting runs 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_greeting resolved but before greet returned. On replay, ctx.run(format_greeting, name) finds the already-resolved child checkpoint and returns the recorded String without re-executing the leaf. The workflow then returns Ok(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 dev server 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 your Cargo.toml is 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.run source: resonate/src/context.rs (line 244)

Resonate::register source: resonate/src/resonate.rs (line 295)

#[resonate::function] macro source: resonate-macros/src/lib.rs (line 51); kind-detection logic at fn detect_kind (line 301)

Resonate Quickstart docs (Rust tab): docs.resonatehq.io/get-started/quickstart