npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cron-lab

v1.0.0

Published

Evaluate, schedule and explain cron expressions. No dependencies, TypesScript

Readme

Cron lab

A comprehensive TypeScript package for working with cron expressions — parse, build, validate, describe in 18 languages, compute occurrences with full timezone support, analyse schedules, and run a scheduling daemon.


Table of contents


Installation

# Install the package
npm install cron-lab

The package has no runtime dependencies. TypeScript 5+ and a Node.js 18+ runtime are required.


Quick start

import {
  parseCron,
  CronBuilder,
  isValid,
  isReachable,
  matchesDate,
  nextOccurrence,
  nextOccurrences,
  getOccurrencesInRange,
  normalizeCron,
  getFrequency,
  compareCron,
  diffCron,
  describeCron,
  createCronDaemon,
} from 'cron-lab';

// 1. Parse
const expr = parseCron('0 9 * * 1-5', { timezone: 'Europe/Paris' });

// 2. Build programmatically
const built = CronBuilder.create()
  .atMinute(0).atHour(9).onWeekdays()
  .inTimezone('Europe/Paris')
  .build();

// 3. Validate
isValid('0 9 * * 1-5');               // true
isValid('60 * * * *');                // false
isReachable(parseCron('0 0 31 2 *')); // { reachable: false, reason: 'impossible-month-day-combo' }

// 4. Test a date
matchesDate(expr, new Date());

// 5. Compute occurrences
nextOccurrence(expr, new Date());
nextOccurrences(expr, new Date(), 5);
getOccurrencesInRange(expr, new Date('2024-04-01'), new Date('2024-04-30'));

// 6. Describe in any language
describeCron(expr, 'fra'); // "À 09:00, du lundi au vendredi"
describeCron(expr, 'eng'); // "At 09:00, from Monday to Friday"
describeCron(expr, 'jpn'); // "09:00に, 月曜日から金曜日まで"

// 7. Analyse
getFrequency(expr);
compareCron(parseCron('0 9 * * *'), expr);
diffCron(parseCron('0 9 * * *'), expr, new Date('2024-04-20'), new Date('2024-04-28'));

// 8. Daemon
const daemon = createCronDaemon();
daemon.register('report', expr);
daemon.on('fire', (id) => console.log(`Running: ${id}`)); // Will print "Running: report" at the expected time
daemon.start();

Modules

Parser

parseCron(expression, options?)

Parses a 5-field cron expression or a @ alias into a structured CronExpression object.

function parseCron(expression: string, options?: ParseCronOptions): CronExpression

ParseCronOptions

| Property | Type | Description | |---|---|---| | timezone | string | IANA timezone identifier (e.g. "Europe/Paris"). Validated immediately — throws RangeError if unknown. |

Throws Error for invalid syntax (wrong field count, out-of-range values, malformed step/range).

Examples

parseCron('* * * * *');
parseCron('0 9 * * 1-5');
parseCron('*/15 8,20 * jan-mar mon,fri');
parseCron('@daily');
parseCron('@hourly');
parseCron('0 9 * * 1-5', { timezone: 'America/New_York' });

Supported @ aliases

| Alias | Equivalent | Meaning | |---|---|---| | @yearly / @annually | 0 0 1 1 * | Once a year, Jan 1 at midnight | | @monthly | 0 0 1 * * | Once a month, 1st at midnight | | @weekly | 0 0 * * 0 | Once a week, Sunday at midnight | | @daily / @midnight | 0 0 * * * | Once a day at midnight | | @hourly | 0 * * * * | Once an hour, at :00 |


Builder

CronBuilder provides a fluent, type-safe API for constructing cron expressions programmatically — no risk of syntax errors.

CronBuilder.create()

Creates a new builder initialised to * * * * *.

CronBuilder.from(expr)

Creates a builder pre-populated from an existing CronExpression. Useful for deriving modified copies.

Minute setters

| Method | Expression | Description | |---|---|---| | atMinute(0) | 0 … | Specific minute(s) | | atMinute(0, 15, 30, 45) | 0,15,30,45 … | Multiple minutes | | everyMinutes(15) | */15 … | Every N minutes | | minuteRange(0, 30) | 0-30 … | Minute range | | everyMinute() | * … | Reset to wildcard |

Hour setters

| Method | Expression | Description | |---|---|---| | atHour(9) | … 9 … | Specific hour(s) | | atHour(8, 20) | … 8,20 … | Multiple hours | | everyHours(6) | … */6 … | Every N hours | | hourRange(9, 17) | … 9-17 … | Hour range | | everyHour() | … * … | Reset to wildcard |

