# Resonate Durable Sleep Scheduled Work Java

> Implement durable sleep and scheduled/recurring work in Java with Resonate — ctx.sleep(Duration) inside workflows for timers, countdowns, reminders, and long-horizon delays that survive process restarts, plus the top-level r.schedule(...) cron API (and the r.schedules sub-client) for periodic invocation of a registered function. Java's r.schedule(...) is a one-call function-dispatch convenience the Go SDK doesn't have yet (Go 0.1.0 has the lower-level Schedules() sub-client instead). Use when a workflow must wait for hours or days, or when a function should run on a fixed cron schedule. Verified against example-countdown-java / example-quickstart-java and develop/java.mdx (docs PR

- Skill: `resonatehq/resonate-durable-sleep-scheduled-work-java` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-durable-sleep-scheduled-work-java`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-durable-sleep-scheduled-work-java/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-durable-sleep-scheduled-work-java

---


# Resonate Durable Sleep + Scheduled Work — 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). Every code block here is compile-verified against `0.1.1` and drawn from `example-countdown-java` / `example-quickstart-java` and `develop/java.mdx` (docs PR #230).

## Overview

Two related capabilities in the Java SDK:

1. **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.
2. **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-level `r.schedules` sub-client); Go's `0.1.0` tag has the lower-level `Schedules()` 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.sleep` but 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.

```java
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.

```java
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.

```java
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:

```java
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:

```java
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; call `await()` to suspend. (Contrast `ctx.run` / `ctx.rpc`, whose futures decode a result.)
- **`java.time.Duration` for sleeps** — `Duration.ofHours(1)`, `Duration.ofDays(7)`, `Duration.ofSeconds(stepSeconds)`. No raw millisecond integers, no cron strings for `ctx.sleep`.
- **`r.schedule(...)` uses a cron string** — the schedule cadence is a 5-field cron expression; the `ctx.sleep` duration is a `Duration`. Don't confuse the two.
- **Top-level Schedule API** — Java ships `r.schedule(...)` and `r.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.sleep` vs `Thread.sleep`** — `Thread.sleep` inside a durable function is not durable (lost on crash, holds the virtual thread for the full duration). Always use `ctx.sleep` for anything that must survive a restart.

## Avoid

- **`Thread.sleep` inside a durable function** — ephemeral; lost on crash; holds the thread for the full duration. Use `ctx.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) in `ctx.run` / `ctx.rpc` so 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 of `r.schedule`** — Java has no keyword arguments, so the SDK packs only the positional `args` list and leaves the kwargs slot empty (`Durable.java`). Pass all arguments in `List.of(arg1, arg2, ...)`; keep `kwargs` as `Map.of()`.
- **Assuming a scheduled function runs without a registered worker** — `r.schedule` only 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.sleep` fundamentals; the Context the sleep API lives on
- `resonate-basic-ephemeral-world-usage-java` — the `r.schedule(...)` / `r.schedules` sub-client surface
- `resonate-recursive-fan-out-pattern-java` — the worker/client builder split and `CountDownLatch` keep-alive pattern; the registered function behind `r.schedule` runs in such a worker
- `durable-execution` — foundational replay semantics; sleep is a durability checkpoint by design
- `resonate-durable-sleep-scheduled-work-typescript` — sibling with `resonate.schedule()` (cron strings, ms durations)
- `resonate-durable-sleep-scheduled-work-rust` — sibling with `resonate.schedule()`
- **SDK parity note:** TypeScript, Python, Rust, and **Java** expose a top-level `schedule()`; the Go SDK does not yet (it uses in-workflow `ctx.sleep` loops or external cron). When porting *to* Java, use `r.schedule(...)` directly.

