Skip to main content

Skill Guide | Rust SDK

Early development

The Rust SDK is v0.1.0 and in active development. APIs may change between releases. It is not yet published on crates.io.

Whether you are a human or an AI agent, this skill guide will help you develop applications using the Resonate Rust SDK.

This skill sheet assumes that you already know Resonate is a good fit for your use case. If you are unsure, please refer to Why Resonate.

It's important to think about the potential architecture of your application as it ties to your use case. This can determine which APIs and options you will use for function activations. If you have a use case in mind, but haven't built with Resonate before, we recommend reviewing some of the common patterns in our example applications.

Installation

How to install the Resonate Rust SDK into your project.

The Rust SDK is not yet published on crates.io. Add it as a git dependency in your Cargo.toml:

Cargo.toml
TOML
[dependencies]
resonate = { git = "https://github.com/resonatehq/resonate-sdk-rs", branch = "master" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

Once added, you can import the Resonate prelude and start using the SDK:

Rust
use resonate::prelude::*;

Initialization

How to initialize a Resonate Client.

Initializing a Resonate instance gives you the APIs needed to register, activate, and await durable functions.

main.rs
Rust
use resonate::prelude::*;

let resonate = Resonate::new(ResonateConfig::default());
  • A Resonate instance can connect to either an in-memory local store (best for development) or a remote Resonate Server (best for production).
  • A Resonate instance can only be used in the Ephemeral World to register and activate functions — it cannot be used inside Durable Functions. Use the Context APIs for function activations inside Durable Functions.

Upon initialization, the Resonate instance will look for environment variables and/or use the options passed via ResonateConfig.

main.rs
Rust
use resonate::prelude::*;

let resonate = Resonate::new(ResonateConfig {
url: Some("http://localhost:8001".into()),
group: Some("worker-group-a".into()),
..Default::default()
});

Zero-dependency development

Unlike other Durable Execution offerings, apart from installing the SDK itself, Resonate enables you to get started without any additional dependencies. This is because Resonate can run in a local development mode that uses in-memory storage for promises and tasks.

main.rs
Rust
use resonate::prelude::*;

let resonate = Resonate::local();

Local development mode can be suitable for awhile. However, when you want to run multiple worker processes, persist state across restarts, or share work between machines, you will need to connect to a Resonate Server. See the Quickstart guide or How to run a Resonate Server for guidance on getting a server up and running.

Authentication

The Rust SDK supports token-based authentication (JWT) when connecting to a secured Resonate Server.

main.rs
Rust
use resonate::prelude::*;

let resonate = Resonate::new(ResonateConfig {
url: Some("https://localhost:8001".into()),
token: std::env::var("RESONATE_TOKEN").ok(),
..Default::default()
});
tip

Token-based authentication is recommended for production. See the Security & Authentication guide for detailed configuration, best practices, and server setup.

Environment variables

The Resonate constructor automatically inspects a handful of environment variables when instantiating the SDK.

Resolution order

When a Resonate instance is created, explicit ResonateConfig fields always win. If a field is not provided, the SDK looks for related environment variables before falling back to built-in defaults.

The following order is used when determining the remote endpoint:

  1. url field supplied to ResonateConfig.
  2. RESONATE_URL environment variable.
  3. RESONATE_HOST and RESONATE_PORT environment variables.
  4. Built-in local development mode (no remote network).
No URL means local mode (zero-dependency development)

If neither a config field nor environment variable produces a URL, the SDK falls back to a local in-memory network. This is convenient for unit tests or quick experiments that do not require a Resonate server.

RESONATE_URL

  • Provides the full base URL (scheme, host, and port) for connecting to a remote Resonate server.

  • Default is unset

  • Example:

    Shell
    export RESONATE_URL="http://localhost:8001"
    Rust
    let resonate = Resonate::new(ResonateConfig::default()); // picks up RESONATE_URL

RESONATE_HOST

  • Hostname or IP address of the remote Resonate server.
  • Default is unset.

RESONATE_PORT

  • Port number of the Resonate server. Ignored if RESONATE_URL is provided.
  • Default is 8001.

RESONATE_TOKEN

  • JWT token for authenticated requests.
  • Default is unset (anonymous requests).

RESONATE_PREFIX

  • Prefix prepended to all promise and task IDs. Useful for namespacing in multi-tenant environments.
  • Default is empty.

Defining durable functions

How to annotate functions with #[resonate::function].

The #[resonate::function] attribute macro transforms a regular async function into a durable function. The SDK detects the function kind from the first parameter:

First parameterKindWhat it means
&ContextWorkflowCan orchestrate sub-tasks via ctx.run(), ctx.rpc(), ctx.sleep()
&InfoLeaf with metadataRead-only access to execution metadata (ID, parent, tags)
(anything else)Pure leafStateless computation — no special environment

All durable functions must return Result<T> (where Result is resonate::error::Result).

Rust
use resonate::prelude::*;

// Workflow — orchestrates sub-tasks
#[resonate::function]
async fn my_workflow(ctx: &Context, input: String) -> Result<String> {
let result = ctx.run(my_leaf, input).await?;
Ok(result)
}

// Pure leaf — stateless computation
#[resonate::function]
async fn my_leaf(input: String) -> Result<String> {
Ok(format!("Processed: {input}"))
}

// Leaf with metadata — access to execution info
#[resonate::function]
async fn my_info_leaf(info: &Info, input: String) -> Result<String> {
Ok(format!("ID: {}, Input: {input}", info.id()))
}

You can also override the registered name:

Rust
#[resonate::function(name = "custom-name")]
async fn my_func() -> Result<()> {
Ok(())
}

Client APIs

The following Client APIs can only be used in the Ephemeral World — they cannot be used inside Durable Functions.

.register()

Register a durable function to expose it to the Resonate system. A registered function can then be activated by the Resonate Client, the Resonate CLI, or from inside another Durable Function using the Context APIs.

Rust
resonate.register(my_workflow).unwrap();
resonate.register(my_leaf).unwrap();

.run()

Resonate's .run() method invokes a function in the same process and returns the result. You can think of it as a "run right here" invocation. After invocation, the function is considered durable and will recover in another process if required.

Rust
let result: String = resonate.run("invocation-id", my_workflow, "input".into()).await?;

The builder supports options before awaiting:

Rust
use std::time::Duration;

let result: String = resonate
.run("invocation-id", my_workflow, "input".into())
.timeout(Duration::from_secs(60))
.await?;

.rpc()

Resonate's .rpc() method (Remote Procedure Call) invokes a function in a remote process and returns the result. You can think of it as a "run somewhere else" invocation.

Rust
let result: String = resonate.rpc("invocation-id", "my_workflow", "input".into()).await?;

The builder supports .timeout(), .version(), .tags(), and .target():

Rust
let result: String = resonate
.rpc("invocation-id", "my_workflow", "input".into())
.target("poll://any@workers")
.await?;

.schedule()

Schedule a function to be invoked on a cron schedule.

Rust
let schedule = resonate
.schedule("my-schedule", "0 * * * *", "my_workflow", "input".into())
.await?;

// Later, delete the schedule
schedule.delete().await?;

.get()

Get a handle to an existing execution by its promise ID.

Rust
let mut handle = resonate.get::<String>("invocation-id").await?;
let result = handle.result().await?;

.promises

The .promises sub-client lets you work directly with durable promises — useful for human-in-the-loop workflows, external coordination, and any pattern where you need to create a promise now and settle it later from a different process.

Rust
use serde_json::json;

// Create a promise with a timeout, initial parameter, and tags
resonate.promises.create("promise-id", timeout_at, json!({}), json!({})).await?;

// Get a promise by ID
let promise = resonate.promises.get("promise-id").await?;

// Settle a promise (resolve)
resonate.promises.settle("promise-id", "resolved", json!("approved")).await?;

// Settle a promise (reject)
resonate.promises.settle("promise-id", "rejected", json!("denied")).await?;

.stop()

Graceful shutdown. Stops background tasks (heartbeat, subscriptions).

Rust
resonate.stop().await?;

Context APIs

How to use the Resonate Context inside Durable Functions.

Resonate's Context enables you to invoke sub-tasks from inside a workflow. This is how you extend the Call Graph and create a world of Durable Functions.

ctx.run()

Invoke a function in the same process. By default, .await executes sequentially — the calling function blocks until the invoked function returns.

Rust
#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<String> {
let result = ctx.run(my_leaf, "input".into()).await?;
Ok(result)
}