Day-of-month setters

| Method | Expression | Description | |---|---|---| | onDay(1) | … 1 … | Specific day(s) | | onDay(1, 15) | … 1,15 … | Multiple days | | everyDays(5) | … */5 … | Every N days | | dayRange(1, 15) | … 1-15 … | Day range | | everyDayOfMonth() | … * … | Reset to wildcard |

Month setters

| Method | Expression | Description | |---|---|---| | inMonth(1) | … 1 … | Specific month(s) | | inMonth(3, 6, 9, 12) | … 3,6,9,12 … | Multiple months | | everyMonths(3) | … */3 … | Every N months | | monthRange(3, 9) | … 3-9 … | Month range | | everyMonth() | … * … | Reset to wildcard |

Day-of-week setters

| Method | Expression | Description | |---|---|---| | onWeekday(1) | … 1 | Specific weekday (0=Sun…6=Sat) | | onWeekday(1, 3, 5) | … 1,3,5 | Multiple weekdays | | onWeekdays() | … 1-5 | Monday–Friday | | onWeekend() | … 0,6 | Saturday and Sunday | | weekdayRange(1, 5) | … 1-5 | Weekday range | | everyDayOfWeek() | … * | Reset to wildcard |

Timezone

builder.inTimezone('Europe/Paris')  // attach IANA timezone (throws if unknown)
builder.inLocalTime()                // remove timezone (use system local time)

Presets

CronBuilder.create().daily()    // 0 0 * * *
CronBuilder.create().hourly()   // 0 * * * *
CronBuilder.create().weekly()   // 0 0 * * 0
CronBuilder.create().monthly()  // 0 0 1 * *
CronBuilder.create().yearly()   // 0 0 1 1 *

Generic setter

builder.set('minute', 0)                     // value
builder.set('minute', [0, 15, 30, 45])      // list of values
builder.set('minute', { step: 15 })          // step from *
builder.set('minute', { step: 5, from: 0 })  // step from value
builder.set('dayOfWeek', { range: [1, 5] })  // range

Terminal methods

builder.build()         // → CronExpression
builder.toRawString()   // → "0 9 * * 1-5"
builder.toString()      // same as toRawString()

Full example

const expr = CronBuilder.create()
  .everyMinutes(15)
  .hourRange(9, 17)
  .onWeekdays()
  .inMonth(1, 2, 3)
  .inTimezone('America/New_York')
  .build();

// → "*/15 9-17 * 1,2,3 1-5" anchored to America/New_York

Validator

isValid(expression)

Returns true if the expression is syntactically valid. Never throws.

isValid('0 9 * * 1-5');   // true
isValid('@daily');         // true
isValid('60 * * * *');    // false — minute out of range
isValid('* * * *');        // false — only 4 fields

matchesDate(expr, date)

Returns true if date (to the minute, seconds ignored) satisfies the expression.

const expr = parseCron('0 9 * * 1-5');
matchesDate(expr, new Date('2024-04-22T09:00:30')); // true  — Mon 09:00
matchesDate(expr, new Date('2024-04-22T09:01:00')); // false — 09:01
matchesDate(expr, new Date('2024-04-20T09:00:00')); // false — Saturday

isReachable(expr)

Determines whether the expression can ever fire on a real calendar date, by checking for impossible day-of-month / month combinations.

function isReachable(expr: CronExpression): ReachabilityResult
isReachable(parseCron('0 0 31 2 *'));
// → { reachable: false, reason: 'impossible-month-day-combo',
//     message: 'Day 31 never occurs in February' }

isReachable(parseCron('0 0 29 2 *'));
// → { reachable: true }  — Feb 29 exists on leap years

isReachable(parseCron('0 0 31 4 *'));
// → { reachable: false }  — April has only 30 days

isReachable(parseCron('0 0 31 1,3 *'));
// → { reachable: true }  — January and March both have 31 days

ReachabilityResult

| Property | Type | Description | |---|---|---| | reachable | boolean | Whether a valid date exists | | reason | UnreachableReason? | Machine-readable reason code | | message | string? | Human-readable explanation |


Scheduler

All functions honour expr.timezone when present — see Timezone support.

nextOccurrence(expr, from?, searchWindowMs?)

Returns the first occurrence strictly after from (default to now), or null if none is found within the search window (default: 1 year).

nextOccurrence(parseCron('0 9 * * 1-5'), new Date('2024-04-19T10:00:00'));
// → Mon Apr 22 2024 09:00:00

// Narrow the window to 24 hours
nextOccurrence(parseCron('0 9 * * 1-5'), new Date('2024-04-22T10:00:00'), 24 * 60 * 60 * 1000);
// → Tue Apr 23 2024 09:00:00

