Skip to content

Schedule Config

Top-level scheduler configuration added to AppConfig.

use horsies::{AppConfig, ScheduleConfig, TaskSchedule};
let config = AppConfig {
schedule: Some(ScheduleConfig::new(vec![/* ... */]).check_interval_seconds(1)),
..AppConfig::for_database_url("postgresql://...")
};
FieldTypeDefaultDescription
enabledbooltrueEnable/disable scheduler
check_interval_secondsu321Seconds between schedule checks (1-60)
schedulesVec<TaskSchedule>[]Schedule definitions

Defines a single scheduled task.

use horsies::{TaskSchedule, SchedulePattern, DailySchedule};
use chrono::NaiveTime;
TaskSchedule::new(
"daily-report",
"generate_report",
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)
.kwargs(serde_json::json!({"format": "pdf"}))
FieldTypeDefaultDescription
nameStringrequiredUnique schedule identifier
task_nameStringrequiredRegistered task name
patternSchedulePatternrequiredWhen to run
argsserde_json::ValueNullPositional arguments (JSON array)
kwargsserde_json::ValueNullKeyword arguments (JSON object)
queue_nameOption<String>NoneQueue override (CUSTOM mode)
enabledbooltrueEnable/disable this schedule
timezoneString"UTC"Timezone for schedule evaluation
catch_up_missedboolfalseExecute missed runs on restart
max_catch_up_runsu32100Maximum runs to enqueue per scheduler tick when catch_up_missed=true (range: 1-10000)

Must be unique across all schedules. Used for state tracking:

TaskSchedule::new(
"hourly-sync",
"sync_data",
SchedulePattern::Interval(IntervalSchedule {
hours: Some(1),
..Default::default()
}),
);
TaskSchedule::new(
"daily-cleanup",
"cleanup_old_data",
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(3, 0, 0).unwrap(),
}),
);

Must match a registered #[task]:

#[task("send_notification")]
async fn send_notification(input: NotifyInput) -> Result<(), TaskError> {
// ...
Ok(())
}
TaskSchedule::new(
"notify-users",
"send_notification", // Must match
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)

Pass arguments to the scheduled task as serialized JSON:

#[task("process_region")]
async fn process_region(input: RegionInput) -> Result<(), TaskError> {
// ...
Ok(())
}
TaskSchedule::new(
"sync-us-east",
"process_region",
SchedulePattern::Interval(IntervalSchedule {
hours: Some(1),
..Default::default()
}),
)
.kwargs(serde_json::json!({
"region": "us-east",
"full_sync": true,
}))

In CUSTOM mode, override the task’s default queue:

#[task("background_job", queue = "normal")]
async fn background_job() -> Result<(), TaskError> {
// ...
Ok(())
}
TaskSchedule::new(
"priority-job",
"background_job",
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)
.queue("critical") // Override to higher priority queue

Schedule evaluated in specified timezone:

// Runs at 9 AM New York time (EST/EDT)
TaskSchedule::new(
"morning-task",
"my_task",
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)
.timezone("America/New_York")

Uses IANA timezone names via chrono-tz. Common values:

  • "UTC" - Coordinated Universal Time
  • "America/New_York" - US Eastern
  • "America/Los_Angeles" - US Pacific
  • "Europe/London" - UK
  • "Asia/Tokyo" - Japan

When catch_up_missed=true, missed runs are executed:

TaskSchedule::new(
"hourly-sync",
"sync_data",
SchedulePattern::Interval(IntervalSchedule {
hours: Some(1),
..Default::default()
}),
)
.catch_up_missed(true)

If scheduler was down 3 hours, 3 tasks are enqueued on restart.

Use for:

  • Data synchronization (must process all intervals)
  • Compliance reporting (all periods must be covered)

Avoid for:

  • Notifications (users do not want 24 emails at once)
  • Status updates (only latest matters)

Disable individual schedules:

TaskSchedule::new(
"deprecated-job",
"old_task",
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)
.enabled(false) // Will not run

Disable entire scheduler:

ScheduleConfig::new(vec![])
.enabled(false) // No schedules run
.check_interval_seconds(30)

When schedule configuration changes:

  1. Scheduler detects via config_hash
  2. Recalculates next_run_at from current time
  3. Logs warning about configuration change

This prevents issues when:

  • Pattern changes (e.g., hourly to daily)
  • Timezone changes
  • Time changes within pattern

At scheduler startup, validates:

  1. Task exists: task_name must be registered
  2. Queue valid: queue_name must match configured queue (CUSTOM mode)
  3. Timezone valid: Must be a recognized IANA timezone name
  4. Schedule names unique: No duplicate name values
// Will fail at startup:
TaskSchedule::new(
"bad",
"nonexistent_task", // Not registered
SchedulePattern::Daily(DailySchedule {
time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
}),
)