Install
$ agentstack add skill-resonatehq-resonate-skills-resonate-saga-pattern-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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Resonate Saga Pattern — Rust
> v0.1.0 caveat. The Rust SDK is in active development. The saga pattern below uses only documented v0.1.0 surface (ctx.run, Result, ? propagation). Verify against the current SDK source before shipping.
Overview
A saga is a long-running transaction split into smaller steps, each with a compensating action. Forward steps run top-to-bottom; on failure, compensations run bottom-to-top. Rust expresses this naturally via Result + ? for forward propagation and a tracked "completed" list for compensation dispatch.
For the language-agnostic mental model, see resonate-saga-pattern-typescript.
When to use
- Multi-step workflow where intermediate state is visible to other systems
- Each step is idempotent or inexpensive to retry individually
- Compensation logic exists for every committed step
- You need "all or nothing" consistency without a distributed transaction
Basic shape
use resonate::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
enum Step { Inventory, Payment, Shipment }
#[derive(Clone, Serialize, Deserialize)]
struct SagaResult {
status: String,
order_id: String,
compensated: Vec,
}
#[resonate::function]
async fn place_order(ctx: &Context, order_id: String) -> Result {
let mut completed: Vec = Vec::new();
// attempt the forward path
let result = try_forward(ctx, &order_id, &mut completed).await;
match result {
Ok(()) => Ok(SagaResult {
status: "success".into(),
order_id,
compensated: vec![],
}),
Err(_err) => {
// compensate in reverse order
let mut comp_names = Vec::new();
for step in completed.iter().rev() {
ctx.run(compensate, (step.clone(), order_id.clone()))
.await?;
comp_names.push(format!("{:?}", step));
}
Ok(SagaResult {
status: "failed".into(),
order_id,
compensated: comp_names,
})
}
}
}
async fn try_forward(
ctx: &Context,
order_id: &str,
completed: &mut Vec,
) -> Result {
ctx.run(reserve_inventory, order_id.to_string()).await?;
completed.push(Step::Inventory);
ctx.run(charge_payment, order_id.to_string()).await?;
completed.push(Step::Payment);
ctx.run(create_shipment, order_id.to_string()).await?;
completed.push(Step::Shipment);
Ok(())
}
#[resonate::function]
async fn compensate((step, order_id): (Step, String)) -> Result {
match step {
Step::Shipment => { /* cancel_shipment(&order_id) */ Ok(()) }
Step::Payment => { /* refund_payment(&order_id) */ Ok(()) }
Step::Inventory => { /* release_inventory(&order_id) */ Ok(()) }
}
}
#[resonate::function]
async fn reserve_inventory(order_id: String) -> Result { Ok(()) }
#[resonate::function]
async fn charge_payment(order_id: String) -> Result { Ok(()) }
#[resonate::function]
async fn create_shipment(order_id: String) -> Result { Ok(()) }
Each forward step is a durable checkpoint. If the worker crashes mid-saga, Resonate resumes from the last successful yield (in Rust, the last successful .await?).
Compensation must be retryable
A compensation that fails stalls the saga in an inconsistent state:
use std::time::Duration;
for step in completed.iter().rev() {
ctx.run(compensate, (step.clone(), order_id.clone()))
.timeout(Duration::from_secs(600)) // 10 min per compensation
.await?;
}
If a compensation exhausts its timeout/retry, you have a real inconsistency — log and alert.
Explicit step objects via enum dispatch
Rust's enum + match is idiomatic for a small, closed set of saga steps:
#[derive(Clone, Serialize, Deserialize)]
enum ForwardStep { Inventory, Payment, Shipment }
impl ForwardStep {
async fn run(&self, ctx: &Context, order_id: &str) -> Result {
match self {
ForwardStep::Inventory => ctx.run(reserve_inventory, order_id.to_string()).await?,
ForwardStep::Payment => ctx.run(charge_payment, order_id.to_string()).await?,
ForwardStep::Shipment => ctx.run(create_shipment, order_id.to_string()).await?,
};
Ok(())
}
}
For large sets of dynamic steps, prefer Vec> — but that adds dyn-trait complexity beyond the basic pattern.
Distinct Rust idioms
Result+?handles forward-path propagation naturally — notry/exceptneeded for "raise on first error"- Explicit
let result = ... .await+matchfor the catch-and-compensate branch, since?would bail before compensation runs completed.iter().rev()for reverse iteration — idiomatic Rust, noreversed()builtin neededenum+matchfor compensation dispatch — cleaner than string-based switches(Step, String)tuple parameter to bundle args into a single serializable input forctx.run— the SDK serializes the whole argument, so tuples work naturallyClonederive on step enums — needed becauseiter().rev()borrows and compensation needs owned values
Avoid
- Panicking inside a durable function (
panic!,unreachable!,assert!) — panics in v0.1.0 don't compensate; useResultexplicitly - Non-idempotent compensations — each compensation may retry
- Compensations that read the SoR without a fresh
ctx.run— the read happens inside a leaf for checkpointing
Related skills
resonate-basic-durable-world-usage-rust—ctx.run, builder options, function kindsresonate-recursive-fan-out-pattern-rust— parallel fan-out, can be combined with sagasdurable-execution— foundational replay semanticsresonate-saga-pattern-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.