resonate-human-in-the-loop-pattern-rust — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-human-in-the-loop-pattern-rust (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
v0.1.0 caveat.ctx.promise::<T>()is a realpub fnin the Rust SDK source (resonate-sdk-rs:resonate/src/context.rs:352) with a fullPromiseTask<T>builder, but it is not yet covered indocs/develop/rust.mdxas of April 2026. The API is safe to use; cite source paths when reviewers ask. APIs may shift between v0.x releases.
A human-in-the-loop workflow blocks a durable function on a promise that an external actor settles — a reviewer clicking "approve," a webhook firing from a third-party system, an operator running a CLI command. The worker doesn't poll; it awaits the PromiseTask future and Resonate parks the execution until the promise settles.
For the language-agnostic mental model, see resonate-human-in-the-loop-pattern-typescript. The Rust shape differs in (a) type-parameterized promises ctx.promise::<T>(), (b) lazy builder with .timeout / .data / .id / .create / .await, (c) SDK-generated IDs you fetch via .id().await?.
use resonate::prelude::*;
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<String> {
// build a promise with a 24-hour SLA and the expense ID attached as data
let task = ctx
.promise::<Decision>()
.timeout(Duration::from_secs(24 * 60 * 60))
.data(&serde_json::json!({ "expense_id": expense_id }))?;
// fetch the SDK-generated promise ID so an external actor can target it
let promise_id = task.id().await?;
// stash the ID where the reviewer UI / webhook will see it
ctx.run(save_approval_id, (expense_id.clone(), promise_id.clone())).await?;
// block until someone resolves/rejects/cancels the promise
let decision: Decision = task.await?;
if decision.approved {
ctx.run(process_reimbursement, expense_id.clone()).await?;
Ok(format!("approved by {}", decision.reviewer))
} else {
Ok(format!("rejected"))
}
}
async fn save_approval_id((expense_id, promise_id): (String, String)) -> Result<()> {
// write (expense_id, promise_id) to a DB or queue a notification
Ok(())
}
async fn process_reimbursement(expense_id: String) -> Result<()> {
Ok(())
}From outside the worker — a webhook, CLI, or admin UI — resolve it via the ephemeral-world Resonate client using the fetched promise_id:
use serde_json::json;
// in an Axum/Actix handler, or a separate admin CLI
let resonate = Resonate::new(ResonateConfig::default());
resonate
.promises
.resolve(&promise_id, json!({ "approved": true, "reviewer": "[email protected]" }))
.await?;
// or reject:
resonate
.promises
.reject(&promise_id, json!({ "reviewer": "[email protected]" }))
.await?;
// or cancel (settles as rejected_canceled):
resonate.promises.cancel(&promise_id, json!(null)).await?;The resolved JSON is deserialized into the T you type-parameterized the promise with — Decision in this example.
If the call graph wants to create the promise now but await it later (e.g., after kicking off downstream work), use .create() to get a RemoteFuture<T> handle:
#[resonate::function]
async fn approve_and_also_work(ctx: &Context, job_id: String) -> Result<Decision> {
// eagerly create the promise record so downstream code can see its ID
let approval: RemoteFuture<Decision> = ctx
.promise::<Decision>()
.timeout(Duration::from_secs(3600))
.create()
.await?;
// do other durable work in parallel
ctx.run(send_reviewer_notification, job_id.clone()).await?;
// later, block on the human
let decision = approval.await?;
Ok(decision)
}This matches the TS ctx.promise() → yield promise split into two calls, giving the parent function latitude to do other work between creation and await.
A typical third-party-callback route:
use axum::{extract::Path, Json, routing::post, Router};
use serde_json::Value;
async fn docusign_webhook(
Path(envelope_id): Path<String>,
Json(body): Json<Value>,
) -> axum::http::StatusCode {
let resonate = Resonate::new(ResonateConfig::default());
let status = body.get("status").and_then(|v| v.as_str()).unwrap_or("");
let promise_id = format!("docusign:{}", envelope_id);
match status {
"completed" => {
let _ = resonate
.promises
.resolve(&promise_id, serde_json::json!({ "signed": true }))
.await;
}
"declined" | "voided" => {
let _ = resonate
.promises
.reject(&promise_id, serde_json::json!({ "reason": status }))
.await;
}
_ => {}
}
axum::http::StatusCode::OK
}
pub fn app() -> Router {
Router::new().route("/webhooks/docusign/:envelope_id", post(docusign_webhook))
}The durable function that awaits the DocuSign promise would have created it with ctx.promise().timeout(...).create().await? earlier — once the webhook hits, the worker's .await? unblocks and the workflow continues.
#[resonate::function]
async fn multi_approver(ctx: &Context, request_id: String) -> Result<String> {
let approvers = vec!["alice", "bob", "carol"];
// create one promise per approver, fetch IDs in parallel
let mut tasks = Vec::with_capacity(approvers.len());
for approver in &approvers {
let task = ctx
.promise::<Decision>()
.timeout(Duration::from_secs(48 * 60 * 60))
.data(&serde_json::json!({ "approver": approver, "request": request_id }))?;
let id = task.id().await?;
ctx.run(notify_approver, (approver.to_string(), id.clone())).await?;
tasks.push(task);
}
// block on every promise in order; any rejection propagates
let mut approvals = Vec::with_capacity(tasks.len());
for task in tasks {
approvals.push(task.await?);
}
if approvals.iter().all(|d| d.approved) {
Ok("approved".into())
} else {
Ok("rejected".into())
}
}
async fn notify_approver((approver, promise_id): (String, String)) -> Result<()> {
// send email, Slack, or queue a UI task
Ok(())
}"First-to-respond wins" races aren't directly supported — there's no select! equivalent on PromiseTask<T> yet. Workaround: spawn a helper workflow per approver and have the first to resolve mark a shared promise via resonate.promises.resolve from outside the Resonate execution layer.
ctx.promise::<Decision>() makes the return type explicit at creation; no turbofish needed when T is inferable from downstream use, otherwise .await requires T: DeserializeOwned.id() if you need to stash it before blocking. If you don't need the ID, skip this step and go straight to .await.data() takes any serializable; json! macro is the fastest path for ad-hoc payloads. Returns Result<Self> because serialization can fail — handle the ?.create(). Implements Future<Output = Result<T>>; await when ready.resolve() is async but webhook handlers shouldn't block the incoming request on SDK internals beyond network latencyT, .await? returns a deserialization error. Match the schema on both sides or use serde_json::Value for flexibilityctx.promise(id=...) at v0.1.0resonate-basic-ephemeral-world-usage-rust — resonate.promises.create/resolve/reject/cancel from the webhook sideresonate-basic-durable-world-usage-rust — ctx.promise::<T>() builder detailsresonate-saga-pattern-rust — sagas with human-gated forward stepsresonate-human-in-the-loop-pattern-typescript / -python — sibling SDKs for comparisondurable-execution — foundational replay semantics; promise-await survives crashes~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.