Scheduling Workflows
You can schedule DBOS workflows to run on a cron schedule. Schedules are stored in the database and can be created, paused, resumed, and deleted at runtime. Each time a schedule fires, its workflow is executed by exactly one worker process.
To schedule a workflow, first define a workflow whose input is a ScheduledWorkflowInput.
This struct carries the cron tick time (ScheduledTime) and a user-defined Context value attached to the schedule, which you can decode with DecodeScheduleContext:
func myPeriodicTask(ctx dbos.Context, input dbos.ScheduledWorkflowInput) (any, error) {
scheduleCtx, err := dbos.DecodeScheduleContext[string](input)
if err != nil {
return nil, err
}
logger.Info("running scheduled task",
"scheduled_time", input.ScheduledTime,
"context", scheduleCtx)
return nil, nil
}
dbos.RegisterWorkflow(dbosContext, myPeriodicTask)
Then, create a schedule for it using CreateSchedule with a ScheduleSpec containing a crontab expression:
err := dbos.CreateSchedule(dbosContext, dbos.ScheduleSpec{
ScheduleName: "my-task-schedule", // The schedule name is a unique identifier of the schedule
Workflow: myPeriodicTask, // A registered workflow function
Schedule: "0 */5 * * * *", // Every 5 minutes
Context: "my context", // Passed into every iteration of the workflow
})
Note that CreateSchedule will fail if the schedule already exists.
If you're defining a set of static schedules to be created on program start, you can instead use ApplySchedules to create them atomically, updating them if they already exist:
err := dbos.ApplySchedules(dbosContext, []dbos.ScheduleSpec{
{
ScheduleName: "schedule-a",
Workflow: workflowA,
Schedule: "0 */10 * * * *", // Every 10 minutes
Context: "context-a",
},
{
ScheduleName: "schedule-b",
Workflow: workflowB,
Schedule: "0 0 0 * * *", // Every day at midnight
Context: "context-b",
},
})
When ApplySchedules updates an existing schedule, it replaces the entire definition with the new entry, so any optional field left unset is cleared.
For example, if a schedule was routed to a named queue and you re-apply it without setting QueueName, it reverts to the internal queue.
The schedule's status and last-fired time are preserved.
To learn more about crontab syntax, see this guide or this crontab editor. DBOS Go uses robfig/cron to parse cron schedules, with seconds as the first field. Valid cron schedules contain exactly 6 items, separated by spaces:
┌────────────── second
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌──────── day of month
│ │ │ │ ┌────── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
Cron expressions are evaluated in UTC by default. Set the CronTimezone field of ScheduleSpec to an IANA timezone name (e.g. "America/New_York") to evaluate the expression in a different timezone.
You can dynamically create many schedules for the same workflow. For example, if you want to perform certain actions periodically for each of your customers, you can create one schedule per customer, using customer ID as context so each workflow knows which customer to act on:
func customerWorkflow(ctx dbos.Context, input dbos.ScheduledWorkflowInput) (any, error) {
customerID, err := dbos.DecodeScheduleContext[string](input)
if err != nil {
return nil, err
}
// ...
return nil, nil
}
dbos.RegisterWorkflow(dbosContext, customerWorkflow)
func onCustomerRegistration(ctx dbos.Context, customerID string) error {
return dbos.CreateSchedule(ctx, dbos.ScheduleSpec{
ScheduleName: fmt.Sprintf("customer-%s-sync", customerID),
Workflow: customerWorkflow,
Schedule: "0 0 * * * *", // Every hour
Context: customerID,
})
}
The Context field on ScheduleSpec is typed as any and is serialized as JSON when the schedule is stored.
Inside the workflow, recover the original value with DecodeScheduleContext.
Managing Schedules
You can pause, resume, and delete schedules at runtime:
// Pause a schedule so it stops firing
err := dbos.PauseSchedule(dbosContext, "my-task-schedule")
// Resume a paused schedule
err = dbos.ResumeSchedule(dbosContext, "my-task-schedule")
// Delete a schedule
err = dbos.DeleteSchedule(dbosContext, "my-task-schedule")
You can also list and inspect schedules:
// List all active schedules
schedules, err := dbos.ListSchedules(dbosContext,
dbos.WithScheduleStatuses(dbos.ScheduleStatusActive))
// Get a specific schedule by name
schedule, err := dbos.GetSchedule(dbosContext, "my-task-schedule")
Backfilling and Triggering
If a schedule was paused or your application was offline, you can backfill missed executions using BackfillSchedule.
Already-executed times are automatically skipped:
start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)
ids, err := dbos.BackfillSchedule(dbosContext, "my-task-schedule", start, end)
Alternatively, set AutomaticBackfill: true on the ScheduleSpec when creating a schedule so that missed executions are automatically backfilled whenever your application starts or a paused schedule is resumed.
Backfills (manual or automatic) compute missed executions using the schedule's current cron expression. If you update a schedule's cron expression and then backfill, the backfill generates one execution per tick of the new expression over the requested window—including times the old expression would never have matched. For example, changing a daily schedule to an hourly one and then backfilling yesterday enqueues 24 executions, not 1.
You can also immediately trigger a schedule using TriggerSchedule:
handle, err := dbos.TriggerSchedule[any](dbosContext, "my-task-schedule")
Scheduling to Queues
By default, scheduled workflows are enqueued on an internal queue.
You can instead enqueue them on a declared queue to manage their concurrency or rate limits.
Set the QueueName field of ScheduleSpec when creating the schedule:
dbos.RegisterQueue(dbosContext, "scheduled_queue",
dbos.WithGlobalConcurrency(1))
err := dbos.CreateSchedule(dbosContext, dbos.ScheduleSpec{
ScheduleName: "my-task-schedule",
Workflow: myPeriodicTask,
Schedule: "0 */5 * * * *",
QueueName: "scheduled_queue",
})
This ensures that scheduled workflow executions respect the queue's flow control settings.
Managing Schedules from Another Application
You can manage schedules from outside your DBOS application using a standalone client.
Because workflows are not registered with a client, set the WorkflowName field (a string) instead of the Workflow function reference:
client, err := dbos.NewClient(context.Background(), dbos.ClientConfig{
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
})
err = dbos.CreateSchedule(client, dbos.ScheduleSpec{
ScheduleName: "my-task-schedule",
WorkflowName: "myPeriodicTask",
Schedule: "0 */5 * * * *",
Context: "my context",
})
How Scheduling Works
Under the hood, DBOS constructs an idempotency key for each scheduled workflow execution.
The key is the concatenation of sched-, the schedule name, and the scheduled time (RFC3339), ensuring each scheduled invocation occurs exactly once even when multiple application instances share the same schedule.
When a schedule fires, its workflow is enqueued by name rather than invoked directly, so the process hosting the schedule does not need to have the workflow registered. Name resolution happens at dequeue time on a worker that has the function, letting any process connected to the system database drive schedules for workflows owned by other processes or languages. Scheduled workflows always run against the latest registered application version, so a stale executor does not pick them up after a new deploy.
You can list the workflows started by a schedule by passing WithFilterScheduleName to ListWorkflows.
For the full API reference, see Workflow Schedules.