Skip to content

Building events (the builder)

The builder is the readable way to declare events — it reads like a sentence and compiles to a plain config object (the form calendaryjs stores and exports). Import it from the calendaryjs/builder subpath:

import {
every,
from,
once,
weekly,
monthly,
daily,
yearly,
date,
nth,
} from "calendaryjs/builder";
every("year").on(date(12, 25)).title("Christmas"); // Dec 25, every year
every(2, "weeks").on("tuesday").title("Standup"); // every other Tuesday
every("month").on(nth(1, "monday")).title("Retro"); // 1st Monday of every month
every("month").on(-1).title("Rent"); // last day of every month
every(3, "days").title("Water plants"); // every 3 days
from("easter").plus({ days: 49 }).title("Pentecost"); // 49 days after a registered anchor
once("2025-06-15").title("Wedding"); // a single date

Every declaration reads in one fixed order — like SQL clauses, never improvised:

<opener> . on(<position>)? . <bounds/exceptions>? . <payload> . build()

frequency → position → bounds → exceptions → what-it-is.

OpenerMeansExample
every(unit)recurring (day/week/month/year)every("year").on(date(12, 25))
every(n, unit)every n unitsevery(2, "weeks").on("tuesday")
from(anchor)relative to an anchorfrom("easter").plus({ days: 49 })
once(date)one-timeonce("2025-06-15")

every("day") (and every(n, "days")) needs no .on(...) — a daily cadence has no position.

Shorthands desugar to the canonical form (same result):

weekly("tuesday"); // = every("week").on("tuesday")
weekly("monday", "wednesday", "friday"); // several weekdays at once
monthly(15); // = every("month").on(15)
monthly(-1); // last day of every month (negative counts from the end)
daily(); // = every("day") — every day
yearly(12, 25); // = every("year").on(date(12, 25))
every("week").on("tuesday"); // a weekday
every("week").on("monday", "wednesday", "friday"); // several weekdays (like an alarm's "Repeat")
every("month").on(15); // a day of the month
every("month").on(-1); // last day of every month (-2 = second-to-last)
every("month").on(nth(1, "monday")); // 1st Monday of every month
every("year").on(date(12, 25)); // a month + day
every("year").on(nth(2, "sunday", "may")); // 2nd Sunday of May (Mother's Day)

Pass several weekday names to fire on each of them every week — it compiles to one weekly event with a dayOfWeek list ([1, 3, 5]). With an interval the selected days stay in phase: every(2, "weeks").on("monday", "wednesday") fires both days on the same weeks.

nth(n, weekday) takes an optional third argument: omit the month and it repeats every month (every("month").on(nth(1, "monday")) — 1st Monday of every month); pass a month to pin one (nth(2, "sunday", "may") — only May). Use -1 for the last occurrence (nth(-1, "friday") — last Friday of every month).

.on() also accepts plugin selectors — a plugin extends the vocabulary by exporting one, e.g. the lunar plugin’s lunar.date(1, 1):

import { lunar } from "calendaryjs-plugin-lunar";
every("year").on(lunar.date(1, 1)).title("Lunar New Year");

from(name) references a resolver registered with cal.registerFormula(name, year => Date) (or supplied by a plugin). Shift with .plus() / .minus():

from("easter").plus({ days: 49 }); // Pentecost
from("easter").minus({ days: 46 }); // Ash Wednesday

Chainable, in any order, before build():

every("year")
.on(date(9, 2))
.title("National Day")
.priority(100) // z-index ordering on a busy day
.color("#d4af37")
.at("09:00", "11:00") // start / end time
.categories("public", "holiday")
.remind({ days: 1 }) // a VALARM, 1 day before
.metadata({ icon: "flag" }) // arbitrary, for your UI
.between(2025, 2035) // year bounds (or date strings)
.exceptYears(2030) // skip a year
.skip("2031-09-02"); // drop one occurrence

Full set — payload: title · id · description · location · url · color · icon · source · status · allDay · at · duration · keywords · categories · metadata · remind. Per-occurrence: skip · override · reschedule. Recurring bounds: between · until · exceptYears · exceptMonths · exceptDates.

The year bounds (between / until / exceptYears) are applied centrally, so they work for every recurring type — const, daily, weekly, monthly, nth-weekday, and plugin types alike.

id is derived from the title if you omit it (diacritics folded: Café → cafe). Set it explicitly for events you store or reference.

addGroup accepts builders directly (it compiles them for you):

import { calendary } from "calendaryjs";
import { every, once, date } from "calendaryjs/builder";
const cal = calendary();
cal.addGroup({
id: "events",
events: [
every("year").on(date(12, 25)).title("Christmas"),
once("2025-06-15").title("Wedding"),
],
});

Builder vs config — two forms, one model

Section titled “Builder vs config — two forms, one model”

The builder is the authoring form; the plain config object is the storage / interchange form (JSON, ICS export). The builder simply build()s into that object — so you get readable authoring without giving up a serializable, portable model. Hand-write the config directly when you prefer; both are supported.