Use .spawn() to run in parallel and get a DurableFuture handle:

Rust
#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<(String, String)> {
let future_a = ctx.run(process, "a".into()).spawn().await?;
let future_b = ctx.run(process, "b".into()).spawn().await?;

let result_a = future_a.await?;
let result_b = future_b.await?;

Ok((result_a, result_b))
}

ctx.rpc()

Invoke a function in a remote process (by registered name). The calling function blocks until the remote function returns.

Rust
#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<String> {
let result = ctx.rpc::<String>("remote-func", "input".into()).await?;
Ok(result)
}

Use .spawn() for parallel remote invocations:

Rust
#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<()> {
let f1 = ctx.rpc::<String>("worker-a", "data".into()).spawn().await?;
let f2 = ctx.rpc::<String>("worker-b", "data".into()).spawn().await?;

f1.await?;
f2.await?;

Ok(())
}

ctx.sleep()

Durable sleep. There is no limit to how long the function can sleep — hours, days, or weeks. The sleep survives process restarts.

Rust
use std::time::Duration;

#[resonate::function]
async fn reminder(ctx: &Context, user_id: String) -> Result<()> {
// Sleep for one hour without blocking the worker
ctx.sleep(Duration::from_secs(3600)).await?;

ctx.rpc::<()>("send-notification", user_id).await?;
Ok(())
}

Builder options

All Context execution methods support builder options before .await or .spawn():

Rust
use std::time::Duration;

#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<String> {
let result = ctx.run(my_leaf, "input".into())
.timeout(Duration::from_secs(30))
.await?;
Ok(result)
}
MethodDescription
.timeout(Duration)Execution timeout
.target(&str)Target worker group (for ctx.rpc())
note

Client-side builders (resonate.run(), resonate.rpc()) also support .version(u32) and .tags(HashMap<String, String>). These are not available on Context builders.

Context accessors

Inside a workflow, the Context provides read-only metadata:

Rust
#[resonate::function]
async fn my_workflow(ctx: &Context) -> Result<()> {
println!("Execution ID: {}", ctx.id());
println!("Parent ID: {}", ctx.parent_id());
println!("Origin ID: {}", ctx.origin_id());
println!("Function name: {}", ctx.func_name());
println!("Timeout at: {}", ctx.timeout_at());
Ok(())
}