4 min readResonate HQJust published

Hello World quickstart in Rust on Resonate

The smallest unit of a Resonate Rust program — a workflow, a leaf function, and a worker — annotated for retrieval.

Resonate brand card on a dark background with an ember spectrum wave at the bottom and the post headline in white Sansation.

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: &Contextworkflow; &Infoleaf 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