Skip to main content

Schedules

Not yet released

The Java ScheduleClient is code complete but has not been released yet. Until it is, you can still use Schedules with Java workflows through the Cadence CLI, which works with any workflow regardless of the client language. See the CLI quick reference on the Schedules concept page.

The Java client exposes schedule management through ScheduleClient, obtained from any WorkflowClient instance. For a full explanation of overlap policies, backfill, catch-up, and when to use Schedules over CronSchedule, see the Schedules concept page.

Getting the client​

ScheduleClient is obtained from an already-initialized WorkflowClient. For the full client setup (service transport, domain, workers), see the Workers page.

WorkflowClient workflowClient = WorkflowClient.newInstance(service, options);
ScheduleClient sc = workflowClient.scheduleClient();

All ScheduleClient methods return CompletableFuture<T>. Call .join() to block synchronously, or compose with .thenApply(...) for non-blocking pipelines:

// blocking
sc.createSchedule("daily-etl", spec, action, policies).join();

// non-blocking
sc.pauseSchedule("daily-etl", "maintenance").thenRun(() -> log.info("paused"));

Creating a schedule​

Java uses a builder pattern for all schedule types:

import com.uber.cadence.client.schedule.*;
import com.uber.cadence.common.RetryOptions;
import java.time.Duration;

ScheduleSpec spec = ScheduleSpec.newBuilder()
.setCronExpression("0 2 * * *") // every day at 2 AM UTC
.build();

ScheduleAction action = ScheduleAction.newBuilder()
.setStartWorkflow(ScheduleAction.StartWorkflowAction.newBuilder()
.setWorkflowType("RunETL")
.setTaskList("etl-workers")
.setExecutionStartToCloseTimeout(Duration.ofHours(2))
.setRetryOptions(RetryOptions.newBuilder()
.setMaximumAttempts(3)
.build())
.build())
.build();

SchedulePolicies policies = SchedulePolicies.newBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.SKIP_NEW)
.build();

sc.createSchedule("daily-etl", spec, action, policies).join();

createSchedule is not idempotent. If the request succeeds on the server but you lose the response (e.g. a network timeout), call describeSchedule to check whether the schedule was actually created before retrying.

Input payload​

Pass serialized bytes as input to each triggered workflow. Use your configured DataConverter for custom types, or ObjectMapper for simple JSON:

byte[] input = new ObjectMapper().writeValueAsBytes(new MyWorkflowInput("2026-06-01"));

ScheduleAction.StartWorkflowAction.newBuilder()
.setWorkflowType("RunETL")
.setTaskList("etl-workers")
.setInput(input)
...

The same bytes are passed to every triggered workflow run.

Workflow ID prefix​

By default the scheduler generates workflow IDs using the schedule ID as a prefix. Override with setWorkflowIdPrefix:

ScheduleAction.StartWorkflowAction.newBuilder()
.setWorkflowType("RunETL")
.setTaskList("etl-workers")
.setWorkflowIdPrefix("etl-run-") // generates IDs like "etl-run-2026-06-01T02:00:00Z"
...

Overlap policies​

ConstantBehavior
ScheduleOverlapPolicy.SKIP_NEW (default)Skip the new fire if a previous run is still active.
ScheduleOverlapPolicy.BUFFERQueue new fires and run them sequentially; depth-limited by bufferLimit.
ScheduleOverlapPolicy.CONCURRENTStart every fire; use concurrencyLimit to cap simultaneous runs.
ScheduleOverlapPolicy.CANCEL_PREVIOUSCancel the active run gracefully, then start the new one.
ScheduleOverlapPolicy.TERMINATE_PREVIOUSTerminate the active run immediately, then start the new one.

Bounded concurrency​

SchedulePolicies policies = SchedulePolicies.newBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.CONCURRENT)
.setConcurrencyLimit(5) // at most 5 simultaneous runs; 0 = unlimited
.build();

Buffer depth​

SchedulePolicies policies = SchedulePolicies.newBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.BUFFER)
.setBufferLimit(10) // queue up to 10 pending fires; 0 = server default cap
.build();

Catch-up policy and window​

setCatchUpPolicy sets the default behavior when the schedule resumes from pause. setCatchUpWindow limits how far back the server looks for missed fires:

