Skip to main content

Schedules

The Go client exposes schedule management through ScheduleClient, obtained from any cadence.Client 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 client.Client. For the full client setup (service transport, domain, yarpc dispatcher), see the Workers page.

// cadenceClient is a client.Client initialized for your domain
sc := cadenceClient.ScheduleClient()

Creating a schedule

import (
"encoding/json"
"go.uber.org/cadence"
"go.uber.org/cadence/client"
)

input, _ := json.Marshal(MyWorkflowInput{Date: "2026-06-01"})

scheduleID, err := sc.Create(ctx, &client.CreateScheduleRequest{
ScheduleID: "daily-etl",
Spec: &client.ScheduleSpec{
CronExpression: "0 2 * * *", // Every day at 2 AM UTC
},
Action: &client.ScheduleAction{
StartWorkflow: &client.ScheduleStartWorkflowAction{
WorkflowType: "RunETL",
TaskList: "etl-workers",
Input: input,
ExecutionStartToCloseTimeout: 2 * time.Hour,
RetryPolicy: &cadence.RetryPolicy{
MaximumAttempts: 3,
},
},
},
Policies: &client.SchedulePolicies{
OverlapPolicy: client.ScheduleOverlapPolicySkipNew,
},
})

Input is a pre-encoded byte slice. Encode it with json.Marshal for simple types, or use your configured DataConverter for custom types. The same bytes are passed to every triggered workflow run.

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

Overlap policies

ConstantBehavior
client.ScheduleOverlapPolicySkipNew (default)Skip the new fire if a previous run is still active.
client.ScheduleOverlapPolicyBufferQueue new fires and run them sequentially; depth-limited by BufferLimit.
client.ScheduleOverlapPolicyConcurrentStart every fire; use ConcurrencyLimit to cap simultaneous runs.
client.ScheduleOverlapPolicyCancelPreviousCancel the active run gracefully, then start the new one.
client.ScheduleOverlapPolicyTerminatePreviousTerminate the active run immediately, then start the new one.

Bounded concurrency

Policies: &client.SchedulePolicies{
OverlapPolicy: client.ScheduleOverlapPolicyConcurrent,
ConcurrencyLimit: 5, // at most 5 simultaneous runs; 0 = unlimited
},

Buffer depth

Policies: &client.SchedulePolicies{
OverlapPolicy: client.ScheduleOverlapPolicyBuffer,
BufferLimit: 10, // queue up to 10 pending fires; 0 = server default cap
},

Catch-up policy and window

CatchUpPolicy sets the default behavior when the schedule resumes from pause. CatchUpWindow limits how far back the server looks for missed fires.

Policies: &client.SchedulePolicies{
OverlapPolicy: client.ScheduleOverlapPolicySkipNew,
CatchUpPolicy: client.ScheduleCatchUpPolicyOne,
CatchUpWindow: 2 * time.Hour, // look back at most 2 hours for missed fires on resume
},
ConstantBehavior
client.ScheduleCatchUpPolicySkip (default)Resume from now; all missed fires are dropped.
client.ScheduleCatchUpPolicyOneDispatch at most one missed fire, then resume from now.
client.ScheduleCatchUpPolicyAllDispatch all missed fires within the catch-up window.

Auto-pause on failure

Policies: &client.SchedulePolicies{
OverlapPolicy: client.ScheduleOverlapPolicySkipNew,
PauseOnFailure: true,
},

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

Jitter

Spec: &client.ScheduleSpec{
CronExpression: "0 0 * * *",
Jitter: 10 * time.Minute, // random delay up to 10 minutes after midnight
},

Bounded schedule window

Use StartTime and EndTime to restrict when the schedule is active:

Spec: &client.ScheduleSpec{
CronExpression: "0 9 * * 1-5", // weekdays at 9 AM
StartTime: time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC),
},

The schedule fires only within the [StartTime, EndTime] window. Zero values mean no bound.

Describing a schedule

resp, err := sc.Describe(ctx, "daily-etl")
if err != nil {
return err
}

fmt.Printf("Paused: %v\n", resp.State.Paused)
fmt.Printf("Next run: %v\n", resp.Info.NextRunTime)
fmt.Printf("Last run: %v\n", resp.Info.LastRunTime)

Pause and unpause

// Pause with a reason
err = sc.Pause(ctx, "daily-etl", "INFRA-4421: cluster maintenance")

// Unpause - resume from now, skip missed fires
err = sc.Unpause(ctx, "daily-etl", "maintenance complete", client.ScheduleCatchUpPolicySkip)

// Unpause - catch up on all missed fires within the catch-up window
err = sc.Unpause(ctx, "daily-etl", "maintenance complete", client.ScheduleCatchUpPolicyAll)

The catch-up policy passed to Unpause overrides the default set in SchedulePolicies.CatchUpPolicy for that single resume event.

Updating a schedule

Update follows a read-modify-write pattern. The SDK fetches the current state, passes it to your callback as a *client.ScheduleUpdate, and sends only the fields you mutate:

err = sc.Update(ctx, "daily-etl", func(u *client.ScheduleUpdate) error {
// Change the cron expression
u.Spec.CronExpression = "0 3 * * *" // move to 3 AM

// Change the overlap policy
u.Policies.OverlapPolicy = client.ScheduleOverlapPolicyBuffer

// Change the workflow type
u.Action.StartWorkflow.WorkflowType = "RunETLV2"

return nil
})

ScheduleUpdate exposes Spec, Action, and Policies. Mutate only the fields you want to change; the rest are left untouched. Changes apply to future fires only. In-flight runs are not affected.

Backfill

err = sc.Backfill(ctx, "daily-etl", &client.BackfillRequest{
BackfillID: "backfill-june-gap",
StartTime: time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 6, 23, 0, 0, 0, 0, time.UTC),
OverlapPolicy: client.ScheduleOverlapPolicyConcurrent, // optional: override for this backfill only
})

OverlapPolicy is optional. If unset, the backfill uses the schedule's configured overlap policy. Backfill fires are tagged with CadenceScheduleIsBackfill=true and CadenceScheduleBackfillID search attributes.

Listing schedules

var nextPageToken []byte
for {
resp, err := sc.List(ctx, 100, nextPageToken)
if err != nil {
return err
}
for _, entry := range resp.Schedules {
fmt.Printf("%s: paused=%v cron=%s\n",
entry.ScheduleID, entry.State.Paused, entry.CronExpression)
}
if len(resp.NextPageToken) == 0 {
break
}
nextPageToken = resp.NextPageToken
}

Deleting a schedule

err = sc.Delete(ctx, "daily-etl")

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