agentsclimarketplace

Resonate basic durable world usage java

Skill resonatehq/resonate-skills/resonate-basic-durable-world-usage-java

Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.

Install
npx -y skills add resonatehq/resonate-skills --skill resonate-basic-durable-world-usage-java

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when writing the body of a Java durable function — any method registered with r.register or passed as a method reference to ctx.run. Core patterns for the Context API surface — the Context first-parameter signature, Context APIs (ctx.run, ctx.rpc, ctx.sleep, ctx.promise, ctx.detached, ResonateFuture.await), per-call options via ctx.options(new Opts()), context accessors (ctx.info), type-keyed dependency injection (ctx.getDependency), retry policies, dispatch-then-await fan-out, and the replay model. For multi-stage sleep timers, countdowns, and the cron Schedule API see resonate-durable-sleep-scheduled-work-java. Requires Java 21+ (virtual threads). Verified against the resonatehq-examples/*-java repos and develop/java.mdx (docs PR #230) at io.resonatehq:resonate-sdk-java:0.1.1.

The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

16.8 KB, as published. Nobody here has run it

Resonate Basic Durable World Usage — 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+ — the SDK uses virtual threads, a feature generally available in Java 21. Every code block here is compile-verified against 0.1.1 with a Java 21 toolchain, and cross-checked against the resonatehq-examples/*-java repos and develop/java.mdx (docs PR #230).

Overview

A durable function in Java is an ordinary static method whose first parameter is a Context, followed by up to five JSON-serializable application arguments. Every Context method returns a ResonateFuture right away — the future object is allocated immediately, while the durable promise behind it is created on the server (inline in local mode, a network round-trip against a real server). You call future.await() to read the result. If the awaited promise is still pending, await suspends the workflow and re-enters it when the promise settles — and suspension is durable, so any worker (including this one after a restart) can resume the continuation.

This skill covers the Context API surface used inside durable functions. The ephemeral-world counterpart (the builder, register, top-level run/rpc/get) lives in resonate-basic-ephemeral-world-usage-java.

When to use

Use this skill whenever you are writing the body of a Java method that will be registered with r.register or handed as a method reference to ctx.run. The moment you touch ctx.run, ctx.rpc, ctx.sleep, ctx.promise, or ctx.detached, this skill applies.

Function shape

A durable function takes a Context first, then up to five args. Both leaf functions and workflows share the shape — a "workflow" is a function that itself performs durable ops:

import io.resonatehq.resonate.Context;

public final class Greeter {
    private Greeter() {}

    // A leaf: no durable op, just computes a value.
    public static String formatGreeting(Context ctx, String name) {
        return "hello, " + name + "!";
    }

    // A workflow: orchestrates sub-tasks via the Context.
    public static String greetWorkflow(Context ctx, String name) {
        return ctx.run(Greeter::formatGreeting, name).await();
    }
}

Functions are always supplied as method references (Owner::fn). The reference's functional-interface arity (Fn.F0 through Fn.F5) bounds a durable function to at most five arguments beyond the Context. Argument and return types must be JSON-serializable — they cross the durability boundary so the workflow can resume on any worker.

ctx.run — same-process invocation

ctx.run dispatches a function in the current process and returns a ResonateFuture immediately. Awaiting right after is sequential:

public static String myWorkflow(Context ctx, String input) {
    return ctx.run(Greeter::formatGreeting, input).await();
}

To run in parallel, dispatch every child first, then await each future (see Fan-out below).

ctx.run functions must return promptly. The runtime drains every ctx.run-spawned child future before the workflow can suspend or settle. A function that blocks indefinitely holds the task lease open until it expires. Long-running or external-blocking work belongs in ctx.rpc (remote dispatch) or ctx.promise (latent durable promise), not ctx.run.

Fan-out / fan-in

Dispatch ALL children first (collect futures), then await each. Awaiting inside the dispatch loop serializes them — never what you want for a fan-out. From example-fan-out-fan-in-java:

import io.resonatehq.resonate.Context.ResonateFuture;
import java.util.ArrayList;
import java.util.List;

public record Delivery(String channel, boolean ok) {}

public static List<Delivery> fanout(Context ctx, List<String> channels, String message) {
    // Fan out: spawn a child per channel, collecting futures without awaiting yet.
    List<ResonateFuture<Delivery>> futures = new ArrayList<>();
    for (String channel : channels) {
        futures.add(ctx.run(FanOutFanIn::send, channel, message));
    }
    // Fan in: await every child and aggregate.
    List<Delivery> delivered = new ArrayList<>();
    for (ResonateFuture<Delivery> future : futures) {
        delivered.add(future.await());
    }
    return delivered;
}

public static Delivery send(Context ctx, String channel, String message) {
    return new Delivery(channel, true);
}

The dispatch loop runs in series in user code, but each ctx.run creates an independent durable promise — the children execute concurrently. The await loop only reads results. The same pattern applies to ctx.rpc.

ctx.rpc — remote-process invocation

ctx.rpc dispatches a registered function by method reference or by name to a remote process. await suspends the workflow until the remote function returns.

public static String myWorkflow(Context ctx, String input) {
    return (String) ctx.rpc("remote-func", input).await();
}

The by-name form returns a ResonateFuture<Object> (hence the cast) and does not require the target to be registered locally — it dispatches to a function registered on the executing group. The method-reference form requires a local registration (it recovers name and version from the registry) and gives a typed future. ctx.rpc takes per-call options (e.g. target) the same way ctx.run does.

ctx.sleep — durable sleep

ctx.sleep takes a java.time.Duration and returns a future with no value to decode. There is no upper bound; sleeps survive process restarts.

import java.time.Duration;

public static void reminder(Context ctx) {
    ctx.sleep(Duration.ofHours(1)).await(); // no value to decode
    // ... send the reminder
}

ctx.promise — latent durable promise

ctx.promise() creates a durable promise no registered function backs — it settles only when an external actor (webhook, human, CLI, or r.promises.resolve) settles it by ID. await suspends until then.

import io.resonatehq.resonate.Context.ResonateFuture;

public static String approval(Context ctx) {
    ResonateFuture<Object> promise = ctx.promise(); // 1-day default timeout, capped at the parent's remaining deadline
    String approvalId = promise.id();
    // hand approvalId to whoever resolves it
    Object decision = promise.await(); // suspends until settled externally
    return decision.toString();
}

ctx.promise(Duration) sets an explicit timeout; ctx.promise() uses a 1-day default capped at the parent's remaining deadline. For full external-resolution mechanics — including r.promises.resolve(id, new Value(...)) — see resonate-human-in-the-loop-pattern-java.

ctx.detached — fire-and-forget remote dispatch

ctx.detached dispatches a remote function without suspending the parent. It returns only the new promise's ID; the detached execution's lifecycle is independent of the parent.

public static void placeOrder(Context ctx, String orderId) {
    String auditId = ctx.detached("writeAuditLog", orderId).id();
    // auditId references the detached execution; the parent does not await it
}

ctx.detached is by-name String only — unlike ctx.run and ctx.rpc, which accept either a method reference or a String, there is no method-reference overload (verified Context.java:600). The detached target must be registered on whichever group executes it.

Per-call options

run / rpc (on the instance and on the Context) take options from a fresh handle minted by options(...), carrying a Context.Opts value. Opts is an immutable record with builder-style with* methods — override one field, inherit the rest:

import io.resonatehq.resonate.Context.Opts;
import io.resonatehq.resonate.Retry.Constant;

public static String checkout(Context ctx) {
    return ctx.options(new Opts().withRetryPolicy(new Constant(5, 0)))
            .run(Greeter::formatGreeting, "world")
            .await();
}
Opts fieldMethodPurpose
timeoutwithTimeout(Duration)Caps the promise deadline (bounded by the parent's remaining deadline).
targetwithTarget(String)Logical routing address for rpc. Empty falls back to the configured group.
versionwithVersion(int)Selects the registered version for by-name dispatch.
retryPolicywithRetryPolicy(RetryPolicy)Per-call retry policy for a ctx.run leaf.

The options handle is a fresh instance — the originating Context is untouched, so held references never interfere.

Context accessors

Inside a workflow, ctx.info() exposes read-only metadata about the current execution:

import io.resonatehq.resonate.Types.Info;

public static void inspect(Context ctx) {
    Info info = ctx.info();
    String id = info.id();             // this execution's promise ID
    String parent = info.parentId();   // parent promise ID
    String origin = info.originId();   // root workflow ID — stable across the whole tree
    String fn = info.funcName();       // registered function name
    long deadline = info.timeoutAt();  // promise deadline, epoch milliseconds
    // info.branchId() and info.tags() are also available
}

ctx.info().originId() is stable across the whole call tree — useful for distributed tracing.

Dependency injection

Register application dependencies (database handles, HTTP clients, gateways) on the instance with withDependency and fetch them inside a function by type with ctx.getDependency:

r.withDependency(new PaymentGateway());

public static String charge(Context ctx, int amount) {
    PaymentGateway gateway = ctx.getDependency(PaymentGateway.class);
    return gateway.submit(amount);
}

Dependencies are keyed by their concrete class — register at most one instance per type, before processing starts. With type-keyed DI you do not have to close clients over the workflow or pass them as serialized args.

Retries

When a function passed to ctx.run (or a top-level run) returns by throwing, Resonate re-executes it according to a retry policy. Retries apply only to leaf functions — a function that itself performs a durable op (ctx.run / ctx.rpc / ctx.sleep / ctx.promise) is a workflow, recovered by replay, and is never retried.

The SDK ships four policies, all implementing Retry.RetryPolicy (delays are in seconds):

PolicyBehavior
new Retry.Exponential(delay, maxRetries, factor, maxDelay)Delay delay * factor^attempt, capped at maxDelay, for maxRetries retries.
new Retry.Linear(maxRetries, delay)Attempt N waits delay * N.
new Retry.Constant(maxRetries, delay)Fixed delay between attempts.
new Retry.Never()A single attempt (no retries).
import io.resonatehq.resonate.Context.Opts;
import io.resonatehq.resonate.Retry.Exponential;

public static String chargeStep(Context ctx, int amount) {
    return ctx.options(new Opts().withRetryPolicy(
                    new Exponential(1, 5, 2, 30))) // 1s base, doubling, capped at 30s, 5 retries
            .run(Greeter::formatGreeting, "card")
            .await();
}

A retry policy can be set three ways, in increasing specificity: SDK-wide (Resonate.builder().retryPolicy(...)), per-function (register(ref, name, version, retryPolicy)), or per-call (ctx.options(new Opts().withRetryPolicy(...)).run(...)). The SDK-wide default is new Exponential(1, 30, 2, Long.MAX_VALUE) — 1-second base, 30 retries, doubling, with an effectively unbounded maxDelay cap.

The replay model

Whenever a workflow suspends and resumes — after a ctx.sleep, a ctx.rpc await, or a pending ctx.promise await — the entire workflow body re-runs from the top. Resonate short-circuits already-settled child promises (their stored results replay without re-executing the function), but any code that runs before reaching a settled durable boundary executes again on every resume.

Consequence: side effects before a durable boundary run more than once.

// WRONG — the println and the DB write re-run on every resume.
public static String badWorkflow(Context ctx, String id) {
    System.out.println("starting order"); // runs on every replay
    db.insert(id);                         // inserts a duplicate row on replay!
    ctx.sleep(Duration.ofHours(1)).await();
    return "done";
}

// CORRECT — the side effect is wrapped in ctx.run, so it is checkpointed:
// on resume the settled promise short-circuits and insertOrder does not re-run.
public static String goodWorkflow(Context ctx, String id) {
    ctx.run(MyWorkflow::insertOrder, id).await(); // checkpointed write
    ctx.sleep(Duration.ofHours(1)).await();
    return "done";
}

Rules of thumb:

  • DB writes, payment calls, emails, and any observable I/O must live inside a ctx.run or ctx.rpc so the result is checkpointed.
  • Pure computation (parsing, formatting, record construction) before a durable boundary is fine — it is idempotent.
  • The Resonate instance (r) cannot be used inside durable functions; the Context (ctx) cannot be used in the ephemeral world.

Distinct Java idioms

  • Method references, not strings, at typed call sitesctx.run(Owner::fn, args) gives a typed ResonateFuture<R>. The by-name ctx.rpc("name", args) form returns ResonateFuture<Object>.
  • Immutable Opts record with with* methodsnew Opts().withRetryPolicy(...).withTarget(...). Each with* returns a fresh record; chain them. Pass via ctx.options(opts).run(...).
  • java.time.Duration for timeDuration.ofHours(1), Duration.ofSeconds(5). Retry-policy delays, by contrast, are plain long seconds.
  • ctx.sleep(...).await() returns nothing — there is no value to decode; call await() to suspend.
  • record types for args and resultspublic record Delivery(String channel, boolean ok) {} is the idiomatic JSON-serializable payload.
  • Type-keyed DI via ctx.getDependency(Foo.class) — no need to close clients over the workflow; register once with r.withDependency(new Foo()).
  • At most five args beyond ContextFn.F0Fn.F5. Bundle more into a record.

Avoid

  • Placing observable side effects (DB writes, emails, charges) outside a ctx.run / ctx.rpc — they re-execute on every replay.
  • Branching on System.currentTimeMillis(), Math.random(), or UUID.randomUUID() directly in a workflow body — non-deterministic values differ across replay passes and diverge execution. Compute them inside a leaf so the result is checkpointed.
  • Awaiting inside a fan-out dispatch loop — this serializes what should be concurrent. Collect all futures, then await.
  • Using a method reference with ctx.detached — it is by-name String only.
  • Using an instance method reference (this::fn, obj::fn) as a durable function — durable functions must be public static. The SDK recovers and invokes the referenced method by reflection without an object instance, so an instance reference will not work even though it compiles.
  • Long-blocking work inside ctx.run — it holds the task lease open until the TTL expires. Use ctx.rpc or ctx.promise.
  • Calling the Resonate instance (r) from inside a durable function — use Context APIs only.

Related skills

  • resonate-basic-ephemeral-world-usage-java — the builder, register, top-level run/rpc/get, promises / schedules sub-clients, stop semantics
  • resonate-recursive-fan-out-pattern-java — dynamic-depth tree fan-outs, cross-worker recursion via ctx.rpc
  • resonate-human-in-the-loop-pattern-java — full ctx.promise resolution mechanics via r.promises.resolve
  • resonate-durable-sleep-scheduled-work-java — recurring work loops, ctx.sleep, the schedules sub-client
  • durable-execution — foundational concepts: checkpointing, replay, the ephemeral/durable split
  • resonate-defaults — full defaults table across all SDKs with source citations
  • resonate-basic-durable-world-usage-python — the closest sibling; the Java Context API mirrors Python

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.