previousOccurrence(expr, from?, searchWindowMs?)

Returns the last occurrence strictly before from (default to now), or null if none is found.

previousOccurrence(parseCron('0 9 * * 1-5'), new Date('2024-04-22T10:00:00'));
// → Mon Apr 22 2024 09:00:00

previousOccurrence(parseCron('0 9 * * 1-5'), new Date('2024-04-22T08:30:00'));
// → Fri Apr 19 2024 09:00:00

nextOccurrences(expr, from, count, searchWindowMs?)

Returns the next count occurrences after from in ascending order.

nextOccurrences(parseCron('0 9 * * 1-5'), new Date('2024-04-19T10:00:00'), 3);
// → [Mon Apr 22, Tue Apr 23, Wed Apr 24] all at 09:00

previousOccurrences(expr, from, count, searchWindowMs?)

Returns the last count occurrences before from in descending order.

previousOccurrences(parseCron('0 9 * * 1-5'), new Date('2024-04-22T10:00:00'), 3);
// → [Mon Apr 22, Fri Apr 19, Thu Apr 18] all at 09:00

getOccurrencesInRange(expr, start, end)

Returns all occurrences in the closed interval [start, end].

getOccurrencesInRange(
  parseCron('0 9 * * 1-5'),
  new Date('2024-04-22T00:00:00'),
  new Date('2024-04-26T23:59:59'),
);
// → [Mon Apr 22, Tue Apr 23, Wed Apr 24, Thu Apr 25, Fri Apr 26] at 09:00

Analyzer

normalizeCron(expr)

Returns the canonical 5-field string — @ aliases expanded, text aliases resolved, leading zeros stripped.

normalizeCron(parseCron('@daily'));           // "0 0 * * *"
normalizeCron(parseCron('00 09 * * 1-5'));   // "0 9 * * 1-5"
normalizeCron(parseCron('0 9 * jan mon'));   // "0 9 * 1 1"

getFrequency(expr)

Analyses the firing frequency of an expression over a 4-week sample window.

Note: this feature is still experimental, the results are not perfect and depends a lot on the analyze window size

function getFrequency(expr: CronExpression): FrequencyInfo

FrequencyInfo

| Property | Type | Description | |---|---|---| | minIntervalMinutes | number | Shortest gap between any two consecutive occurrences | | avgIntervalMinutes | number | Average gap over the 4-week window | | occurrencesPerDay | number | Average occurrences per day | | label | FrequencyLabel | "minutely" · "hourly" · "daily" · "weekly" · "monthly" · "yearly" · "rare" | | warnings | FrequencyWarning[] | Advisory warnings (see below) |

Warnings

| Code | Triggered when | |---|---| | impossible-date | dayOfMonth + month combo can never occur on a real date | | dow-dom-or-semantics | Both dayOfMonth and dayOfWeek restricted — OR semantics may surprise | | no-occurrences | No occurrence found in the 4-week sample window | | very-high-frequency | Minimum interval is less than 1 minute |

getFrequency(parseCron('*/15 * * * *'));
// → { minIntervalMinutes: 15, occurrencesPerDay: 96, label: 'minutely', warnings: [] }

getFrequency(parseCron('0 0 31 2 *'));
// → { ..., warnings: [{ code: 'impossible-date', message: '…' }] }

getFrequency(parseCron('0 0 1 * 1'));
// → { ..., warnings: [{ code: 'dow-dom-or-semantics', message: '…' }] }

compareCron(exprA, exprB)

Compares two expressions over a 4-week sample window.

function compareCron(exprA: CronExpression, exprB: CronExpression): CompareResult

CompareResult

| Property | Type | Description | |---|---|---| | equivalent | boolean | Both produce identical occurrences | | relation | string | "identical" · "a-subset-of-b" · "b-subset-of-a" · "disjoint" · "overlap" | | sharedCount | number | Occurrences present in both | | onlyInA | number | Occurrences only in A | | onlyInB | number | Occurrences only in B |

compareCron(parseCron('0 9 * * *'), parseCron('0 9 * * 1-5'));
// → { equivalent: false, relation: 'b-subset-of-a', onlyInA: 8, onlyInB: 0, sharedCount: 20 }

mergeCron(exprA, exprB)

Merges two expressions into a single one that fires on every occurrence of both. The result is a superset — it may fire on additional instants when a clean field-level merge isn't possible.

normalizeCron(mergeCron(parseCron('0 9 * * 1'), parseCron('0 9 * * 5')));
// → "0 9 * * 1,5"

