-
Notifications
You must be signed in to change notification settings - Fork 2
Cronus Schedule Syntax
Cron expression fields, extensions, and matching semantics.
- Fields
- Field syntax
- Worked examples
- Day-of-month vs day-of-week (POSIX OR)
- Matching semantics
- Examples
- API Reference
- Related Documentation
Standard 5-field cron, minute resolution — seconds and years are deliberately out of scope:
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───── day of week (0-7 or SUN-SAT; 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
| Form | Meaning | Example |
|---|---|---|
* |
every value |
* * * * * — every minute |
n |
exact value |
30 6 * * * — 06:30 daily |
a-b |
inclusive range |
0 9-17 * * * — hourly 9→17 |
a,b,c |
list (mixes with ranges/steps) |
0,30 * * * * — on :00 & :30 |
*/n |
every n from the field minimum |
*/15 * * * * — every 15 min |
a/n |
every n starting at a
|
5/10 * * * * — :05,:15,:25… |
a-b/n |
every n within the range |
10-20/5 * * * * — :10,:15,:20 |
| names | month/day names, case-insensitive | 0 0 * JAN mon-fri |
Malformed fields throw
InvalidScheduleError at parse time —
out-of-range values, zero steps (*/0), steps wider than their span
(*/60 in minutes), reversed (5-1) or half-open (-5, 5-)
ranges, and unknown names are all rejected loudly rather than
silently mis-read. Names must be complete tokens: JAN1 is an error,
never a silent misparse. Ranges do not wrap — use a list (SAT,SUN)
instead of SAT-SUN.
A step is rejected only when it exceeds a REAL span. A degenerate single-value span —
59/1,0-0/1— is a valid explicit single firing; any step on it is a no-op, not an error. Only a step wider than an actual multi-value span (*/60on 0-59,*/12on a 1-12 month field) throws.
Every field-syntax form from the table above, proven against the real parser:
import { parseSchedule } from '@tundralibs/cronus';
// '*' — every value in the field's range.
console.assert(parseSchedule('* * * * *').minute.size === 60);
// 'n' — exact value.
console.assert(parseSchedule('30 6 * * *').minute.has(30));
// 'a-b' — inclusive range.
console.assert(
[...parseSchedule('9-17 * * * *').hour].join(',') ===
'9,10,11,12,13,14,15,16,17',
);
// 'a,b,c' — list (mixes with ranges/steps in the same field).
console.assert(
[...parseSchedule('0,15,30,45 * * * *').minute].join(',') ===
'0,15,30,45',
);
// '*/n' — every n minutes, counting from the field's MINIMUM (0).
console.assert(
[...parseSchedule('*/15 * * * *').minute].join(',') === '0,15,30,45',
);
// 'a/n' — every n counting from a (NOT the field minimum): 5,15,25…55.
console.assert(
[...parseSchedule('5/10 * * * *').minute].join(',') ===
'5,15,25,35,45,55',
);
// 'a-b/n' — every n within an explicit range.
console.assert(
[...parseSchedule('10-20/5 * * * *').minute].join(',') === '10,15,20',
);
// Month/day NAMES — case-insensitive, resolved to their numeric value.
console.assert(
[...parseSchedule('0 0 * jan,dec *').month].join(',') === '1,12',
);
// Day-of-week accepts BOTH 0 and 7 for Sunday — 7 folds to 0.
console.assert([...parseSchedule('0 0 * * 7').dayOfWeek].join(',') === '0');When both day fields are restricted, a date matches if it
satisfies either — the standard cron rule. As in Vixie cron, a
field beginning with * (* or */n) counts as UNrestricted:
0 0 */2 * 1 means "every 2nd day AND Monday", while 0 0 13 * 5
means "the 13th OR Friday":
0 0 13 * 5 → midnight on the 13th OR any Friday
When only one is restricted, only that one applies (0 0 15 * * is
strictly the 15th).
The star-flag check reads the field STRING's first character, not whether
*appears anywhere in it.*,5and5,*parse to the IDENTICAL value set (every value in the field), but only the one starting with*counts as unrestricted — list order changes the matching rule, not just the values:import { parseSchedule } from '@tundralibs/cronus'; console.assert(parseSchedule('0 0 *,5 * 1').domRestricted === false); // '*' leads → unrestricted console.assert(parseSchedule('0 0 5,* * 1').domRestricted === true); // '*' trails → restrictedWith day-of-week fixed to Monday,
0 0 *,5 * 1fires every Monday (AND — day-of-month is unrestricted), while0 0 5,* * 1fires every day (OR — day-of-month is restricted, and its value set already covers every day, so the OR is always satisfied).
A field that is "restricted" (doesn't start with
*) can still cover every value — and the OR rule does not know the difference.0-6for day-of-week matches every day, exactly like*would, but because it doesn't start with*it still counts as restricted. Pair it with a restricted day-of-month under the POSIX OR rule and the day-of-week side is always satisfied, so the expression fires daily — not just on the day-of-month value it looks like it's pinned to:import { matches, parseSchedule } from '@tundralibs/cronus'; // Looks like "the 13th, or any day 0-6" — reads as "the 13th only" // at a glance, but 0-6 covers every weekday, so the OR fires daily. const dailyByAccident = parseSchedule('0 0 13 * 0-6'); console.assert(matches(dailyByAccident, new Date(2026, 7, 14))); // a Friday, NOT the 13th — still fires
-
Local time. Matching reads the host's local wall clock
(
getMinutes()/getHours()/…), with Vixie cron's DST semantics:-
Fall-back (an hour repeats): a fixed-time job — minute AND
hour both concrete, e.g.
30 1 * * *— fires once, not twice; wildcard jobs (* * * * *,*/5 …,30 * * * *) keep firing every physical minute through the repeated hour. - Spring-forward (an hour never occurs): jobs scheduled in the skipped hour do not run that day (no catch-up).
-
Fall-back (an hour repeats): a fixed-time job — minute AND
hour both concrete, e.g.
-
Minute boundary. The ticker fires at each
:00second boundary and evaluates every job against that minute; there is no sub-minute scheduling. -
No next-run computation. An expression that can never match
(e.g.
0 0 30 2 *) parses fine and simply never fires — validity is syntactic, fireability is not checked. -
Sunday. Day-of-week accepts
0and7; both fold to Sunday.
| Expression | Meaning |
|---|---|
* * * * * |
every minute |
*/5 * * * * |
every 5 minutes |
0 * * * * |
hourly, on the hour |
30 6 * * * |
daily at 06:30 |
0 3 * * MON |
Mondays at 03:00 |
0 9-17/2 * * 1-5 |
weekdays at 09,11,13,15,17:00 |
0 0 1 JAN,JUL * |
Jan 1 and Jul 1 at midnight |
0 0 13 * 5 |
the 13th OR any Friday (POSIX OR) |
Parse an expression into a ParsedSchedule (per-field value sets plus
restriction flags).
parseSchedule(expression: string): ParsedScheduleParameters:
-
expression- A 5-field cron expression.
Returns: The compiled ParsedSchedule.
Throws:
-
InvalidScheduleError- On the wrong field count or any malformed field.
Example:
import { parseSchedule } from '@tundralibs/cronus';
const schedule = parseSchedule('*/15 9-17 * * MON-FRI');Does a Date (local time, minute resolution) satisfy a parsed
schedule?
matches(schedule: ParsedSchedule, date: Date): booleanExample:
import { matches, parseSchedule } from '@tundralibs/cronus';
matches(parseSchedule('30 6 * * *'), new Date(2026, 0, 1, 6, 30)); // trueValidate without throwing — true means syntactically parseable, NOT
that the schedule will ever match a real date (0 0 30 2 * validates
true and simply never fires).
isValidSchedule(expression: string): booleanExample:
import { isValidSchedule } from '@tundralibs/cronus';
console.assert(isValidSchedule('*/15 9-17 * * MON-FRI') === true);
console.assert(isValidSchedule('*/60 * * * *') === false); // step wider than the span
console.assert(isValidSchedule('not a cron') === false);Cronus.isValid(schedule) and Cronus.matches(schedule, at?) wrap the
above for one-off checks without importing the engine functions —
useful for validating a schedule from user input without constructing
a Cronus instance:
import { Cronus } from '@tundralibs/cronus';
console.assert(Cronus.isValid('*/5 * * * *') === true);
console.assert(Cronus.isValid('nope') === false);
console.assert(Cronus.matches('30 6 * * *', new Date(2026, 0, 1, 6, 30)));- Cronus-Jobs - How schedules drive job runs
-
Cronus-Errors -
InvalidScheduleErrorcontext shape