Schedule Syntax¶
The second argument to pgrelay.schedule_job() — the spec — accepts four
forms. pg_relay tells them apart by shape, so there's no format flag to set,
and every form is validated the moment you register it: a typo raises an error
at the schedule_job() call, never silently at dispatch time.
| Form | Example | Reads as |
|---|---|---|
| Traditional cron (5 fields) | '*/5 * * * *' |
every 5 minutes |
| Seconds cron (6 fields) | '20 * * * * *' |
every minute, at :20 |
| Interval | '30 seconds' |
30 seconds after each launch |
pgrelay.schedule value |
pgrelay.daily('04:30')::text |
calendar rules cron can't express |
Traditional cron — 5 fields¶
The same five fields pg_cron uses, in the same order:
Each field accepts:
| Syntax | Meaning | Example |
|---|---|---|
* |
every value | * * * * * — every minute |
5 |
exactly 5 | 5 4 * * * — 04:05 daily |
1,15 |
a list | 0 0 1,15 * * — the 1st and 15th |
8-18 |
a range | 0 8-18 * * * — hourly, 8am–6pm |
*/10 |
every 10th value | */10 * * * * — every 10 minutes |
8-18/2 |
a stepped range | 0 8-18/2 * * * — 8, 10, 12, 14, 16, 18 |
Values are numeric only (no jan or mon names). Worked examples:
'0 3 * * *' -- daily at 03:00
'*/15 * * * *' -- every quarter hour
'0 9 * * 1-5' -- weekdays at 09:00
'30 22 * * 0' -- Sundays at 22:30 (0 or 7 both mean Sunday)
'0 6 1 * *' -- the 1st of every month at 06:00
'5,35 8-18/2 * * *'-- :05 and :35 past every second hour, 8am–6pm
The day-of-month / day-of-week rule
Standard cron behaviour applies: when both the day-of-month and
day-of-week fields are restricted (neither is *), a day matches if
either matches. '0 0 13 * 5' fires on the 13th of the month and
on every Friday — not only on Friday-the-13th. When only one is
restricted, only that one has to match.
Seconds cron — 6 fields, beyond pg_cron¶
Give the spec six fields and the first one means seconds (0–59), with the remaining five exactly as above. This is scheduling pg_cron cannot do — the Processor polls every second, so second-level specs fire on time:
'*/10 * * * * *' -- every 10 seconds
'20 * * * * *' -- every minute, at :20 past
'0 */5 * * * *' -- every 5 minutes, on the minute (same as '*/5 * * * *')
'30 0 3 * * *' -- daily at 03:00:30
A 5-field spec is simply a 6-field spec with seconds pinned to 0 — daily at
03:00 means 03:00:00 exactly.
How precise is 'on time'?
The Processor polls once per second, so a job fires within roughly a second of its occurrence — plus whatever your queue depth adds. For calendar reporting that's exact; treat it as "to the second," not "to the millisecond."
Interval — fixed cadence¶
Anything PostgreSQL accepts as a positive interval works:
An interval is a cadence, not a calendar: the next run is computed from
each launch, so a '90 minutes' job launched at 03:00 next runs at 04:30,
then 06:00 — drifting across hour and day boundaries in a way cron never
does. That's exactly right for "keep this fresh every N" work; use cron when
you mean "at these clock times."
pgrelay.schedule values — complex calendar rules¶
For rules cron can't express — the 2nd Tuesday, the last day of the
month, different times on different days — the spec can be a value of the
pgrelay.schedule companion type, passed
as text (a plain ::text cast). Build it with the companion's constructors:
-- The 2nd Tuesday of every month at 09:00 UTC:
SELECT pgrelay.schedule_job('board_pack', pgrelay.nth_dow('tue', '09:00', 2)::text,
'CALL build_board_pack()');
-- The last day of every month at 23:00 UTC:
SELECT pgrelay.schedule_job('month_close', pgrelay.month_end('23:00')::text,
'CALL close_month()');
-- Weekdays at 04:30 and 17:00 UTC, firing one of your own channels:
SELECT pgrelay.schedule_job('twice_daily', pgrelay.weekdays('04:30,17:00')::text,
'sales', p_channel := 'refresh_chan');
-- Different times per day:
SELECT pgrelay.schedule_job('mixed_week',
pgrelay.on_day_times('mon=05:00', 'tue=17:30,20:00', 'wed=12:00')::text,
'REFRESH MATERIALIZED VIEW CONCURRENTLY ops.dash_mv');
Two things to know:
- The companion must be installed (
schedule/install.sql— see its Installation chapter). It's optional: the other three spec forms never need it, and registering apgrelay.schedulespec without it raises a clear error telling you so. - Companion values evaluate in UTC — that's the type's own documented behaviour. Cron specs, by contrast, evaluate in the database server's timezone (below).
Timezones¶
Cron and interval specs are evaluated by the database, in the timezone of
the session that computes the occurrence — in steady state that is the
Processor's connection, which uses the server's default TimeZone unless you
set PGTZ in the Processor's environment. In practice: cron clock times mean
server time. pgrelay.schedule values are always UTC.
Skipped occurrences, never replayed¶
Every form shares one rule: the next occurrence is always computed strictly after now. If the Processor was down (or the job paused) across several occurrences, the job fires once on catch-up and then resumes its rhythm — an hourly job down for three hours does not fire three times in a row. pg_cron behaves the same way, so migrated jobs keep their semantics.
Previewing and validating a spec¶
pgrelay._next_run(spec [, after]) is the calculator the scheduler itself
uses, and it's granted to management roles precisely so you can dry-run a spec
before trusting a job to it:
SELECT pgrelay._next_run('0 3 * * *'); -- next 03:00 from now
SELECT pgrelay._next_run('*/10 * * * * *'); -- a few seconds away
SELECT pgrelay._next_run('0 9 * * 1', '2026-12-25 00:00'); -- the Monday after Christmas
SELECT pgrelay._next_run('0 0 30 2 *'); -- ERROR: never matches a real date
An invalid spec raises the same error schedule_job() would, naming the
offending field — so a preview that succeeds is a spec that will register.
Continue to Creating and Changing Jobs.