Install
$ agentstack add skill-resonatehq-resonate-skills-resonate-human-in-the-loop-pattern-java ✓ 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 Used
- ✓ 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 Human-in-the-Loop Pattern — Java
> Prerelease note. resonate-sdk-java is published on Maven Central — pin io.resonatehq:resonate-sdk-java:0.1.1. The API mirrors the Python SDK and may change before a stable 1.0. Requires Java 21+ (virtual threads, a feature generally available in Java 21). There is no Java HITL example repo yet, so every code block here uses only documented SDK surface, compile-verified against 0.1.1 and cross-checked against develop/java.mdx (docs PR #230) and the SDK source.
Overview
For the language-agnostic mental model, start with resonate-human-in-the-loop-pattern-typescript. The idea is identical: create a latent durable promise, hand its ID to the external actor who will settle it, and await — the workflow parks until settlement arrives, surviving any number of crashes or restarts.
Resolution from Java is direct. The Java SDK ships a top-level r.promises sub-client (it mirrors Python), so settling a promise from outside the workflow is r.promises.resolve(id, new Value(...)). There is no manual base64 encoding — that extra step is a Go-specific workaround for a sub-client Go lacks. Don't copy it into Java.
When to use
- Approval gates (budget, deploy, content moderation)
- Third-party webhook callbacks (Stripe, DocuSign, Twilio)
- Operator unblock steps in runbooks
- Any step where the decision or data originates outside the Resonate worker set
Basic shape
Workflow side — ctx.promise() → promise.id() → publish → await
import io.resonatehq.resonate.Context;
import io.resonatehq.resonate.Context.ResonateFuture;
import java.time.Duration;
public final class Approval {
private Approval() {}
public record ReviewRequest(String item, String requester) {}
/**
* Parks until an external actor settles the latent promise. promise.id() returns the id once
* the promise has been created (instantaneous in local mode, a network round-trip against a
* real server); await suspends the workflow (durably) until it is settled from outside.
*/
public static String approvalWorkflow(Context ctx, ReviewRequest req) {
// A latent durable promise — no registered function backs it; it settles only when an
// external caller resolves it. 24-hour timeout, capped at the workflow deadline.
ResonateFuture promise = ctx.promise(Duration.ofHours(24));
String approvalId = promise.id();
// Publish the promise ID inside a ctx.run so the side effect is checkpointed and does
// not re-run on replay. In production: write to a DB, push to a notification queue, etc.
ctx.run(Approval::publishApprovalId, req.item(), approvalId).await();
// Suspend until the promise is settled externally; decode the decision.
Object decision = promise.await();
return "item " + req.item() + " decided: " + decision;
}
/** Checkpointed publication of the promise ID. Replace the println with a real notification. */
public static String publishApprovalId(Context ctx, String item, String approvalId) {
System.out.printf(" [workflow] awaiting approval for %s — promise id: %s%n", item, approvalId);
return "published";
}
}
Key points:
ctx.promise()takes an optionalDurationtimeout;ctx.promise()with no arg uses a 1-day default capped at the parent's remaining deadline.promise.id()returns the promise's id once it has been created — instantaneous in local mode, blocking on the create round-trip against a real server.- Publish the ID inside a
ctx.runso the publication is itself durable. A bare side effect abovepromise.await()re-runs on every replay pass. promise.await()with no decode is fine when you only need to know the promise settled; here we read the decisionObject.
Resolving from outside
The Java SDK gives you three mechanisms. Prefer the sub-client when resolving from Java.
1. The promises sub-client (preferred when resolving from Java)
import io.resonatehq.resonate.Types.Value;
// approve — the Value's data is decoded into what the workflow's await expects
r.promises.resolve(approvalId, new Value(null, "approved")).join();
// reject
r.promises.reject(approvalId, new Value(null, "rejected: budget exceeded")).join();
// cancel
r.promises.cancel(approvalId, new Value(null, "withdrawn")).join();
new Value(headers, data) — pass null headers and the decision as data. Each call returns a CompletableFuture, so .join() (or compose with thenApply) to wait for it. r.promises also exposes get, create, and search. No base64 encoding is required — the SDK codec handles the durability boundary for you.
2. CLI (simplest — ops gates, manual approval)
resonate promise resolve --data '"approved"'
resonate promise reject --data '"rejected: budget exceeded"'
The --data value is the JSON payload decoded into the type the workflow's await expects. For a String-shaped decision, a quoted JSON string like '"approved"' is correct. See resonate-cli for the full flag reference.
3. Server HTTP API (any language / tool)
POST to the server's promise-settle endpoint with the promise ID and a JSON body — useful from a webhook handler, an admin script, or a serverless function in any language. Going straight to the server bypasses the SDK codec, so the data field must be the base64 encoding of the JSON-serialized payload (the wire format the SDK stores — value → JSON → base64, Codec.java). "approved" as JSON, base64-encoded, is ImFwcHJvdmVkIg==:
curl -s -X POST "http://localhost:8001/promises/${APPROVAL_ID}/resolve" \
-H "Content-Type: application/json" \
-d '{"value":{"data":"ImFwcHJvdmVkIg=="}}'
This base64 step is exactly what r.promises.resolve and the CLI do for you — prefer mechanism 1 or 2 from Java/ops, and reach for the raw endpoint only from a non-Java caller. The ImFwcHJvdmVkIg== value is correct for the default NoopEncryptor; if you configured an Encryptor on the builder, the payload is encrypted before base64, so a raw caller would have to replicate that encryption too — another reason to use the sub-client or CLI.
Hand-off across processes
A realistic deployment has the worker that creates the promise and the process that resolves it in different programs. The pattern:
- The worker workflow creates the promise and persists
promise.id()somewhere the resolver can read it (a DB row, a notification, a ticket). - The external actor (an HTTP webhook handler, an operator running the CLI, an admin service) settles it by ID via one of the three mechanisms above.
- The worker's
promise.await()wakes and returns the decoded decision — even if the worker crashed and restarted in between.
Because resolution is by promise ID, the resolver needs nothing but the ID and a connection to the same Resonate server.
Known gaps
- No hand-chosen promise IDs.
ctx.promise()generates the ID internally; you cannot pass an application-layer ID like"approval/order-42". Fetch the ID viapromise.id()after creation and publish it explicitly. This may change as the API settles toward1.0.
Distinct Java idioms
r.promises.resolve(id, new Value(null, data))— the clean external-resolution path. No base64 layer (that's a Go-only workaround).Value(headers, data)takesnullheaders and the payload asdata.CompletableFuturefrom the sub-client —r.promises.resolve(...)returns aCompletableFuture; call.join()to wait orthenApplyto compose.- Publish the ID inside
ctx.run. Java durable functions are not generators (noyield); wrap every observable side effect — DB write, notification, the hand-off that publishes the promise ID — in actx.runso it is checkpointed and not repeated on replay. ctx.promise(Duration)vsctx.promise()— theDurationoverload sets an explicit timeout; the no-arg form uses the 1-day default capped at the parent deadline.promise.await()returnsObject—ctx.promise()is untyped; cast or.toString()the decoded decision as needed.
Avoid
- Copying the Go base64 encoding dance. The Java SDK has
r.promises.resolve— usenew Value(null, data)directly. ManualJSON → base64 → quotedencoding is a Go-only workaround for a sub-client that Go lacks. - Polling via
ctx.sleep+ a status check. Defeats park-and-resume; burns checkpoints and wall-clock time. Usectx.promise()+promise.await(). - Publishing the promise ID outside a
ctx.run. A bare write abovepromise.await()re-runs on every replay. Checkpoint the publication. - Settling before publishing the ID. A narrow race: if the resolver runs before the
ctx.runthat publishes the ID is checkpointed, a crash between those lines can lose the ID. Checkpoint the publication first, then await. - Long-blocking work inside
ctx.run.ctx.runfunctions must return promptly; a blocking call holds the task lease open until TTL expires. Long waits belong inctx.promise(latent, externally settled).
Related skills
resonate-basic-durable-world-usage-java—ctx.promise,ResonateFuture.await, the replay modelresonate-basic-ephemeral-world-usage-java— ther.promisessub-client surface (resolve/reject/cancel/get/create/search)resonate-cli—resonate promise resolve / reject / cancelflag referencedurable-execution— foundational replay semantics; whyawaitsurvives crashesresonate-human-in-the-loop-pattern-typescript— language-agnostic mental modelresonate-human-in-the-loop-pattern-python— the closest sibling; the Javapromisessub-client mirrors Python
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.