normalizeCron(mergeCron(parseCron('0 8 * * *'), parseCron('0 17 * * *')));
// → "0 8,17 * * *"

getOverlaps(exprs)

Detects every minute within a 4-week window where two or more expressions fire simultaneously.

function getOverlaps(exprs: Map<string, CronExpression>): OverlapGroup[]
getOverlaps(new Map([
  ['backup',  parseCron('0 2 * * *')],
  ['report',  parseCron('0 2 * * 1')],   // overlaps with backup every Monday
  ['cleanup', parseCron('0 3 * * *')],
]));
// → [{ at: <Mon 02:00>, ids: ['backup', 'report'] }, …]

diffCron(exprA, exprB, start, end)

Lists every occurrence within [start, end] that belongs to one expression but not the other, sorted chronologically.

function diffCron(
  exprA: CronExpression,
  exprB: CronExpression,
  start: Date,
  end: Date,
): DiffEntry[]

DiffEntry

| Property | Type | Description | |---|---|---| | at | Date | The occurrence date | | source | "a" \| "b" | Which expression owns it |

diffCron(
  parseCron('0 9 * * *'),    // every day
  parseCron('0 9 * * 1-5'),  // weekdays only
  new Date('2024-04-20'),
  new Date('2024-04-28'),
);
// → [
//   { at: Sat Apr 20 09:00, source: 'a' },
//   { at: Sun Apr 21 09:00, source: 'a' },
//   { at: Sat Apr 27 09:00, source: 'a' },
//   { at: Sun Apr 28 09:00, source: 'a' },
// ]

Daemon

The daemon manages a registry of named cron expressions and emits a "fire" event at each occurrence. There is no callback passed to start — all job dispatch goes through on("fire", listener).

createCronDaemon()

function createCronDaemon(): CronDaemon

CronDaemon methods

| Method | Description | |---|---| | register(id, expr) | Add or replace an entry; next occurrence is computed immediately | | unregister(id) | Remove an entry; timer adjusts if it was the next scheduled one | | pause(id) | Suspend an entry — it stays in the registry but is skipped | | resume(id) | Unsuspend an entry; next occurrence is recomputed | | start() | Start the daemon (idempotent — second call while running is a no-op) | | stop() | Cancel the timer and clear history | | on(event, listener) | Subscribe to an event; returns an unsubscribe function | | history(n?, id?) | Last N fire records, newest first | | entries() | Read-only array of registered entries with nextAt |

Lifecycle

const daemon = createCronDaemon();

daemon.register('backup', parseCron('0 2 * * *'));
daemon.register('report', parseCron('0 9 * * 1-5', { timezone: 'Europe/Paris' }));

const offFire  = daemon.on('fire',  (id) => runJob(id));
const offError = daemon.on('error', (id, err) => console.error(id, err));

daemon.start();

// Dynamic management while running
daemon.pause('report');
daemon.resume('report');
daemon.unregister('backup');
daemon.register('new-job', parseCron('@hourly'));

// Inspect
daemon.entries();              // all registered entries with nextAt and paused status
daemon.history(10, 'report'); // last 10 fire records for "report"

// Shutdown — clears timer and history
daemon.stop();

// Detach listeners
offFire();
offError();

Events

| Event | When | detail argument | |---|---|---| | "fire" | At each scheduled occurrence | — | | "error" | When a "fire" listener throws | The caught error |

Each "fire" listener is called independently — a throwing listener emits "error" but does not prevent the remaining "fire" listeners from running on the same tick.

CronHistoryEntry

| Property | Type | Description | |---|---|---| | id | CronId | Entry identifier | | firedAt | Date | Timestamp of the fire event | | durationMs | number | Combined wall-clock time of all "fire" listeners |

History is capped at 1 000 entries and cleared when stop() is called.


Describer

Renders a CronExpression as a human-readable sentence in one of 18 languages.

describeCron(expr, locale)

function describeCron(expr: CronExpression, locale: string|CronLocalization): string

Throws if locale is not one of the supported ISO-639-1 or ISO-639-2 codes.

const expr = parseCron('0 9 * * 1-5');

describeCron(expr, 'fra'); // "À 09:00, du lundi au vendredi"
describeCron(expr, 'eng'); // "At 09:00, from Monday to Friday"
describeCron(expr, 'deu'); // "Um 09:00 Uhr, von Montag bis Freitag"
describeCron(expr, 'spa'); // "A las 09:00, de lunes a viernes"
describeCron(expr, 'jpn'); // "09:00に, 月曜日から金曜日まで"
describeCron(expr, 'ara'); // "الساعة 09:00, من الاثنين إلى الجمعة"