SchedulePolicies policies = SchedulePolicies.newBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.SKIP_NEW)
.setCatchUpPolicy(ScheduleCatchUpPolicy.ONE)
.setCatchUpWindow(Duration.ofHours(2)) // look back at most 2 hours for missed fires on resume
.build();
ConstantBehavior
ScheduleCatchUpPolicy.SKIP (default)Resume from now; all missed fires are dropped.
ScheduleCatchUpPolicy.ONEDispatch at most one missed fire, then resume from now.
ScheduleCatchUpPolicy.ALLDispatch all missed fires within the catch-up window.

Auto-pause on failure​

SchedulePolicies policies = SchedulePolicies.newBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.SKIP_NEW)
.setPauseOnFailure(true)
.build();

When pauseOnFailure is set, the schedule pauses automatically the first time a triggered workflow run fails. Unpause it with sc.unpauseSchedule(...) once the issue is resolved.

Jitter​

ScheduleSpec spec = ScheduleSpec.newBuilder()
.setCronExpression("0 0 * * *")
.setJitter(Duration.ofMinutes(10)) // random delay up to 10 minutes after midnight
.build();

Bounded schedule window​

Use setStartTime and setEndTime to restrict when the schedule is active:

ScheduleSpec spec = ScheduleSpec.newBuilder()
.setCronExpression("0 9 * * 1-5") // weekdays at 9 AM
.setStartTime(Instant.parse("2026-07-01T00:00:00Z"))
.setEndTime(Instant.parse("2026-12-31T00:00:00Z"))
.build();

The schedule fires only within the [startTime, endTime] window. null values mean no bound.

Timezone​

Embed the timezone directly in the cron expression using a CRON_TZ= prefix:

ScheduleSpec spec = ScheduleSpec.newBuilder()
.setCronExpression("CRON_TZ=America/New_York 0 9 * * 1-5") // 9 AM Eastern on weekdays
.build();

If no timezone prefix is provided, UTC is assumed.

Create in a paused state​

Pass a ScheduleInitialState to prevent the schedule from firing before a worker deployment or other dependency is ready:

sc.createSchedule(
"daily-etl",
spec,
action,
policies,
new ScheduleInitialState(true, "waiting for worker deployment", "release-pipeline")
).join();

Describing a schedule​

ScheduleDescription desc = sc.describeSchedule("daily-etl").join();

System.out.println("Paused: " + desc.getState().isPaused());
System.out.println("Pause reason: " + desc.getState().getPauseReason());
System.out.println("Paused at: " + desc.getState().getPausedAt());
System.out.println("Paused by: " + desc.getState().getPausedBy());
System.out.println("Next run: " + desc.getInfo().getNextRunTime());
System.out.println("Last run: " + desc.getInfo().getLastRunTime());
System.out.println("Total runs: " + desc.getInfo().getTotalRuns());
System.out.println("Created: " + desc.getInfo().getCreateTime());
System.out.println("Last updated: "+ desc.getInfo().getLastUpdateTime());
System.out.println("Missed runs: " + desc.getInfo().getMissedRuns());
System.out.println("Skipped runs: "+ desc.getInfo().getSkippedRuns());

getOngoingBackfills() returns a list of ScheduleInfo.BackfillInfo for any backfill operations currently in progress:

for (ScheduleInfo.BackfillInfo bf : desc.getInfo().getOngoingBackfills()) {
System.out.printf("backfill %s: %d/%d runs completed%n",
bf.getBackfillId(), bf.getRunsCompleted(), bf.getRunsTotal());
}

Pause and unpause​

// Pause with a reason
sc.pauseSchedule("daily-etl", "INFRA-4421: cluster maintenance").join();

// Unpause using the catch-up policy configured in the schedule's SchedulePolicies
sc.unpauseSchedule("daily-etl", "maintenance complete").join();

// Unpause and override the catch-up policy for this resume only
sc.unpauseSchedule("daily-etl", "maintenance complete", ScheduleCatchUpPolicy.ALL).join();

The ScheduleCatchUpPolicy passed to unpauseSchedule overrides the default set in SchedulePolicies for that single resume event.

Updating a schedule​

Pass a callback that receives the current ScheduleDescription and returns the desired one. The client fetches the current configuration, then sends only the spec, action, and policies fields your callback replaced, so you don't need to carry over fields you aren't changing:

sc.updateSchedule("daily-etl", current ->
current.toBuilder()
.setSpec(current.getSpec().toBuilder()
.setCronExpression("0 3 * * *") // move to 3 AM
.build())
.build()
).join();

This is a read-then-write operation, not an atomic transaction. Concurrent updates to the same schedule are last-write-wins, with no conflict detection.

