Resonate Durable Sleep + Scheduled Work — Java
Prerelease note.
resonate-sdk-javais published on Maven Central — pinio.resonatehq:resonate-sdk-java:0.1.1. The API mirrors the Python SDK and may change before a stable1.0. Requires Java 21+ (virtual threads, a feature generally available in Java 21). Every code block here is compile-verified against0.1.1and drawn fromexample-countdown-java/example-quickstart-javaanddevelop/java.mdx(docs PR #230).
Overview
Two related capabilities in the Java SDK:
- Durable sleep inside a workflow —
ctx.sleep(Duration)pauses execution; the worker process can exit and resume later without losing its place. The server holds the timer promise; the cost is one promise record, not process uptime. - Scheduled / recurring work —
r.schedule(...)registers a cron schedule that periodically invokes a registered function. The Java SDK includes this top-level Schedule API (plus the lower-levelr.schedulessub-client); Go's0.1.0tag has the lower-levelSchedules()sub-client (direct cron-fired promises) but not this same one-call function-dispatch convenience yet. This removes the need for an in-workflow sleep loop or an external cron trigger.
Both patterns are durable: Resonate holds the continuation (or the schedule) in its store, not in a long-running thread.
When to use
- Delays spanning minutes, hours, days, or weeks that must survive crashes (
ctx.sleep) - Reminder sequences (7-day trial expiry, multi-stage onboarding drips)
- Countdown workflows that act on each tick
- Periodic jobs on a fixed cron (
r.schedule) — daily reports, nightly reconciliation - Anywhere you would reach for
Thread.sleepbut need the work to survive a process restart
ctx.sleep basics
ctx.sleep takes a java.time.Duration and returns a future with no value to decode — await() to suspend until the timer fires.
import io.resonatehq.resonate.Context;
import java.time.Duration;
public static void reminder(Context ctx) {
ctx.sleep(Duration.ofHours(1)).await(); // no value to decode; suspends until the timer fires
// ... send the reminder
}
Crash recovery. With a real Resonate server, killing the worker mid-sleep and restarting with the same promise ID resumes from the outstanding timer rather than restarting the workflow. Local mode (new Resonate()) runs state in process memory, so crash recovery requires a real server. Long-horizon sleeps are cheap — duration is unbounded; cost is roughly one promise record, and the process need not stay alive.
Countdown loop (in-workflow recurring pattern)
example-countdown-java shows the canonical in-workflow loop: run a side effect via ctx.run (durable, checkpointed), then ctx.sleep between ticks.
import io.resonatehq.resonate.Context;
import java.time.Duration;
public static int countdown(Context ctx, int start, int stepSeconds) {
int sent = 0;
for (int i = start; i > 0; i--) {
// The side effect lives in a durable step — its result is recorded, so a resume after a
// crash short-circuits already-sent ticks instead of re-sending them.
ctx.run(Countdown::tick, i).await();
sent++;
// Sleep between ticks (but not after the last one). A durable sleep can suspend for
// seconds, hours, or days and still resume exactly where it left off.
if (i > 1) {
ctx.sleep(Duration.ofSeconds(stepSeconds)).await();
}
}
return sent;
}
public static String tick(Context ctx, int count) {
System.out.println(" [tick] " + count);
return "ok";
}
A crash during the ctx.sleep between ticks resumes mid-loop — completed ctx.run ticks short-circuit on replay; the pending sleep re-suspends until its timer fires.
Multi-stage sleeps
Sequential ctx.sleep calls are independent durable checkpoints. A crash mid-sleep resumes from that exact sleep on restart — earlier sleeps that already settled are skipped.
import java.time.Duration;
// Three-phase renewal reminder: 7 days out, 1 day out, renewal day.
public static void renewalReminder(Context ctx, String subId) {
ctx.sleep(Duration.ofDays(7)).await();
ctx.run(Reminders::sendRenewalWarning, subId).await();
ctx.sleep(Duration.ofDays(6)).await(); // 1 day before renewal
ctx.run(Reminders::sendFinalWarning, subId).await();
ctx.sleep(Duration.ofDays(1)).await(); // renewal day
ctx.run(Reminders::chargeRenewal, subId).await();
}
Scheduled work with r.schedule(...)
The Java SDK has a top-level cron Schedule API. r.schedule(...) creates a schedule that periodically invokes a registered function:
import io.resonatehq.resonate.Resonate;
import java.util.List;
import java.util.Map;
Resonate.ResonateSchedule daily = r.schedule(
"daily-report", // schedule id
"0 0 * * *", // cron expression (midnight daily)
"generateReport", // registered function name
List.of(), // positional args passed to the function
Map.of(), // keyword args
null, // promise timeout (null = default)
1); // function version
daily.delete(); // remove the schedule when no longer needed
The function named in the schedule ("generateReport") must be registered on a worker so the server has somewhere to dispatch each firing. Each tick creates a durable invocation, so the scheduled function gets the same crash-resilience as any other Resonate workflow.
The lower-level r.schedules sub-client is available for direct manipulation:
r.schedules.get("daily-report").join(); // fetch the schedule record
r.schedules.search(Map.of(), 100, null).join(); // list schedules (tags, limit, cursor)
r.schedules.delete("daily-report").join(); // remove it
Each r.schedules method returns a CompletableFuture, so .join() (or compose with thenApply) to wait.
r.schedule vs an in-workflow ctx.sleep loop. Reach for r.schedule when the cadence is a fixed cron and each firing is independent (no state carried across ticks). Reach for an in-workflow ctx.sleep loop when the interval is driven by business logic inside the workflow or the run carries state from one tick to the next.
Distinct Java idioms
ctx.sleep(Duration).await()returns nothing — sleep futures carry no value; callawait()to suspend. (Contrastctx.run/ctx.rpc, whose futures decode a result.)java.time.Durationfor sleeps —Duration.ofHours(1),Duration.ofDays(7),Duration.ofSeconds(stepSeconds). No raw millisecond integers, no cron strings forctx.sleep.r.schedule(...)uses a cron string — the schedule cadence is a 5-field cron expression; thectx.sleepduration is aDuration. Don't confuse the two.- Top-level Schedule API — Java ships
r.schedule(...)andr.schedules; the Go SDK does not have this yet. When porting a scheduled workflow from TypeScript, Python, or Rust, the Java equivalent exists — no in-workflow-loop workaround needed. ctx.sleepvsThread.sleep—Thread.sleepinside a durable function is not durable (lost on crash, holds the virtual thread for the full duration). Always usectx.sleepfor anything that must survive a restart.
Avoid
Thread.sleepinside a durable function — ephemeral; lost on crash; holds the thread for the full duration. Usectx.sleep.- Un-checkpointed side effects before a sleep — any code before a
ctx.sleep(or any durable boundary) re-executes on resume. Wrap observable side effects (DB writes, emails, webhooks) inctx.run/ctx.rpcso the durable promise records the result and short-circuits replay. - Clock-precision assumptions —
ctx.sleep(Duration.ofHours(24))firing in 23–25h is within spec (server/worker drift). Don't treat ±1h variance as a bug for long-horizon sleeps. - Confusing the cron string with the function name in
r.schedule— the argument order is(id, cron, funcName, args, kwargs, timeout, version). - Putting function arguments in the
kwargs(Map.of()) slot ofr.schedule— Java has no keyword arguments, so the SDK packs only the positionalargslist and leaves the kwargs slot empty (Durable.java). Pass all arguments inList.of(arg1, arg2, ...); keepkwargsasMap.of(). - Assuming a scheduled function runs without a registered worker —
r.scheduleonly creates the schedule; a worker must register the named function to execute each firing.
Related skills
resonate-basic-durable-world-usage-java—ctx.run,ctx.rpc,ctx.sleepfundamentals; the Context the sleep API lives onresonate-basic-ephemeral-world-usage-java— ther.schedule(...)/r.schedulessub-client surfaceresonate-recursive-fan-out-pattern-java— the worker/client builder split andCountDownLatchkeep-alive pattern; the registered function behindr.scheduleruns in such a workerdurable-execution— foundational replay semantics; sleep is a durability checkpoint by designresonate-durable-sleep-scheduled-work-typescript— sibling withresonate.schedule()(cron strings, ms durations)resonate-durable-sleep-scheduled-work-rust— sibling withresonate.schedule()- SDK parity note: TypeScript, Python, Rust, and Java expose a top-level
schedule(); the Go SDK does not yet (it uses in-workflowctx.sleeploops or external cron). When porting to Java, user.schedule(...)directly.