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 yearevery(2, "weeks").on("tuesday").title("Standup"); // every other Tuesdayevery("month").on(nth(1, "monday")).title("Retro"); // 1st Monday of every monthevery("month").on(-1).title("Rent"); // last day of every monthevery(3, "days").title("Water plants"); // every 3 daysfrom("easter").plus({ days: 49 }).title("Pentecost"); // 49 days after a registered anchoronce("2025-06-15").title("Wedding"); // a single dateThe grammar (always the same order)
Section titled “The grammar (always the same order)”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.
Openers
Section titled “Openers”| Opener | Means | Example |
|---|---|---|
every(unit) | recurring (day/week/month/year) | every("year").on(date(12, 25)) |
every(n, unit) | every n units | every(2, "weeks").on("tuesday") |
from(anchor) | relative to an anchor | from("easter").plus({ days: 49 }) |
once(date) | one-time | once("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 oncemonthly(15); // = every("month").on(15)monthly(-1); // last day of every month (negative counts from the end)daily(); // = every("day") — every dayyearly(12, 25); // = every("year").on(date(12, 25))Positions — .on(...)
Section titled “Positions — .on(...)”every("week").on("tuesday"); // a weekdayevery("week").on("monday", "wednesday", "friday"); // several weekdays (like an alarm's "Repeat")every("month").on(15); // a day of the monthevery("month").on(-1); // last day of every month (-2 = second-to-last)every("month").on(nth(1, "monday")); // 1st Monday of every monthevery("year").on(date(12, 25)); // a month + dayevery("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");Anchored dates — from(anchor)
Section titled “Anchored dates — from(anchor)”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 }); // Pentecostfrom("easter").minus({ days: 46 }); // Ash WednesdayPayload & constraints
Section titled “Payload & constraints”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 occurrenceFull 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.
idis derived from the title if you omit it (diacritics folded: Café → cafe). Set it explicitly for events you store or reference.
Use it — no .build() needed
Section titled “Use it — no .build() needed”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.