Install
$ agentstack add skill-resonatehq-resonate-skills-resonate-basic-durable-world-usage-rust ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Resonate Basic Durable World Usage — Rust
> v0.1.0 caveat. The Resonate Rust SDK is in active development (not yet on crates.io). API surface described here matches docs/develop/rust.mdx as of April 2026. Verify against the current SDK source before relying on any specific shape.
Overview
Durable functions in Rust are async functions decorated with #[resonate::function]. The macro wraps your function in Resonate's durable-execution machinery — every successful ctx.run / ctx.rpc / ctx.sleep is a checkpoint, and the function resumes from the last checkpoint on process restart.
This skill covers the Context API surface used inside those functions. The ephemeral-world counterpart (registration, top-level invocation, promises) lives in resonate-basic-ephemeral-world-usage-rust.
The contract
- Attribute macro:
#[resonate::function](or#[resonate::function(name = "alias")]) - Async function:
async fn— tokio is the default runtime - Return type:
Result(aliased fromresonate::error::Result) — use?for propagation - First parameter inferred kind:
| First param | Kind | Capabilities | |---|---|---| | &Context | Workflow | ctx.run, ctx.rpc, ctx.sleep, .spawn() parallelism, context accessors | | &Info | Leaf with metadata | Read-only access to info.id(), info.parent_id(), etc. | | value types (String, MyStruct) | Pure leaf | No context; stateless computation |
Minimal workflow shape:
use resonate::prelude::*;
#[resonate::function]
async fn process_order(ctx: &Context, order_id: String) -> Result {
let order = ctx.run(load_order, order_id.clone()).await?;
let charge = ctx.rpc::("charge_card", order.clone()).await?;
Ok(format!("order={} charge={}", order, charge))
}
#[resonate::function]
async fn load_order(order_id: String) -> Result {
Ok(format!("order-{}", order_id))
}
If this workflow crashes after load_order but before rpc("charge_card"), it resumes at the rpc call on restart — load_order is NOT re-executed; its stored result is returned.
ctx.run — same-process invocation
Sequential (await directly)
#[resonate::function]
async fn foo(ctx: &Context, input: String) -> Result {
let a = ctx.run(step_a, input.clone()).await?;
let b = ctx.run(step_b, a).await?;
Ok(b)
}
Parallel (.spawn() returns a DurableFuture)
#[resonate::function]
async fn foo(ctx: &Context, input: String) -> Result {
let fut_a = ctx.run(step_a, input.clone()).spawn().await?;
let fut_b = ctx.run(step_b, input).spawn().await?;
let a = fut_a.await?;
let b = fut_b.await?;
Ok((a, b))
}
.spawn() starts the sub-task without blocking; the returned DurableFuture is awaited later. This is the Rust analog of TS's begin_run / Python's begin_run — different shape, same semantics.
ctx.rpc — remote-process invocation
The registered name is a string; the result type needs a turbofish:
#[resonate::function]
async fn orchestrator(ctx: &Context, batch_id: String) -> Result {
let _result = ctx.rpc::("worker_fn", batch_id).await?;
Ok(())
}
With target group + parallelism:
#[resonate::function]
async fn parallel_remote(ctx: &Context) -> Result {
let f1 = ctx.rpc::("worker-a", "data".into())
.target("poll://any@group-a")
.spawn()
.await?;
let f2 = ctx.rpc::("worker-b", "data".into())
.target("poll://any@group-b")
.spawn()
.await?;
f1.await?;
f2.await?;
Ok(())
}
ctx.sleep — durable sleep
ctx.sleep takes a std::time::Duration. There is no upper limit; sleeps survive process restarts because the continuation lives in the Resonate server:
use std::time::Duration;
#[resonate::function]
async fn daily_digest(ctx: &Context, user_id: String) -> Result {
loop {
ctx.sleep(Duration::from_secs(24 * 60 * 60)).await?;
ctx.rpc::("send_digest", user_id.clone()).await?;
}
}
Builder options
All Context execution methods return a builder that accepts options before .await or .spawn():
use std::time::Duration;
#[resonate::function]
async fn foo(ctx: &Context) -> Result {
let result: String = ctx.run(expensive_leaf, "input".into())
.timeout(Duration::from_secs(30))
.await?;
Ok(result)
}
Documented Context builder options:
| Method | Applies to | Purpose | |---|---|---| | .timeout(Duration) | ctx.run, ctx.rpc | Execution timeout | | .target(&str) | ctx.rpc only | Worker group routing (poll://any@group-name) |
Note: .version(u32) and .tags(HashMap) are ephemeral-world only (on resonate.run() / resonate.rpc()) — they are NOT available on Context builders.
Context accessors
Inside a workflow, &Context provides read-only metadata:
#[resonate::function]
async fn foo(ctx: &Context) -> Result {
println!("id={}", ctx.id());
println!("parent={}", ctx.parent_id());
println!("origin={}", ctx.origin_id());
println!("func={}", ctx.func_name());
println!("timeout_at={}", ctx.timeout_at());
Ok(())
}
origin_id is the top-level invocation that kicked off this call graph; useful for tracing across RPC hops.
ctx.info() returns a snapshot Info struct with a superset of these accessors, including branch_id() and tags() (a &HashMap of tags set at invocation time). Useful when you want to hand execution metadata to a helper function without passing the whole Context.
Dependency access: ctx.get_dependency::()
Dependencies set on the Resonate instance with .with_dependency(value) (see resonate-basic-ephemeral-world-usage-rust) are retrieved inside a durable function by type:
use std::sync::Arc;
use sqlx::PgPool;
#[resonate::function]
async fn write_order(ctx: &Context, order_id: String) -> Result {
let pool: Arc = ctx.get_dependency::();
// wrap I/O in a leaf so the effect is checkpointed
ctx.run(insert_order_row, (pool.clone(), order_id)).await?;
Ok(())
}
async fn insert_order_row((pool, order_id): (Arc, String)) -> Result {
sqlx::query("INSERT INTO orders (id) VALUES ($1) ON CONFLICT DO NOTHING")
.bind(&order_id)
.execute(&*pool)
.await?;
Ok(())
}
Type-dispatched: there is one dependency per type per Resonate instance. For multiple values of the same logical type (e.g. two Postgres pools pointing at different databases), wrap them in newtypes and register each separately. ctx.get_dependency::() panics if no dependency of that type was registered — wire your DI at startup and keep it static, not conditional.
&Info also exposes get_dependency::(), so a leaf that takes info: &Info as its first parameter can access dependencies without needing the full Context.
> Note: ctx.get_dependency + Info::get_dependency are in the v0.1.0 SDK source (resonate-sdk-rs:resonate/src/context.rs:115, info.rs:42) but not yet covered in docs/develop/rust.mdx as of April 2026. Rust-skill review discovered the docs lag; the API is real.
Human-in-the-loop: ctx.promise::()
Context-side promises let a durable function block until an external actor (webhook, UI, CLI, operator) resolves or rejects. The returned PromiseTask is a lazy builder: you can attach .timeout(Duration) and .data(&impl Serialize), fetch the generated ID via .id().await?, eagerly create() a handle for later awaiting, or .await it directly to block.
use serde::{Serialize, Deserialize};
use std::time::Duration;
#[derive(Serialize, Deserialize)]
struct Decision {
approved: bool,
reviewer: String,
}
#[resonate::function]
async fn expense_approval(ctx: &Context, expense_id: String) -> Result {
// create a promise the reviewer will resolve from outside
let decision: Decision = ctx
.promise::()
.timeout(Duration::from_secs(24 * 60 * 60)) // 24-hour SLA
.data(&serde_json::json!({ "expense_id": expense_id }))?
.await?; // blocks until resolved
if decision.approved {
ctx.run(process_reimbursement, expense_id.clone()).await?;
Ok(format!("approved by {}", decision.reviewer))
} else {
Ok(format!("rejected by {}", decision.reviewer))
}
}
From outside the worker, resolve it with the ephemeral-world resonate.promises.resolve(...) API using the same ID. Since the ID is SDK-generated per-invocation, the typical pattern is: fetch task.id().await? before awaiting, stash it somewhere the reviewer can read (DB, webhook payload, UI), then await the promise.
For the deep HITL pattern (multi-approver, webhooks, SLAs), see resonate-human-in-the-loop-pattern-rust.
> Note: ctx.promise::() is in the v0.1.0 SDK source (resonate-sdk-rs:resonate/src/context.rs:352) with a full PromiseTask builder (.timeout, .data, .id, .create, .await) but not yet covered in docs/develop/rust.mdx as of April 2026.
Leaf with &Info
A pure leaf that still needs execution metadata uses &Info as its first parameter:
#[resonate::function]
async fn stamped_log(info: &Info, message: String) -> Result {
println!("[{}] {}", info.id(), message);
Ok(())
}
&Info is read-only — no sub-task invocation, no sleep — which lets the SDK optimize its execution differently from workflows.
Common shapes
Sequential pipeline with typed ? propagation:
#[resonate::function]
async fn ingest(ctx: &Context, url: String) -> Result {
let raw = ctx.run(fetch, url).await?;
let parsed = ctx.run(parse, raw).await?;
let count = ctx.run(persist, parsed).await?;
Ok(count)
}
Parallel fan-out over a known set:
#[resonate::function]
async fn enrich_batch(ctx: &Context, ids: Vec) -> Result> {
let mut futures = Vec::with_capacity(ids.len());
for id in ids {
futures.push(ctx.run(enrich_one, id).spawn().await?);
}
let mut results = Vec::with_capacity(futures.len());
for f in futures {
results.push(f.await?);
}
Ok(results)
}
Error mapping at a step boundary:
#[resonate::function]
async fn resilient(ctx: &Context, input: String) -> Result {
match ctx.rpc::("flaky_worker", input.clone()).await {
Ok(v) => Ok(v),
Err(_) => ctx.run(fallback, input).await,
}
}
Distinct Rust idioms
#[resonate::function]attribute macro — not a higher-order function call like TS'sresonate.register. Registration still happens ephemeral-side; the attribute just transforms the function's signature for the SDK to pick up- Turbofish on
ctx.rpc::— Rust needs the result type when it can't be inferred from the call site .spawn().await?pattern for parallelism — the double-await is real:.spawn()returns aDurableFuture(which itself is a future that resolves to the DurableFuture), and then you await theDurableFuturelater to get theTDuration::from_secs/from_millis/from_millis/from_micros— never raw numeric literals for timeResult+?everywhere — idiomatic Rust propagation; matches the SDK's uniform return shapetokio::mainruntime — async without it requires boilerplate; every Resonate Rust app uses#[tokio::main]on its entry point- Dependencies via type-dispatched
ctx.get_dependency::()— attach atResonate::new(...).with_dependency(value)time; retrieve in any durable function by type. Newtype if you need multiple values of the same logical type
Rust SDK API coverage status
Honest reading of v0.1.0 as of April 2026, verified against resonate-sdk-rs source (not just docs):
In the SDK source + documented in rust.mdx
ctx.run/ctx.rpc/ctx.sleepwith builders +.spawn()parallelism- Context accessors
ctx.id/parent_id/origin_id/func_name/timeout_at Infoaccessors on leaf-with-info functions
In the SDK source BUT not in rust.mdx (safe to use; docs lag)
ctx.promise::()— Context-side HITL primitive withPromiseTaskbuilder (.timeout,.data,.id,.create,.await). Fully featured; discovered in post-session audit againstresonate/src/context.rs:352ctx.get_dependency::()— type-dispatched DI. Panics if not registered.Info::get_dependencyis the same shape on leavesctx.info()— returns anInfosnapshot with extra accessors (branch_id(),tags())
Each has been verified as pub fn with source-level docstrings; they appear intended for users. Treat them as "supported but not yet in rust.mdx; verify signatures against resonate-sdk-rs at the commit you depend on."
NOT in the SDK source at v0.1.0
ctx.detached— no fire-and-forget; usectx.rpc(...).spawn().await?and simply don't await the returned future, or start a separate RPC without.awaitctx.random.random()/ctx.time.time()— no deterministic randomness/time helpers. Standard RustSystemTime::now()andrandwill cause replay inconsistency. Workaround: do non-deterministic work inside a leaf so the value is checkpointedctx.panic()/ctx.assert()— use standardpanic!/assert!(non-recoverable; retry policy sees them). For recoverable invariant checks, propagate viaResult
Each of these may land in a later v0.x; write your Rust workflows around the available set for now.
Related skills
resonate-basic-ephemeral-world-usage-rust— Client APIs, registration, top-level invocation, promisesresonate-basic-debugging-rust— v0.1.0-specific failure modes, git-dep install, serde errorsdurable-execution+resonate-philosophy— foundational conceptsresonate-basic-durable-world-usage-typescript+-python— sibling SDKs for comparison
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: resonatehq
- Source: resonatehq/resonate-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.