Full replace​

The other updateSchedule overloads are a full replace: any field omitted from the request is cleared on the server. Call describeSchedule first to read the current configuration, then supply all fields you want to keep:

// Read current state
ScheduleDescription current = sc.describeSchedule("daily-etl").join();

// Build updated spec, reusing other fields from current
ScheduleSpec newSpec = current.getSpec().toBuilder()
.setCronExpression("0 3 * * *") // move to 3 AM
.build();

SchedulePolicies newPolicies = current.getPolicies().toBuilder()
.setOverlapPolicy(ScheduleOverlapPolicy.BUFFER)
.build();

sc.updateSchedule("daily-etl", newSpec, current.getAction(), newPolicies).join();

Changes apply to future fires only. In-flight workflow runs are not affected.

Backfill​

Java accepts a List<ScheduleBackfill>, allowing multiple time ranges in a single call:

import com.uber.cadence.client.ScheduleBackfill;
import java.time.Instant;
import java.util.List;

sc.backfillSchedule(
"daily-etl",
List.of(
new ScheduleBackfill(
Instant.parse("2026-06-20T00:00:00Z"),
Instant.parse("2026-06-23T00:00:00Z"),
ScheduleOverlapPolicy.CONCURRENT // optional: override for this range
)
)
).join();

The overlapPolicy argument is optional. Use new ScheduleBackfill(startTime, endTime) to inherit the schedule's configured overlap policy. Backfill fires are tagged with CadenceScheduleIsBackfill=true search attribute.

Listing schedules​

listSchedules returns ListSchedulesResponse (the Thrift-generated type) with explicit pagination. Both Java and Go require a manual token loop; Python is the only client that auto-paginates:

import com.uber.cadence.ListSchedulesResponse;
import com.uber.cadence.ScheduleListEntry;

byte[] token = null;
do {
ListSchedulesResponse result = sc.listSchedules(100, token).join();
for (ScheduleListEntry entry : result.getSchedules()) {
System.out.printf("%s: paused=%b cron=%s%n",
entry.getScheduleId(),
entry.getState().isPaused(),
entry.getCronExpression());
}
token = result.getNextPageToken();
} while (token != null && token.length > 0);

ScheduleListEntry returns data from the visibility store only. For full runtime info (policies, last run time, etc.), call describeSchedule on individual schedule IDs.

Deleting a schedule​

sc.deleteSchedule("daily-etl").join();

Deleting a schedule does not cancel or terminate any workflows it already started.

Schedule search attributes​

Cadence sets search attributes on two different workflow types. Use them to filter and query via the visibility API.

On each triggered workflow run​

AttributeTypeValue
CadenceScheduleIDKeywordThe schedule ID
CadenceScheduleTimeDatetimeThe nominal scheduled fire time (not actual start time)
CadenceScheduleIsBackfillBooltrue if started by a backfill request
CadenceScheduleBackfillIDKeywordThe backfill ID, if provided; absent on normal fires

CadenceScheduleTime is the time the schedule intended to fire, not when the workflow actually started. Use it to determine which time window a triggered run should process.

On the scheduler workflow itself​

These are set on the internal scheduler workflow (not on triggered runs) so that listSchedules can surface schedule state without querying each scheduler workflow individually.

AttributeTypeValue
CadenceScheduleStateKeyword"active" or "paused"
CadenceScheduleCronKeywordThe current cron expression
CadenceScheduleWorkflowTypeKeywordThe target workflow type name

Differences from Go and Python clients​

AspectJavaGoPython
Client accessworkflowClient.scheduleClient() → ScheduleClientcadenceClient.ScheduleClient()Methods directly on Client
API styleBuilder pattern; all methods return CompletableFutureStruct literals; blocking callsKeyword args; async/await
BackfillList<ScheduleBackfill>: multiple ranges per callSingle range per callSingle range per call
UpdateCallback overload (SDK fetches current state and sends only changed fields), or full replace where the caller must call describeSchedule firstCallback: SDK fetches current state, passes mutable view to callback, sends only changed fieldsSame callback pattern as Go
List paginationExplicit token loopExplicit token loopAuto-paginated async generator
TimezoneCRON_TZ= prefix embedded in cron stringCRON_TZ= prefix in cron stringSet in spec fields
ScheduleInfo fieldsongoingBackfills, missedRuns, skippedRunsSame, plus bufferedFireCount and runningWorkflowCountSame as Go (raw proto fields)