--- title: Rule reference description: The full Calendite rule syntax, covering weekdays, nth-of-month, dates, shifts, Easter, and exclusions. order: 3 --- # Rule reference The complete recurrence rule syntax. If you're new to the rule language, [repeating events](/docs/everyday-use/repeating-events.html) teaches it from the beginning and [building repeats](/docs/everyday-use/building-repeats.html) covers the picker that writes rules for you. > This page is generated from the rule engine's own specification, which is verified > against the engine: every example was run through it to confirm the dates it produces. > The full argument lists for `MOON(...)` and `EASTER(...)` have their own pages, > [Moon rules](/docs/reference/moon-rules.html) and [Easter rules](/docs/reference/easter-rules.html). --- ## Contents 1. [The three things to know first](#1-the-three-things-to-know-first) 2. [Every day](#2-every-day) 3. [Every N days / weeks / months / years](#3-every-n-days-weeks-months-years) 4. [A weekday every week](#4-a-weekday-every-week) 5. [A day of the month](#5-a-day-of-the-month) 6. [A day of the year](#6-a-day-of-the-year) 7. ["2nd Saturday of the month" style](#7-2nd-saturday-of-the-month-style) 8. [Weekdays and weekends](#8-weekdays-and-weekends) 9. [A one-off date](#9-a-one-off-date) 10. [Excluding dates with `!`](#10-excluding-dates-with) 11. [Categories, tags, events and feeds](#11-categories-tags-events-and-feeds) 12. [Move to a qualifying day with `~`](#12-move-to-a-qualifying-day-with) 13. [Moving dates by a fixed amount with `+` and `-`](#13-moving-dates-by-a-fixed-amount-with-and-) 14. [Powerful patterns with `__OF__`](#14-powerful-patterns-with-__of__) 15. [A specific ISO week of the year](#15-a-specific-iso-week-of-the-year) 16. [Limiting to certain months](#16-limiting-to-certain-months) 17. [Moon phases](#17-moon-phases) 18. [Easter and the days that move with it](#18-easter-and-the-days-that-move-with-it) 19. [Intersections](#19-intersections) 20. [Notes & limitations](#20-notes-limitations) 21. [Cheat sheet](#21-cheat-sheet) 22. [Recipes](#22-recipes) --- ## 1. The three things to know first **1. Combine rules with commas.** A schedule can be several rules separated by commas. The engine gathers the dates from each one: ```text Mon, Thu ``` → every Monday **and** every Thursday. **2. A `!` in front means "exclude".** A rule that starts with `!` *removes* dates instead of adding them. It's perfect for "…except": ```text WD, !BankHolidays ``` → every weekday **except** bank holidays. **3. Order matters — put inclusions first, exclusions last.** The engine reads your rules left to right, adding dates for normal rules and removing them for `!` rules. So write what you want first, then subtract: ```text WD, !Fri ✅ every weekday except Friday !Fri, WD ❌ removes Friday, then adds all weekdays back — Friday returns ``` If a schedule has **no** normal (non-`!`) rule at all, the event simply falls on its own start date. Everything below is just different ways to fill in those rules. ### The building-block codes **Days of the week** (exactly these, capitalised): | Mon | Tue | Wed | Thu | Fri | Sat | Sun | |-----|-----|-----|-----|-----|-----|-----| **Months** (exactly these, capitalised): | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | |-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----| --- ## 2. Every day | Rule | Meaning | | ----- | ---------------- | | `Day` | Every single day | That's it — `Day` on its own fires every day. --- ## 3. Every N days / weeks / months / years These repeat relative to the event's **start date**. Add a number for the interval; leave it off for "every one". | Rule | Meaning | | ---------- | ---------------------------------------------- | | `Day2` | Every 2nd day (`Day5` = every 5 days) | | `Weekly` | Every week, on the start date's weekday | | `Weekly2` | Every 2 weeks | | `Monthly` | Every month, on the start date's day-of-month | | `Monthly2` | Every 2 months | | `Yearly` | Every year, on the start date's month and day | | `Yearly2` | Every 2 years | > **Short months:** if the start date is the 31st, `Monthly` automatically falls on > the last day of shorter months (the 30th, or 28th/29th in February). *Example — `Day3` from 1 Jan 2025 →* `2025-01-01, 2025-01-04, 2025-01-07, …` --- ## 4. A weekday every week Just write the day code. Add a trailing number to skip weeks. | Rule | Meaning | | ------ | ------------------------------------ | | `Thu` | Every Thursday | | `Thu2` | Every **2nd** Thursday (fortnightly) | | `Sat3` | Every 3rd Saturday | *Example — `Thu2` from 1 Jan 2025 →* `2025-01-02, 2025-01-16, 2025-01-30, 2025-02-13, …` --- ## 5. A day of the month Write the day number followed by `Month`. | Rule | Meaning | | ---------- | ----------------------------------- | | `5Month` | The 5th of every month | | `5Month2` | The 5th of every **2nd** month | | `-1Month` | The **last day** of every month | | `-2Month` | The second-to-last day of the month | A negative number counts back from the end of the month. *Example — `-1Month` →* `2025-01-31, 2025-02-28, 2025-03-31, …` *Example — `5Month2` →* `2025-01-05, 2025-03-05, 2025-05-05, …` ### Months that are too short Not every month has a 31st, and February has no 30th. **By default the rule simply doesn't happen that month:** *Example — `31Month` →* `2025-01-31, 2025-03-31, 2025-05-31, 2025-07-31, 2025-08-31, 2025-10-31, 2025-12-31` — seven dates a year, and nothing in the five short months. That is deliberate. It is what the iCalendar standard requires, so it is what every calendar Calendite exchanges events with does, and an event imported from elsewhere can never mean anything else. It is often *not* what someone means by "the 31st of every month", though, so you can say what you want instead: | Rule | February 2025 | April 2025 | | ------------------ | ------------- | ---------- | | `31Month` | *(nothing)* | *(nothing)* | | `31Month(CLAMP)` | the 28th | the 30th | | `31Month(NEXT)` | 3 March | 1 May | `CLAMP` uses the last day the month does have. `NEXT` keeps counting past the end of the month. February 2025 runs out after the 28th, so its 29th, 30th and 31st are the 1st, 2nd and 3rd of March — the day number still means something once it has left the month, rather than everything past the end piling onto the 1st. A leap February is a day longer, so the 31st lands on 2 March instead. This is also how English law treats a 29 February birthday: age is attained on 1 March. Two things worth knowing: - **The rhythm doesn't change.** `31Month2(CLAMP)` stays on the odd months and clamps the short ones (`2025-01-31, 2025-03-31, 2025-05-31, 2025-07-31, 2025-09-30, 2025-11-30`). A date that runs over is still counted against the month it came *from*, so `NEXT` never nudges the schedule along a month at a time. - **`-1Month` is usually simpler.** If you mean "the end of the month", say that — `31Month(CLAMP)` produces exactly the same dates the long way round. The same suffix works on a day of the year (§6), so `29Feb` happens only in leap years, `29Feb(CLAMP)` falls back to the 28th, and `29Feb(NEXT)` counts on to 1 March. --- ## 6. A day of the year Write the day and month together. Add a trailing number for a multi-year interval. | Rule | Meaning | | --------- | ----------------------------------- | | `25Dec` | Every 25th December | | `1Jan` | Every 1st January | | `25Dec2` | Every 25th December, every 2nd year | A negative day counts back from the end of that month, the same way `-1Month` does for an ordinary month (§5): | Rule | Meaning | | -------- | ------------------------------------------ | | `-1Aug` | The last day of August (the 31st) | | `-2Aug` | The second-to-last day of August (the 30th) | | `-1Feb` | The last day of February | The date is worked out per year rather than fixed, so `-1Feb` lands on the 28th in a common year and the 29th in a leap year. **February 29th.** `29Feb` happens only in leap years — the other years are skipped, as §5 describes. Add a suffix to say otherwise: | Rule | 2024 (leap) | 2025 | | --------------- | ----------- | ------- | | `29Feb` | 29 Feb | *(nothing)* | | `29Feb(CLAMP)` | 29 Feb | 28 Feb | | `29Feb(NEXT)` | 29 Feb | 1 Mar | A day the month can never have is refused outright — there is no 31st of April in any year, so `31Apr` is a typo rather than a schedule. Saying what you want makes it meaningful again: `31Apr(CLAMP)` is the 30th and `31Apr(NEXT)` is 1 May. *Example — `-1Feb` →* `2024-02-29, 2025-02-28, 2026-02-28, …` --- ## 7. "2nd Saturday of the month" style Put an occurrence number **in front** of the day. | Rule | Meaning | | -------- | --------------------------------------- | | `2Sat` | The 2nd Saturday of the month | | `1Mon` | The 1st Monday of the month | | `-1Thu` | The **last** Thursday of the month | | `2Sat2` | The 2nd Saturday of every **2nd** month | *Example — `2Sat` →* `2025-01-11, 2025-02-08, 2025-03-08, …` **Occurrence vs. week — the underscore.** An underscore changes the meaning from "the Nth *occurrence*" to "the day that falls in the Nth *week* of the month": | Rule | Meaning | | ------- | --------------------------------------------------------- | | `2Wed` | The **2nd Wednesday** of the month | | `2_Wed` | The Wednesday that falls in the **2nd week** of the month | These differ when the month starts mid-week. For February 2024, `2Wed` is the 14th but `2_Wed` is the 7th. **Week 1 is the week containing the 1st**, even when most of it belongs to the previous month, and weeks begin on your locale's first day (UK: Monday). February 2025 opens on a Saturday, so its week 1 is Jan 27 – Feb 2 and its week 2 is Feb 3 – 9 — making `2_Mon` the 3rd, while `2Mon` is the 10th. The two forms agree whenever the day you ask for is not earlier in the week than the day the month opens on. A month starting on a Monday makes them identical throughout. **A day outside the month is not an occurrence.** `1_Mon` in January 2025 asks for the Monday of Dec 30 – Jan 5, which is 30 December — so January simply has none, and the rule resumes in months whose first week does contain a Monday. The same applies at the end: `-1_Sat` asks for the Saturday of the month's last week, which in January 2025 is 1 February, so again there is no occurrence. --- ## 8. Weekdays and weekends `WD` means weekdays, `WE` means weekend days. | Rule | Meaning | | ---- | ----------------- | | `WD` | Every weekday | | `WE` | Every weekend day | > Which days count as the weekend depends on your region — the engine uses your > locale's week configuration (UK: Mon–Fri weekdays, Sat–Sun weekend). You can also count **occurrences** of them within a month, exactly like named days, with an optional month interval: | Rule | Meaning | | ------- | --------------------------------------- | | `2WD` | The 2nd weekday of the month | | `-1WD` | The **last** weekday of the month | | `3WE` | The 3rd weekend day of the month | | `-1WD2` | The last weekday of every **2nd** month | ### Intervals and week-of-month A trailing number (no leading occurrence number) gives two more forms, following the same occurrence-vs-week underscore rule as `2Sat` / `2_Sat`: | Rule | Meaning | | ------- | -------------------------------------------------------- | | `WD2` | Every **2nd weekday**, counting weekdays from the start (Mon, Wed, Fri, Tue, …) | | `WD3` | Every 3rd weekday | | `WE2` | Every 2nd weekend day | | `WD_2` | Every weekday **in the 2nd week** of each month | | `WE_2` | Every weekend day in the 2nd week of each month | The **no-underscore** form counts individual days (`WD2` = every other weekday). The **underscore** form selects a whole week of the month (`WD_2` = all weekdays in its 2nd week), mirroring `2_Sat` = "the Saturday in the 2nd week of the month". *Example — `WD2` from Mon 6 Jan 2025 →* `Jan 6, 8, 10, 14, 16, 20, 22, 24, …` *Example — `WD_2` →* `Jan 6–10, Feb 3–7, …` (the weekdays of each month's 2nd week) ### Grouped day-sets Wrap a dot-separated set of days in parentheses to match any of them: | Rule | Meaning | | --------------- | -------------------------------- | | `(Mon.Wed.Fri)` | Every Monday, Wednesday **or** Friday | | `(WD)` | Every weekday (same as `WD`) | | `(WE)` | Every weekend day | Standalone, `(Mon.Wed.Fri)` is just shorthand for `Mon, Wed, Fri`. It's most useful as the inner part of an `__OF__` pattern — `2__(Mon.Wed.Fri)__OF__MONTH` (§14) — or an ISO-week rule (§15). --- ## 9. A one-off date Write a full date in `YYYY-MM-DD` form (zero-padded): ```text 2025-05-17 ``` → occurs on 17 May 2025, and only then. One-off dates are most useful combined with other rules — see exclusions below. --- ## 10. Excluding dates with `!` Put `!` in front of any rule to *remove* its dates. | Rule | Meaning | | --------------- | ------------------------------------ | | `!1Sat` | Not on the 1st Saturday of the month | | `!BankHolidays` | Not on any bank holiday | | `!2025-12-25` | Not on 25 December 2025 | Combine freely (remember: inclusions first, exclusions after): ```text Sat, !1Sat, !2Sat ``` → every Saturday **except** the first two of each month. *(Jan 2025 → 18th & 25th; Feb 2025 → 15th & 22nd.)* ```text WD, !2025-12-25 ``` → every weekday, but skip Christmas Day 2025. --- ## 11. Categories, tags, events and feeds These rules reference dates that live **outside** the rule itself. They only resolve when the app supplies the relevant data — on their own they contribute no dates. ### Categories A **category** is a named group of dates, written in `PascalCase` (it must start with a capital and contain at least one lowercase letter): ```text BankHolidays ``` Use it like any other rule — most often as an exclusion: ```text WD, !BankHolidays ``` → every working weekday. **Pick one member of a category** by naming it in parentheses (also PascalCase): ```text BankHolidays(Christmas) ``` → only the Christmas date(s) from the bank-holidays group. You can define your own categories too, e.g. `KarateDays` or `SchoolHolidays`, and reference them the same way. ### Named selectors: `Tag`, `Event`, `Feed` When you want to be explicit about *what kind* of thing you're referencing, use a named selector. The name inside the brackets starts with a capital and is letters/digits: | Rule | Means | | ---------------- | -------------------------------------------------------- | | `Tag(Work)` | The dates of **every event carrying the tag** `Work` | | `Event(Work)` | The occurrence dates of the **single event series** `Work` | | `Feed(BankHolidays)` | The dates supplied by the external **feed** `BankHolidays` | `Tag(Work)` and `Event(Work)` are deliberately different date sets — one is every event sharing a tag, the other is one specific event — which is why the kind is spelled out. *Example:* `WD, !Tag(Karate)` → every weekday except days carrying the Karate tag. > **`WORKDAY` alias:** `WORKDAY` is accepted as a shorthand for `Tag(Work)`. It's > rewritten before evaluation, so it behaves identically — standalone (`WORKDAY`, > `!WORKDAY`) and nested (`1__WORKDAY__OF__MONTH`). Like any selector it needs a > data source to resolve. --- ## 12. Move to a qualifying day with `~` Sometimes you want "the weekday nearest to a date" — e.g. a holiday observed on a weekday when the real date lands on a weekend. Use the `~` operator: ```text {type} ~ {direction} {anchor} ``` - **`{type}`** is the kind of day to land on. Usually `WD` or `WE`, but any rule works: `WORKDAY`, a weekday (`Mon`…`Sun`), a day-set (`(Mon.Fri)`), a tag (`Tag(Work)`), a moon phase (`MOON(FULL)`), or an interval (`Day`). - **`{anchor}`** is the occasion you are adjusting around — e.g. `25Dec`. - **`{direction}`** is one of: | Symbol | Meaning | | ------ | --------------------------------------------- | | `<` | Strictly before | | `<=` | On the date if it qualifies, otherwise before | | `>` | Strictly after | | `>=` | On the date if it qualifies, otherwise after | | `=` | Nearest | | Rule | Meaning | | ------------- | ------------------------------------------------------------ | | `WD~<25Dec` | The weekday just **before** Christmas | | `WD~<=25Dec` | Christmas itself if it's a weekday, else the weekday before | | `WD~>25Dec` | The weekday just **after** Christmas | | `WD~=25Dec` | The **nearest** weekday to Christmas | *Example — `WD~>25Dec` (2025) →* Christmas is Thursday, so the next weekday is `2025-12-26`. **The type can be any rule**, which is what makes the operator useful beyond weekdays: | Rule | Meaning | | --------------------- | ---------------------------------------------- | | `Tag(Work)~<25Dec` | The last **work day** before Christmas | | `(Mon.Fri)~<25Dec` | The last **Monday or Friday** before Christmas | | `MOON(FULL)~<25Dec` | The last **full moon** before Christmas | | `-1Fri~<25Dec` | The last **Friday of the month** before Christmas | `Tag(Work)` and `WORKDAY` need a data source, like any tag (§11). ### What can go in the type slot The test is whether the rule **produces days**. If it names days you could point at on a calendar, it can be adjusted around: | Can be the type | Example | | ------------------------------------------------- | -------------------- | | Weekday / weekend / every day | `WD`, `WE`, `Day` | | A named weekday, or a day-set | `Fri`, `(Mon.Fri)` | | An ordinal, **including a negative one** | `1Mon`, `-1Fri` | | A date, a yearly date, an interval | `2026-05-17`, `25Dec`, `Monthly` | | A moon phase | `MOON(FULL)` | | A tag, event, feed or category *(needs its data)* | `Tag(Work)`, `WORKDAY` | | An `__OF__` composite | `-1__WD__OF__MONTH` | An anchored period can be the type — `Fri__OF__WEEK(25Dec)~<1Jan` is the Friday of Christmas week, taken before New Year's Day — but only one bracketed group is allowed, so `(1..2)__WD__OF__WEEK(25Dec)~<1Jan` is out. | Cannot be the type | Why | | --------------------- | ------------------------------------------------------- | | `LIMIT(...)` | A filter, not a producer — see below | | `@&@(...)` | Resolved before the engine sees it (§11) | | Two bracketed groups | `(1..2)__WD__OF__WEEK(25Dec)` — one group is the limit | **A `LIMIT` cannot be adjusted around, on either side.** It filters days rather than producing them, so there is nothing to land on. Worse, asking it a question turns it inside out — a `LIMIT` matches the months it *removes* — so `LIMIT(Jan)~<25Dec` quietly meant "the last day not in January before Christmas" and answered the 24th. Both `LIMIT(Jan)~<25Dec` and `WD~ **"The X closest to Y" is meaningful when X and Y run on cycles that drift > against each other, and empty when they share a cycle.** That is the test to apply before writing a `~=`, and it is about the two rules' periods, not their frequencies. ### What `~` deliberately does not do Even where "closest" varies, English is ambiguous between two readings, and `~` only implements the first: 1. **Per anchor** — for each 22nd, the nearest full moon. Many answers. This is `~`. 2. **Superlative** — of all the candidate full moons, the single one nearest to a 22nd. One answer. Reading 2 needs a bounded set of candidates to mean anything: over June and July alone the answer might be 23 June, but widen the span and it changes. A rule has to answer `occursOn(date)` from a date alone, so a rule whose dates shift depending on how far you scroll is not a rule at all. There is no syntax for reading 2, and asking for it with `~` silently gets you reading 1 instead. See `Bounded_Superlative_Selection.md` in the rule engine's repository for what adding it would involve. **For "the X of the week of Y", use `__OF__WEEK(...)` instead (§14).** `~` finds the *nearest* match, which is not the same question and gives a different answer near a week boundary. Christmas 2028 falls on a Monday, so `Fri~=25Dec` returns Friday the 22nd — nearer, but the week *before* Christmas — while `Fri__OF__WEEK(25Dec)` returns the 29th. Use `~` when you mean "nearest", and the anchored period when you mean "in the same week". **Negate the type with a leading `!`.** Put `!` in front of the type to land on a day that is **not** that type — e.g. the next *non*-working day: | Rule | Meaning | | -------------- | ------------------------------------------------ | | `!WD~>25Dec` | The first **non-weekday** (weekend) after Christmas | | `!WD~<25Dec` | The first non-weekday before Christmas | Here the `!` negates the *type*, not the whole rule — `!WD~>25Dec` still **adds** a date (a weekend day), it doesn't exclude anything. *Example — `!WD~>25Dec` (2024) →* Christmas is Wednesday, so the next weekend day is `2024-12-28` (Saturday). --- ## 13. Moving dates by a fixed amount with `+` and `-` Sometimes you don't want the nearest *sort* of day — you want a plain "three days before that". Add or subtract a duration: ```text {rule} + P{n}{Unit} {rule} - P{n}{Unit} ``` `{Unit}` is `Day`, `Week`, `Month` or `Year`. Every date the rule produces moves by that amount. | Rule | Meaning | | ---------------------- | ------------------------------------------ | | `25Dec-P1Day` | Christmas Eve, every year | | `25Dec+P1Day` | Boxing Day | | `Mon-P1Day` | Every Sunday-before-a-Monday | | `Event(Karate)-P1Day` | The day before every karate class | | `Tag(Payday)-P1Week` | A week before each payday | | `-1Thu+P2Day` | Two days after the last Thursday of the month | *Example — `25Dec-P1Day` →* `2025-12-24, 2026-12-24, 2027-12-24, …` ### Why the `P` The `P` marks a **duration**, and without it the rule would be ambiguous. This grammar already uses `{number}{unit}` to mean a *position*: `1Month` is the 1st of the month (§5) and `-1Fri` is the last Friday (§7). So `25Dec-1Day` could not be told apart from those, and a bare `-` clashes with ISO dates too. With the `P` everything reads cleanly, including `2025-05-17-P1Day`. ### This is not the same as `~` The two look similar and answer very different questions. | | Asks | `25Dec`, 2028 | | --- | --- | --- | | `WD~<25Dec` | the nearest day *of a type* | **22nd** — Christmas is a Monday, so it skips back to the Friday | | `25Dec-P1Day` | exactly one day earlier | **24th** — always, whatever weekday that is | Use `~` when you mean "the working day before"; use `-P1Day` when you mean "the day before". A shift never changes its mind based on what weekday something lands on, which also means it can't quietly collapse the way an ill-posed `~` can (§12). ### Months clamp to the end of the month There is no 31st of February, so a month or year shift lands on the last day that exists: | Rule | Result | | ---------------- | ------------------------------- | | `31Jan+P1Month` | 28 Feb (29 Feb in a leap year) | Clamping loses the original day, so **shifts do not undo each other**: `31Jan+P1Month-P1Month` is the **28th** of January, not the 31st. This surprises people, so it is worth saying plainly — the 31st became the 28th on the way out and there is nothing left to say it was ever the 31st. ### Chaining Shifts apply left to right, so you can stack them: ```text 1Month+P1Month-P1Day ``` → the 1st of the month, forward a month, back a day = **the last day of every month**. (`-1Month` says the same thing more directly, §5.) ### Limits - A `LIMIT(...)` cannot be shifted. It removes days rather than producing them, so there is nothing to move — the same reason it cannot be adjusted around (§12). - A shift of zero (`25Dec+P0Day`) is rejected; write `25Dec`. - `~` and `__&__` bind **looser** than a shift, so `WD~<25Dec-P1Day` is the weekday before *Christmas Eve*, and `Mon__&__Fri-P1Day` intersects against the shifted Friday. --- ## 14. Powerful patterns with `__OF__` For advanced schedules, the engine has a "pick the Nth *thing* out of a *period*" pattern: ```text {which}__{inner}__OF__{period} ``` - **`{period}`** is `DAY`, `WEEK`, `MONTH`, `YEAR`, or a specific month (`Jan`…`Dec`). - **`{inner}`** is the kind of day to pick. Use `WD`, `WE`, a single day code (`Mon`…`Sun`), a grouped day-set (`(Mon.Wed.Fri)`), or a data-backed reference — a category (`BankHolidays`) or a selector (`Tag(X)`, `Event(X)`, `Feed(X)`). - **`{which}`** selects which occurrence(s): | Form | Meaning | | --------- | ------------------------------------------------ | | `1` | The 1st | | `-1` | The last (negative counts from the end) | | `(1..5)` | The 1st through 5th (a range) | | `(1.3.5)` | The 1st, 3rd and 5th only (a set) | | `ALL` | Every one in the period (**specific months only**) | Range/set members may be negative — `(-3..-1)` for the last three, `(1.-1)` for the first and the last. A range's two endpoints must share a sign (use a set for mixed). ### With `MONTH` / `YEAR` / `WEEK` / `DAY` | Rule | Meaning | | ---------------------------- | ------------------------------------------ | | `1__WD__OF__MONTH` | First weekday of the month | | `-1__WD__OF__MONTH` | Last weekday of the month | | `(1..5)__WD__OF__MONTH` | First through fifth weekday of the month | | `(1.3.5)__WD__OF__MONTH` | 1st, 3rd and 5th weekday of the month | | `-1__Fri__OF__MONTH` | Last Friday of the month | | `-2__WE__OF__YEAR` | Second-to-last weekend day of the year | *Example — `-1__Fri__OF__MONTH` →* `2025-01-31, 2025-02-28, 2025-03-28, …` **Positions are counted over the whole period**, whatever day the event begins on. An event created on the 4th Thursday of November with the rule `4__Thu__OF__Nov` fires that very day; one created on the 3rd of January with `2__WD__OF__MONTH` fires nothing in January (the 2nd weekday, the 2nd, is already past) and picks up in February. ### With a specific month (repeats yearly) Here the period is a month code, and you can use `ALL`, plus an optional year-interval suffix: | Rule | Meaning | | ------------------------- | ------------------------------------------- | | `4__Thu__OF__Nov` | 4th Thursday of November (US Thanksgiving) | | `-1__Mon__OF__May` | Last Monday of May (US Memorial Day) | | `1__Sun__OF__Nov` | First Sunday of November | | `ALL__Mon__OF__Jan` | Every Monday in January | | `ALL__Mon__OF__(Jan.Jul)` | Every Monday in January **and** July | | `4__Thu__OF__Nov2` | 4th Thursday of November, every 2nd year | *Example — `4__Thu__OF__Nov` →* `2023-11-23, 2024-11-28, 2025-11-27, …` ### With a tag, event, feed or category The inner rule can also be a data-backed reference (§11). This is how you express "the Nth **working day** of the month" — model your work days as a tag or event and pick from them: | Rule | Meaning | | --------------------------- | --------------------------------------------- | | `1__Tag(Work)__OF__MONTH` | First day carrying the `Work` tag, each month | | `-1__Tag(Work)__OF__MONTH` | **Last** work day of the month | | `1__Event(Work)__OF__MONTH` | First occurrence of the `Work` event, each month | | `-1__BankHolidays__OF__YEAR`| The last bank holiday of the year | *Example — `-1__Tag(Work)__OF__MONTH` →* the last `Work` day in each month. ### With a grouped day-set The inner rule can be a parenthesised day-set (§8), so you can pick from a chosen handful of weekdays: | Rule | Meaning | | ----------------------------- | --------------------------------------------- | | `2__(Mon.Wed.Fri)__OF__MONTH` | The 2nd day among Mon/Wed/Fri each month | | `-1__(Mon.Wed.Fri)__OF__MONTH`| The last Mon/Wed/Fri of the month | *Example — `2__(Mon.Wed.Fri)__OF__MONTH` (2024) →* `Jan 3, Feb 5, Mar 4, …` ### Anchoring the period to another rule Put a rule in brackets after the period and it stops repeating on the calendar and pins itself to **the period containing that rule's dates**: ```text {which}__{inner}__OF__{period}({anchor}) ``` `1__WD__OF__MONTH` is the first weekday of *every* month. `1__WD__OF__MONTH(25Dec)` is the first weekday of *December* — the month Christmas falls in. | Rule | Meaning | | --------------------------------- | ------------------------------------------------ | | `Fri__OF__WEEK(25Dec)` | The Friday of Christmas week | | `Mon__OF__WEEK(3Tue)` | The Monday of the week holding the 3rd Tuesday | | `WD__OF__WEEK(25Dec)` | Every weekday of Christmas week | | `1__WORKDAY__OF__WEEK(25Dec)` | The first work day of Christmas week | | `-1__WD__OF__MONTH(25Dec)` | The last weekday of December | | `1__Mon__OF__YEAR(Event(Launch))` | The first Monday of the launch year | The anchor is any rule at all — a date, another rule, or a data-backed reference (`Tag(X)`, `Event(X)`, `Feed(X)`, a category). Several anchors landing in one period describe the same period, so the result is not repeated. **Omitting `{which}` gives every matching day** of that period, which is why `Fri__OF__WEEK(25Dec)` reads as simply "the Friday of Christmas week" — there is only one. Supply `{which}` to pick by position, using the same grammar as above. **Why not just use `~`?** Because "the week of" and "the nearest" are different questions. `Mon~<3Tue` (§12) searches backwards for a Monday, so it needs you to know that Monday precedes Tuesday; `Mon__OF__WEEK(3Tue)` does not. More importantly, the nearest match can land in the wrong week: in 2028 Christmas is a Monday, so `Fri~=25Dec` gives Friday 22 December — nearer, but the week *before* Christmas — while `Fri__OF__WEEK(25Dec)` correctly gives the 29th. *Example — `Fri__OF__WEEK(25Dec)` →* `2025-12-26, 2026-12-25, 2027-12-24, 2028-12-29` --- ## 15. A specific ISO week of the year To land on days inside a numbered ISO week of the year, use `__OF__WEEK`: ```text {days} __OF__ WEEK{number} ``` `{days}` may be a single day code, a grouped set `(Mon.Wed.Fri)`, or `(WD)` / `(WE)`. | Rule | Meaning | | -------------------------- | ------------------------------ | | `Mon__OF__WEEK5` | The Monday of ISO week 5 | | `(Mon.Wed.Fri)__OF__WEEK5` | Mon, Wed and Fri of ISO week 5 | | `(WD)__OF__WEEK5` | Every weekday of ISO week 5 | Week numbers run 1–53. ISO weeks start on Monday, and week 1 is the week containing the year's first Thursday. This is "a day inside week 5", which is different from "the 5th Monday of the year". *Example — `Mon__OF__WEEK5` (2025) →* `2025-01-27`. --- ## 16. Limiting to certain months `LIMIT(...)` restricts **everything else** to only the listed months. Write it last. ```text Mon, Thu, LIMIT(Jan.Mar) ``` → every Monday and Thursday, but only in January and March. ```text WD, LIMIT(Jan.Feb.Mar) ``` → every weekday in Q1 only. Months inside `LIMIT(...)` are separated by dots. ### Everything *except* certain months with `!LIMIT(...)` Put a `!` in front to flip it: `!LIMIT(...)` keeps the **whole year except** the listed months. ```text Day, !LIMIT(Jan) ``` → every day of the year except in January. ```text Mon, !LIMIT(Jul.Aug) ``` → every Monday except during the summer break (July and August). The `!` here does **not** turn `LIMIT` into an inclusion — like plain `LIMIT` it only ever *removes* dates. It just flips **which** months are removed: `LIMIT(Jan)` removes everything but January; `!LIMIT(Jan)` removes only January. (See §20 on how `!` changes meaning depending on where it sits.) So `LIMIT(Jan)` and `!LIMIT(Jan)` are exact complements — together they cover the whole year with no overlap. ### Scoping a `LIMIT` to a group with `{ }` Normally a `LIMIT` applies to the **whole** schedule. Wrap some rules in braces `{ … }` to make a `LIMIT` (or any exclusion) apply only *within* that group: ```text {Mon, LIMIT(Jan.Mar)}, {Fri, LIMIT(Jul)} ``` → Mondays in Jan & March, **and** Fridays in July — each `LIMIT` stays inside its own group and doesn't touch the other. ```text {WD, !Fri}, Sun ``` → (weekdays except Friday) plus every Sunday — the `!Fri` only applies to the group, so it can't remove anything from the `Sun` rule. A group must contain at least one normal (non-`!`) rule, and groups can't be nested. --- ## 17. Moon phases For lunar schedules, use `MOON(...)`. ```text MOON(FULL) MOON(NEW) ``` You can target a **named phase**, an **illumination percentage** (0–100), or a **range**: | Rule | Meaning | | ---------------------- | -------------------------------- | | `MOON(FULL)` | The full moon | | `MOON(NEW)` | The new moon | | `MOON(100)` | Full moon (100% lit) | | `MOON(0)` | New moon (0% lit) | | `MOON(50..75)` | When the moon is 50–75% lit | | `MOON(50, WAXING)` | 50% lit while **growing** | | `MOON(50..75, WANING)` | 50–75% lit while **shrinking** | A given brightness happens twice a month (once growing, once shrinking), so adding `WAXING` or `WANING` picks just one side. (`FIRST_HALF` / `SECOND_HALF` are accepted as synonyms; `BOTH` is the default.) **Named phases:** `NEW`, `WAXING_CRESCENT`, `FIRST_QUARTER`, `WAXING_GIBBOUS`, `FULL`, `WANING_GIBBOUS`, `LAST_QUARTER`, `WANING_CRESCENT`. *Example — `MOON(FULL)` (2025) →* `2025-01-14, 2025-02-13, 2025-03-15, …` --- ## 18. Easter and the days that move with it Easter is worked out, not looked up. Write `EASTER` on its own for Easter Sunday, or name the day you want in brackets: ```text EASTER EASTER(GoodFriday) EASTER(Monday) ``` | Rule | Meaning | | --------------------------- | ------------------------------------------- | | `EASTER` | Easter Sunday | | `EASTER(GoodFriday)` | Good Friday | | `EASTER(Monday)` | Easter Monday | | `EASTER(AshWednesday)` | Ash Wednesday, the start of Lent | | `EASTER(MotheringSunday)` | Mothering Sunday | | `EASTER(Pentecost)` | Whit Sunday | | `EASTER(Orthodox)` | Orthodox Easter Sunday | | `EASTER(GoodFriday, Orthodox)` | Orthodox Good Friday | *Example — `EASTER` →* `2025-04-20, 2026-04-05, 2027-03-28, …` *Example — `EASTER(GoodFriday)` (2026) →* `2026-04-03`; *`EASTER(Monday)` →* `2026-04-06` **The days you can name**, as offsets from Easter Sunday: | Name | Days | Name | Days | | ----------------- | ---- | ---------------- | ---- | | `CleanMonday` | −48 | `Sunday` | 0 | | `ShroveTuesday` | −47 | `Monday` | +1 | | `AshWednesday` | −46 | `Ascension` | +39 | | `MotheringSunday` | −21 | `Pentecost` / `WhitSunday` | +49 | | `PalmSunday` | −7 | `WhitMonday` | +50 | | `MaundyThursday` | −3 | `TrinitySunday` | +56 | | `GoodFriday` | −2 | `CorpusChristi` | +60 | | `HolySaturday` | −1 | | | **Two reckonings:** `Western` (the default, and what the UK uses) or `Orthodox`. The bracket takes up to one day and one reckoning, in either order, each optional: `EASTER(Monday, Orthodox)` and `EASTER(Orthodox, Monday)` are the same rule. Any day name works with either reckoning — it is only an offset — though `AshWednesday`, `ShroveTuesday`, `MotheringSunday` and `CorpusChristi` are Western observances and `CleanMonday` an Orthodox one. *Example — `EASTER(Orthodox)` (2026) →* `2026-04-12` A few things worth knowing: - Names are spelled exactly as in the table, like a category member (`BankHolidays(GoodFriday)`). `EASTER(goodfriday)` and `EASTER(Tuesday)` are rejected. - `EASTER` is in capitals, like `MOON` and `WD`, because the engine computes it. PascalCase `Easter` is a *category* (§11) and still means whatever data you give it. - It is one date a year, so it composes with everything else here: `!EASTER(GoodFriday)` excludes it, `WD~25Dec` = a non-weekday, still additive). On an intersection it excludes the whole thing (§19). On a `LIMIT` it flips which months are removed (`!LIMIT(Jan)` = every month *except* January; §16) — it stays a filter, not an inclusion. You can't negate a single intersection operand, and you can't exclude a whole `~` rule. - **Brace groups** can't be nested, must hold at least one normal rule, and are always additive (a leading `!` on a group doesn't turn it into an exclusion). --- ## 21. Cheat sheet | Pattern | Example | Reads as | | ----------------------- | ------------------------- | ------------------------------------- | | `Day` | `Day` | every day | | `DayN` | `Day3` | every 3 days | | `Weekly/Monthly/Yearly` | `Monthly2` | every 2 months | | `Mon`…`Sun` | `Fri` | every Friday | | `N` | `Fri2` | every 2nd Friday | | `NMonth` | `5Month` | the 5th of each month | | `-1Month` | `-1Month` | last day of the month | | `NDec` | `25Dec` | every 25 December | | `-NDec` | `-1Feb` | last day of February | | `N` | `2Sat` | 2nd Saturday of the month | | `N_` | `2_Sat` | Saturday in the 2nd week | | `-N` | `-1Thu` | last Thursday of the month | | `WD` / `WE` | `WD` | every weekday / weekend day | | `NWD` / `NWE` | `-1WD` | last weekday of the month | | `WDn` / `WEn` | `WD2` | every 2nd weekday (by count) | | `WD_n` / `WE_n` | `WD_2` | weekdays in the 2nd week of the month | | `YYYY-MM-DD` | `2025-05-17` | that one date | | `!` | `!1Sat` | exclude | | `` | `BankHolidays` | a named group of dates | | `(Member)` | `BankHolidays(Christmas)` | one date from a group | | `Tag/Event/Feed(X)` | `Tag(Work)` | dates from a tag/event/feed | | `~=` | `WD~=25Dec` | nearest weekday to a date | | `!~` | `!WD~>25Dec` | non-weekday after a date | | `(Day.Day…)` | `(Mon.Wed.Fri)` | any day in the set | | `{ …, LIMIT(…) }` | `{Mon, LIMIT(Jan.Mar)}` | scope a LIMIT to a group | | `__OF__` | `-1__Fri__OF__MONTH` | last Friday of the month | | `__OF__` | `4__Thu__OF__Nov` | 4th Thursday of November | | `__OF__WEEK` | `Mon__OF__WEEK5` | Monday of ISO week 5 | | `__OF__(…)` | `Fri__OF__WEEK(25Dec)` | the period containing another rule | | `LIMIT(...)` | `LIMIT(Jan.Mar)` | restrict to these months | | `!LIMIT(...)` | `!LIMIT(Jan.Mar)` | every month except these | | `MOON(...)` | `MOON(FULL)` | on the full moon | | `EASTER(...)` | `EASTER(GoodFriday)` | Easter, or a day that moves with it | | `__&__` | `13Month__&__Fri` | matches both rules at once | | `+P` | `25Dec+P1Day` | that many days/weeks/months/years later | | `-P` | `25Dec-P1Day` | …and earlier — Christmas Eve | | `(CLAMP)` | `31Month(CLAMP)` | short months use their last day | | `(NEXT)` | `29Feb(NEXT)` | keep counting into the next month | --- ## 22. Recipes | What you want | Rule | | ------------------------------------------------ | ------------------------------ | | Every weekday except bank holidays | `WD, !BankHolidays` | | Every Saturday except the first two of the month | `Sat, !1Sat, !2Sat` | | Payday on the last weekday of the month | `-1__WD__OF__MONTH` | | Payday on the last **working** day of the month | `-1__Tag(Work)__OF__MONTH` | | Mondays and Thursdays, Q1 only | `Mon, Thu, LIMIT(Jan.Feb.Mar)` | | US Thanksgiving | `4__Thu__OF__Nov` | | Fortnightly team meeting on Wednesdays | `Wed2` | | Bin collection every other Tuesday | `Tue2` | | The 5th of every other month | `5Month2` | | Nearest weekday to Christmas | `WD~=25Dec` | | Friday the 13th | `13Month__&__Fri` | | Full-moon events | `MOON(FULL)` | | Good Friday / Easter Monday | `EASTER(GoodFriday)` / `EASTER(Monday)` | | Weekdays, minus the Easter break | `WD, !EASTER(GoodFriday), !EASTER(Monday)` | | Pancake day | `EASTER(ShroveTuesday)` | | Mothering Sunday | `EASTER(MotheringSunday)` |