Specify dates and times using crontab syntax

JobQueue uses a 6-field crontab-like format with an initial seconds field. The ZendHQ cron scheduler uses UTC time. It does not support cron expressions written in your local time zone.

Time format

The cron expression is a list of six space-separated fields representing seconds, minutes, hours, days of the month, months, and days of the week:

<seconds> <minutes> <hours> <day-of-month> <month> <day-of-week>

Field

Valid values

Valid special characters

seconds

0-59

*, , - /

minutes

0-59

*, , - /

hours

0-23

*, , - /

day-of-month

1-31

*, , - /

month

1-12 or JAN-DEC

not case-sensitive

*, , - /

day-of-week

0-7 or SUN-SAT or MON-SUN

not case-sensitive

both 0 and 7 mean Sunday

*, , - /

With ZendHQ cron expressions, you can restrict both the day-of-month and the day-of-week fields. If both are restricted (that is, they do not contain the * character), then, contrary to the traditional cron implementation, both criteria must match the current day for the job to run.

Example:

  • Run at noon every 13th day of the month that is also a Friday:

    0 0 12 13 * FRI

Ranges and periods

To define ranges, use the following special characters:

  • Asterisk

    Asterisks (*) expand to a full list of all valid values of the specific field. For example, an asterisk * in the month field means "all months from January to December, inclusively".

  • Comma

    Use commas (,) to separate items of a list. For example, MON,WED,FRI in the day-of-week field means "Mondays, Wednesdays, and Fridays".

  • Dash

    Use dashes (-) to define a range of values. For example, 2-4 in the hours field means the hours between 2 and 4, inclusively, that is, "2, 3, and 4".

    ZendHQ JobQueue cron expressions also support overflowing ranges like FRI-MON, which is expanded to FRI,SAT,SUN,MON.
  • Slash

    Use slashes (/) to specify incrementer values. Incrementers can be used with ranges and single values. If used with a single value, the value is changed to a range with a starting value and incrementer.

    Examples:

    • Run every 15 seconds:

      */15 * * * * *
    • Run daily every full hour between 9 AM and 3 PM (between 9:00 and 15:00), and then again between 4 PM and 8 AM (between 16:00 and 8:00) every second full hour:

      0 0 9-15,16-8/2 * * *

Time zone

Every recurring job is evaluated in a time zone, given as an IANA time zone identifier like America/New_York. The cron expression is matched against the wall-clock time of that time zone, so a job keeps running at the same local time all year round. The resulting execution time is always stored and reported in UTC.

The accepted identifiers are the ones provided by the time zone database of the server the ZendHQ daemon runs on, so an identifier that is accepted on one server can be rejected on another.

IANA identifiers are matched exactly: Europe/Tallinn is accepted where europe/tallinn is not. They are stored exactly as given and reported back unchanged, and time zone aliases such as US/Eastern are accepted where the time zone database of the server provides them, without being rewritten to the zone they link to.

The UTC value is the one exception. It is not an IANA identifier but the Job Queue's own default value, so it is matched case-insensitively and stored in its canonical spelling: UTC.

Precedence

A recurring job takes its time zone from the first of the following that is set:

  1. the time zone of the job itself;

  2. the default job time zone of the queue the job belongs to;

  3. the zendhqd.jobqueue.timezone configuration directive;

  4. UTC.

The value is resolved when the queue or the job is created or updated, like every other Job Queue default: the configuration directive is applied when a queue is created, and the default of a queue is copied onto the recurring jobs created in it. Changing the directive or the default of a queue afterwards does not affect the queues and the jobs that already exist.

Every queue and every recurring job carries a time zone, so there is no "no time zone" state. One-shot jobs have no cron expression to evaluate and never carry one.

Daylight saving time

Time zones with daylight saving time have two days a year on which a local wall-clock time either does not exist or occurs twice. Recurring jobs follow the local wall clock, and on those two days the two are reconciled as follows:

  1. When the clocks are moved forward, a job scheduled for a local time that does not exist runs at the next valid local time instead (02:30 becomes 03:30). The local times of the day have a gap, while the interval between the actual runs stays the same.

  2. When the clocks are moved back, a job scheduled for a local time that occurs twice runs on the first of the two occurrences. The local times of the day are regular, while the actual runs skip the repeated hour.

For example, a job scheduled every 30 minutes still runs every 30 minutes through the spring transition, but has a 90-minute gap in the autumn one. Use the UTC time zone for jobs that must not be affected at all.

Note that fixed-interval jobs are not exempt from the policy: a job running every 30 minutes loses or repeats runs exactly like a daily one does.

Daylight saving time examples

The examples below use the America/New_York time zone, which moves the clock forward on 2026-03-08 and back on 2026-11-01.

A daily job 0 0 8 * * * keeps its local time across both transitions, while its UTC time moves by an hour:

Local time UTC Notes
2026-03-07 08:00:00 2026-03-07 13:00:00Z
2026-03-08 08:00:00 2026-03-08 12:00:00Z clocks are moved forward
2026-10-31 08:00:00 2026-10-31 12:00:00Z
2026-11-01 08:00:00 2026-11-01 13:00:00Z clocks are moved back

A job 0 30 2 * * * scheduled for a local time that does not exist on the spring transition day runs at the next valid local time, 02:30 becoming 03:30:

Local time UTC Notes
2026-03-07 02:30:00 2026-03-07 07:30:00Z
2026-03-08 03:30:00 2026-03-08 07:30:00Z 02:30 does not exist
2026-03-09 02:30:00 2026-03-09 06:30:00Z

A job 0 30 1 * * * scheduled for a local time that occurs twice on the autumn transition day runs on the first of the two occurrences, 01:30 EDT and not 01:30 EST:

Local time UTC Notes
2026-10-31 01:30:00 2026-10-31 05:30:00Z
2026-11-01 01:30:00 2026-11-01 05:30:00Z first of the two occurrences
2026-11-02 01:30:00 2026-11-02 06:30:00Z

A fixed-interval job 0 */30 0-6 * * * shows the asymmetry between the two transitions. On the spring transition day the local times have a gap, 01:30 being followed by 03:00, while the actual runs keep the steady 30-minute interval:

Local time UTC Notes
2026-03-08 01:00:00 2026-03-08 06:00:00Z
2026-03-08 01:30:00 2026-03-08 06:30:00Z
2026-03-08 03:00:00 2026-03-08 07:00:00Z 02:00 and 02:30 do not exist
2026-03-08 03:30:00 2026-03-08 07:30:00Z
2026-03-08 04:00:00 2026-03-08 08:00:00Z

On the autumn transition day the local times are regular, while the actual runs have a 90-minute gap between 05:30Z and 07:00Z, because the repeated hour is skipped:

Local time UTC Notes
2026-11-01 00:30:00 2026-11-01 04:30:00Z
2026-11-01 01:00:00 2026-11-01 05:00:00Z
2026-11-01 01:30:00 2026-11-01 05:30:00Z
2026-11-01 02:00:00 2026-11-01 07:00:00Z the repeated hour is skipped
2026-11-01 02:30:00 2026-11-01 07:30:00Z
  • Time zone support is opt-in. Without a time zone, behavior is unchanged and uses UTC.

  • Existing recurring jobs are not automatically converted. To make a job DST-aware, set its time zone manually by using a PHP script or zendhqctl update.

  • During spring-forward, jobs scheduled for nonexistent local times run at the next valid local time. During fall-back, jobs scheduled for duplicated local times run once, at the first occurrence.

  • Jobs that must run at fixed UTC intervals, such as every hour, should generally not set a time zone.