Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cron Jobs

Cron jobs schedule registered tasks from cron-like definitions. They are useful for recurring work such as cleanup, reporting, reconciliation, and periodic synchronization.

Cron entries can be added with typed Rust builders or with crontab text:

  • WorkerOptions::with_cron(...) accepts one cron entry. It accepts typed builders, raw Crontab values, and crontab text.
  • WorkerOptions::with_crons(...) accepts multiple typed or raw Crontab values.
  • WorkerOptions::with_crontab(...) accepts crontab text, but is deprecated in favor of with_cron(...).

Typed cron builders

Use Cron when the schedule belongs to a Rust TaskHandler. The task identifier comes from T::IDENTIFIER, so it stays aligned with the job you registered with define_job.

use graphile_worker::{
    Cron, CrontabFill, IntoTaskHandlerResult, TaskHandler, WorkerContext,
    WorkerOptions,
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct SendDigest {
    account_id: i64,
}

impl TaskHandler for SendDigest {
    const IDENTIFIER: &'static str = "send_digest";

    async fn run(self, _ctx: WorkerContext) -> impl IntoTaskHandlerResult {
        // Send the digest.
    }
}

let worker = WorkerOptions::default()
    .define_job::<SendDigest>()
    .with_cron(
        Cron::daily_at::<SendDigest>(8, 0)?
            .id("send_digest_morning")
            .fill(CrontabFill::hours(1))
            .payload(SendDigest { account_id: 42 })?,
    )
    .pg_pool(pg_pool)
    .init()
    .await?;

Typed schedules are built with:

Cron::every_minute::<MyTask>()
Cron::every_n_minutes::<MyTask>(5)?
Cron::hourly_at::<MyTask>(15)?
Cron::daily_at::<MyTask>(8, 0)?
Cron::weekly_on::<MyTask>(chrono::Weekday::Mon, 8, 0)?
Cron::monthly_on::<MyTask>(1, 8, 0)?
Cron::yearly_on::<MyTask>(1, 1, 8, 0)?

The typed constructors validate field ranges. Minutes are 0..=59, hours are 0..=23, days of month are 1..=31, and months are 1..=12.

Builder options include:

Cron::every_n_minutes::<SendDigest>(10)?
    .id("send_digest_every_10_minutes")
    .fill(CrontabFill::minutes(30))
    .max_attempts(5)
    .queue("digest")
    .priority(-10)
    .job_key("send_digest_dedupe")
    .job_key_mode(graphile_worker::CronJobKeyMode::PreserveRunAt)
    .payload(SendDigest { account_id: 42 })?;

Use payload(...) for a typed payload that serializes as JSON. Use payload_value(...) when you already have a serde_json::Value.

Multiple cron entries

Use with_crons for a collection of typed builders or Crontab values:

let worker = WorkerOptions::default()
    .define_job::<SendDigest>()
    .define_job::<SyncMetrics>()
    .with_crons([
        Cron::daily_at::<SendDigest>(8, 0)?.build(),
        Cron::every_n_minutes::<SyncMetrics>(15)?.build(),
    ])
    .pg_pool(pg_pool)
    .init()
    .await?;

Crontab text

with_cron also accepts crontab text and returns a Result because the string is parsed at runtime.

let worker = WorkerOptions::default()
    .define_job::<SendDigest>()
    .with_cron(
        r#"
        Run at 08:00 UTC every day.
        0 8 * * * send_digest ?id=send_digest_morning&fill=1h {account_id:42}
        "#,
    )?
    .pg_pool(pg_pool)
    .init()
    .await?;

The text format is:

* * * * * task_identifier ?options {payload}

The five schedule fields are UTC minute, UTC hour, UTC day of month, UTC month, and UTC day of week. Days of week use 0..=6.

Supported schedule values are:

  • * for every valid value.
  • */n for every value divisible by n.
  • n for one explicit value.
  • a-b for an inclusive range.
  • Comma-separated combinations such as 0,15,30,45 or 4,10-15.

Examples:

# Every minute.
* * * * * tick

# Every 4 hours at minute 0.
0 */4 * * * rollup

# Mondays at 04:30 UTC.
30 4 * * 1 send_weekly_email

# More than one value in a field.
30 4,10-15 * * 1 send_weekly_email

Task identifiers must start with a letter or underscore. After that they may contain letters, numbers, colon, underscore, or hyphen.

Options

Crontab text options use query-string syntax and must start with ?.

0 8 * * * send_digest ?id=send_digest_morning&fill=1h&max=5&queue=digest&priority=10

Supported options are:

  • id: stable identifier for this cron entry. By default the task identifier is used. Set this when the same task has more than one schedule.
  • fill: backfill duration for missed executions.
  • max: overrides the job's maximum attempts.
  • queue: schedules the job in a named queue.
  • priority: overrides the job priority.
  • job_key: sets a job key for deduplication.
  • job_key_mode: controls an existing unlocked job with the same key. Supported serialized values are replace and preserve_run_at.

Changing a cron entry identifier can cause the worker to treat it as a new cron entry. Prefer setting a stable id when a task has multiple cron schedules.

Payloads

Cron jobs receive a JSON object payload. The worker adds a _cron object to the payload for each scheduled job:

{
  "_cron": {
    "ts": "2026-06-16T08:00:00",
    "backfilled": false
  }
}

ts is the timestamp when the cron execution was due, and backfilled is true when the job was scheduled as a backfill.

With typed builders, set the payload with payload(...) or payload_value(...). With crontab text, write a JSON5 object after the options:

0 8 * * * send_digest ?id=send_digest_morning {account_id:42, urgent:false}

The parsed payload is merged with the default cron payload properties. The text payload must start with { and stay on one line.

Backfill and fill

By default, cron entries do not backfill missed executions. Add fill to ask the worker to schedule missed runs from a recent time window when it starts or recovers a known cron entry.

Typed builder:

Cron::every_n_minutes::<SyncMetrics>(10)?
    .id("sync_metrics")
    .fill(CrontabFill::minutes(30))

Crontab text:

*/10 * * * * sync_metrics ?id=sync_metrics&fill=30m

Fill durations are made from time units:

  • s: seconds
  • m: minutes
  • h: hours
  • d: days
  • w: weeks

Units may be combined in order, for example 1w2d3h30m. In Rust, use CrontabFill::seconds, minutes, hours, days, weeks, or CrontabFill::new(w, d, h, m, s).

Backfill only applies to cron entries that were already known to the worker. When a cron entry is first registered, the worker records it as known but does not immediately create old jobs for it. Tests cover this behavior by asserting that a newly registered cron entry has no last_execution and creates no backfill jobs.

When a known cron entry has a previous execution timestamp, the worker can catch up missed matching schedule times inside the configured fill window. A four-hour cron with fill=1d, for example, can create the missed four-hour executions from the last day. Larger fill windows can schedule more jobs and take longer at startup.

Runtime behavior

The cron runner checks schedule times and inserts regular jobs for matching entries. If time advances by multiple matching intervals, the runner catches up by scheduling the missed ticks it observes. Jobs are then processed by the same registered task handlers as jobs added through the regular job APIs.