Skip to main content

resonate-human-in-the-loop-pattern-rust

Implement human-in-the-loop workflows in Rust with Resonate — durable functions that block on ctx.promise::<T>() until an external actor (webhook, UI, CLI, operator) resolves via resonate.promises.resolve/reject. Use when a Rust workflow step must wait on a decision or data that doesn't come from another worker. Note: ctx.promise is in the SDK (0.6.0) but not yet in rust.mdx.

跳到安装

来源信息

仓库
resonatehq/resonate-skills
最近来源活动
2026年8月6日 11:59
检测到的 SKILL.md 语言
英语
星标
6
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
resonate-human-in-the-loop-pattern-rust
description
Implement human-in-the-loop workflows in Rust with Resonate — durable functions that block on ctx.promise::<T>() until an external actor (webhook, UI, CLI, operator) resolves via resonate.promises.resolve/reject. Use when a Rust workflow step must wait on a decision or data that doesn't come from another worker. Note: ctx.promise is in the SDK (0.6.0) but not yet in rust.mdx.
# Resonate Human-in-the-Loop Pattern — Rust > **SDK note (0.6.0).** `ctx.promise::<T>()` is a real `pub fn` in the Rust SDK source (`resonate-sdk-rs:resonate/src/context.rs`) with a full `PromiseTask<T>` builder, but it is not yet covered in `docs/develop/rust.mdx`. The API is safe to use; cite source paths when reviewers ask. APIs may shift between 0.x releases. ## Overview 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?`. ## When to use - Approval gates (expense, deploy, content moderation) - Third-party callbacks (Stripe, DocuSign, Twilio) - Operator unblock steps (break-glass in runbooks) - Any step where the data or decision comes from outside the Resonate worker set ## Basic shape ```rust 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`: ```rust 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": "alice@acme.io" })) .await?; // or reject: resonate .promises .reject(&promise_id, json!({ "reviewer": "bob@acme.io" })) .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. ## Eagerly create, hand off the future 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: ```rust #[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. ## Webhook-driven resolution (Axum) A typical third-party-callback route: ```rust 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. ## Multi-approver fan-out (all must approve) ```rust #[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. ## Distinct Rust idioms - **Typed promises `PromiseTask<T>`** — `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().await?` before `.await?`** — the SDK generates IDs; fetch via `.id()` if you need to stash it before blocking. If you don't need the ID, skip this step and go straight to `.await` - **`serde_json::json!` + `&impl Serialize`** — `.data()` takes any serializable; `json!` macro is the fastest path for ad-hoc payloads. Returns `Result<Self>` because serialization can fail — handle the `?` - **`RemoteFuture<T>`** — the handle returned by `.create()`. Implements `Future<Output = Result<T>>`; await when ready - **Axum/Actix webhook handlers** — fire-and-return-200 is the right shape; the SDK's `.resolve()` is async but webhook handlers shouldn't block the incoming request on SDK internals beyond network latency ## Avoid - **Polling via `ctx.sleep` + a status check** — defeats the durable-await semantics; costs checkpoints + wall-clock time - **Using the promise ID outside serde-compatible payloads** — if an external actor constructs a custom JSON payload that doesn't deserialize to your typed `T`, `.await?` returns a deserialization error. Match the schema on both sides or use `serde_json::Value` for flexibility - **Assuming promise IDs are stable across invocations** — the SDK generates them per-invocation within the call graph. A replay reuses the same ID for the same logical promise (idempotency), but you cannot hand-craft IDs like you could in TS/Python's `ctx.promise(id=...)` ## Related skills - `resonate-basic-ephemeral-world-usage-rust` — `resonate.promises.create/resolve/reject/cancel` from the webhook side - `resonate-basic-durable-world-usage-rust` — `ctx.promise::<T>()` builder details - `resonate-saga-pattern-rust` — sagas with human-gated forward steps - `resonate-human-in-the-loop-pattern-typescript` / `-python` — sibling SDKs for comparison - `durable-execution` — foundational replay semantics; promise-await survives crashes
在 GitHub 查看