Skip to content

API Reference

Constructors

Always build a schedule through one of these — never write out the underlying ROW(...) by hand. The composite has seven fields, five or six of which are usually NULL; a misplaced comma in a hand-written ROW() is a silent bug, whereas a constructor call fails loudly at the point you made the mistake, because every constructor returns the validating pgrelay.schedule domain.

Constructor Mode Meaning
pgrelay.on_dow(dow, times) 1 Every matching weekday
pgrelay.weekly(dow, times) 1 Alias for on_dow — reads better for "once a week"
pgrelay.daily(times) 1 Every day
pgrelay.weekdays(times) 1 Monday–Friday
pgrelay.weekends(times) 1 Saturday–Sunday
pgrelay.nth_dow(dow, times, VARIADIC nth) 2 The nth (or nth-from-last) weekday of the month
pgrelay.every_weeks(dow, weeks, times[, anchor]) 3 Every N weeks
pgrelay.fortnightly(dow, times[, anchor]) 3 Alias for every_weeks(dow, 2, times, anchor)
pgrelay.on_dom(times, VARIADIC dom) 4 Specific day(s) of the month
pgrelay.month_end(times) 4 Alias for on_dom(times, -1)
pgrelay.month_start(times) 4 Alias for on_dom(times, 1)
pgrelay.on_day_times(VARIADIC groups) 5 Independent per-day times — each argument is one 'dow=times' group

Why nth and dom take plain integers, not smallint

nth and dom are declared VARIADIC int, not VARIADIC smallint. This is intentional: PostgreSQL doesn't implicitly cast a plain integer literal to smallint, so a smallint[] variadic parameter would reject calls like pgrelay.on_dom('330', 3, 5) outright. The values are narrowed internally. To pass an existing array instead of listing values, use the VARIADIC keyword at the call site: pgrelay.on_dom('330', VARIADIC ARRAY[3, 5, 22]).

Examples

-- weekdays at 04:30 and 17:00
SELECT pgrelay.weekdays('4:30,1700');

-- every day at 02:00
SELECT pgrelay.daily('0200');

-- Tuesdays at 06:45
SELECT pgrelay.weekly('tue', '645');

-- 3rd, 5th, 7th, 11th, 22nd of the month at 03:30
SELECT pgrelay.on_dom('330', 3, 5, 7, 11, 22);

-- 2nd Tuesday of the month at 06:45
SELECT pgrelay.nth_dow('tue', '645', 2);

-- last Friday of the month at 17:00
SELECT pgrelay.nth_dow('fri', '1700', -1);

-- 1st and 3rd Monday, twice a day
SELECT pgrelay.nth_dow('mon', '05:30,1700', 1, 3);

-- every fortnight on Tuesday, aligned to the shared default epoch
SELECT pgrelay.fortnightly('tue', '645');

-- every fortnight on Tuesday, staggered onto the opposite week
SELECT pgrelay.fortnightly('tue', '645', DATE '2026-08-10');

-- every third week, Monday and Wednesday
SELECT pgrelay.every_weeks('mon,wed', 3, '0200', DATE '2026-08-03');

-- last day of every month at 23:30
SELECT pgrelay.month_end('2330');

-- Monday at 05:00; Tuesday at 17:30 and 20:00; Wednesday at 12:00 -- one
-- group per argument, or a single pre-joined 'dow=times;dow=times' string
SELECT pgrelay.on_day_times('mon=05:00', 'tue=17:30,20:00', 'wed=12:00');

Evaluation

These are what your application actually calls at runtime:

Function Returns Meaning
pgrelay.fires_on(s, date) boolean Does this schedule fire on this calendar date?
pgrelay.occurrences(s, from, to) setof timestamptz Every fire instant in [from, to] (both boundaries inclusive), in UTC, ascending, with no duplicates.
pgrelay.next_run(s [, after]) timestamptz The next fire instant strictly after after (default now()). Searches a 400-day window; returns NULL if nothing qualifies inside it (this only happens with an unrealistic schedule — the largest supported gap, week_interval = 52, is 364 days).
pgrelay.matches(s [, ts]) boolean Is ts (default now()) a fire instant, to the minute?
pgrelay.describe(s) text A human-readable rendering, e.g. '2nd Tuesday of the month at 06:45 UTC' — useful in logs and admin screens.

matches() needs a reliable per-minute tick to be safe

If whatever calls matches() misses a minute (a restart, a slow query, a paused container), that minute's scheduled run is silently skipped — there is no catch-up. For anything that actually matters, use next_run() stored in a watermark column instead, which self-heals after any outage. See the Watermark Pattern chapter.

-- does this schedule run on 29 Feb 2024?
SELECT pgrelay.fires_on(pgrelay.month_end('2330'), DATE '2024-02-29');
-- t (Feb 2024 is a leap year, so the last day is the 29th)

-- every fire instant in January 2026
SELECT * FROM pgrelay.occurrences(
    pgrelay.weekdays('4:30,1700'), '2026-01-01', '2026-01-31 23:59:59+00');

-- when does this next run, after right now?
SELECT pgrelay.next_run(pgrelay.nth_dow('mon', '0600', 1));

-- a friendly rendering for a dashboard
SELECT pgrelay.describe(pgrelay.fortnightly('tue', '645'));
-- 'every 2nd week on Tue (from week of 1970-01-05) at 06:45 UTC'

Parsers

Exposed directly in case you need them — most applications never call these themselves, since the constructors and evaluators already use them internally.

Function Returns Meaning
pgrelay.parse_dow(text) smallint[] Parses a dow string into sorted, deduplicated ISO weekday numbers.
pgrelay.parse_times(text) time[] Parses a times string into sorted, deduplicated times of day.
pgrelay.dow_token(text) smallint Parses a single day token ('tue', 'thursday', '4') into its ISO weekday number, or NULL if unrecognised.
pgrelay.ordinal(smallint) text Renders a number as an ordinal string (2'2nd', -1'last').
pgrelay.week_epoch() date The fixed default week-zero anchor (1970-01-05, a Monday) used when anchor is left NULL.
pgrelay.parse_day_times(text) setof (dow smallint, tm time) Parses a day_times string into its sorted, deduplicated (weekday, time) pairs.

Combining schedules: pgrelay.schedule[] overloads

Every evaluation function above also has a pgrelay.schedule[] overload — same name, taking an array of schedules instead of one. This is strictly more expressive than any single value: an array can mix modes (weekdays at 08:00 plus month-end at 23:30), which no scalar schedule — in any mode, including day_times — can ever express on its own.

pgrelay.next_run(s[] [, after])     -> timestamptz       -- earliest across every element
pgrelay.fires_on(s[], date)         -> boolean            -- does any element fire?
pgrelay.occurrences(s[], from, to)  -> setof timestamptz  -- union, ascending, de-duplicated
pgrelay.matches(s[] [, ts])         -> boolean            -- does any element match?
pgrelay.describe(s[])               -> text               -- every element's own describe(), joined "; "
-- when is either of these two jobs next due?
SELECT pgrelay.next_run(
    ARRAY[pgrelay.weekdays('0800'), pgrelay.month_end('2330')]);

A NULL array and an empty array both behave predictably — NULL for next_run(), false for the boolean functions, NULL for describe() — never an error, and a NULL element mixed into a real array is simply skipped.


Continue to the Watermark Pattern to see how these fit together in a real table.