Supported locales

| Code | Language | Code | Language | |---|---|---|---| | afr, af | Afrikaans | jpn, jp | Japanese | | ara, ar | Arabic | nld, nl | Dutch | | ben, bn | Bengali | por, pt | Portuguese | | deu, de | German | rus, ru | Russian | | ell, el | Greek | spa, sp | Spanish | | eng, en | English | urd, ur | Urdu | | epo, eo | Esperanto | zho, zh | Chinese (Simplified) | | fra, fr | French | heb, he | Hebrew | | hin, hi | Hindi | ita, it | Italian |

Custom locales

Supply your own CronLocalization object to support any language:

import { describeCron, CronLocalization } from './index';

const MY_LOCALE: CronLocalization = { /* … */ };
describeCron(expr, MY_LOCALE);

The built-in locale instances (FR_LOCALE, EN_LOCALE, DE_LOCALE, …) and the full LOCALES registry are exported for reference or extension.


Timezone support

When a CronExpression carries a timezone property, all occurrence calculations interpret the five cron fields as wall-clock times in that timezone. The Date objects returned are always standard UTC-based values — only the interpretation of the fields changes.

// "Fire at 09:00 Paris time every weekday"
const expr = parseCron('0 9 * * 1-5', { timezone: 'Europe/Paris' });

// Winter (UTC+1): fires at 08:00 UTC
// Summer (UTC+2): fires at 07:00 UTC
const next = nextOccurrence(expr, new Date());

The timezone is respected by matchesDate, nextOccurrence, previousOccurrence, getOccurrencesInRange, getFrequency, compareCron, diffCron, and the daemon.

Setting a timezone via the builder:

CronBuilder.create()
  .atMinute(0).atHour(9).onWeekdays()
  .inTimezone('America/New_York')
  .build();

Valid values are any IANA identifiers recognised by the runtime's Intl implementation: "UTC", "Europe/Paris", "America/New_York", "Asia/Tokyo", etc.


Type reference

CronExpression

interface CronExpression {
  raw:        string;    // original expression string
  timezone?:  string|undefined;  // optional IANA timezone
  minute:     CronField; // 0–59
  hour:       CronField; // 0–23
  dayOfMonth: CronField; // 1–31
  month:      CronField; // 1–12
  dayOfWeek:  CronField; // 0–7 (0 and 7 = Sunday)
}

CronField

Discriminated union of five variants:

type CronField =
  | { type: 'all' }                                       // *
  | { type: 'value';  values: number[] }                  // 1,5,10
  | { type: 'range';  from: number; to: number }          // 1-5
  | { type: 'step';   base: '*' | number; step: number }  // */15 or 2/3
  | { type: 'list';   items: CronField[] }                // 1,3-5,*/2

FrequencyInfo

interface FrequencyInfo {
  minIntervalMinutes: number;
  avgIntervalMinutes: number;
  occurrencesPerDay:  number;
  label:    FrequencyLabel;
  warnings: FrequencyWarning[];
}

CompareResult

interface CompareResult {
  equivalent:  boolean;
  relation:    'identical' | 'a-subset-of-b' | 'b-subset-of-a' | 'disjoint' | 'overlap';
  sharedCount: number;
  onlyInA:     number;
  onlyInB:     number;
}

ReachabilityResult

interface ReachabilityResult {
  reachable: boolean;
  reason?:   'impossible-month-day-combo';
  message?:  string;
}

Cron syntax

┌─────────── minute       (0–59)
│ ┌───────── hour         (0–23)
│ │ ┌─────── day of month (1–31)
│ │ │ ┌───── month        (1–12)
│ │ │ │ ┌─── day of week  (0–7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *

| Syntax | Example | Meaning | |---|---|---| | Wildcard | * | Every value | | Single value | 5 | At 5 only | | List | 1,3,5 | At 1, 3, and 5 | | Range | 1-5 | From 1 to 5 inclusive | | Step on wildcard | */15 | Every 15 units | | Step on value | 2/3 | Every 3 units starting at 2 | | Step on range | 1-30/5 | Every 5 units from 1 to 30 | | Combination | 0,*/15 | At 0 and every 15 units |

Month names (case-insensitive): jan feb mar apr may jun jul aug sep oct nov dec

Day names (case-insensitive): sun mon tue wed thu fri sat

Day-of-month and day-of-week interaction

When both dayOfMonth and dayOfWeek are restricted (neither is *), standard cron applies a logical OR: the job fires if either condition is satisfied. When only one is restricted, it acts as the sole filter. getFrequency emits a dow-dom-or-semantics warning when it detects this situation.