Dates and times
The date field (a calendar day), the datetime field (a wall-clock time in an IANA zone with its UTC instant) and the recurrence field (an event's repeating schedule and its occurrences), with parameters, stored shapes and template values.
All three are held once for every locale. Times are stored with the zone they belong to, so a 19:00 start stays 19:00 on both sides of a clock change.
date
A calendar day with no time and no zone: an opening date, a deadline, a date of birth.
fields:
- { name: openedOn, type: date, min: "1800-01-01" }
- { name: closesOn, type: date, index: true } | Parameter | Type | Default | What it does |
|---|---|---|---|
min | YYYY-MM-DD | none | The earliest day accepted. |
max | YYYY-MM-DD | none | The latest day accepted. |
| Code | Message |
|---|---|
date | Enter a date as YYYY-MM-DD |
min_date | Must be on or after 1800-01-01 |
max_date | Must be on or before … |
Stored value: a string, "2026-04-01". With index: true it is projected as that day at midnight UTC, so lists filter and sort by it.
Template value: a date value. Format it with |date, which writes the day in the request locale’s words and order:
<p>Open since {{ entry.openedOn|date:"long" }}</p>
<time datetime="{{ entry.openedOn.date }}">{{ entry.openedOn|date }}</time> | Property | What it is |
|---|---|
date | The day as YYYY-MM-DD. |
local, utc | The day at midnight, as a time. |
iso | RFC 3339 form. |
isAllDay | Always true for a date. |
zone | UTC. |
A date field reads an old datetime value lossily (its local day) and an ISO date in a text value exactly.
datetime
A moment in time in the zone it happens in: a publication time, a start time, a deadline at noon in Paris.
fields:
- { name: doorsOpen, type: datetime, zone: Europe/London }
- { name: launchAt, type: datetime, min: "2026-01-01T00:00:00Z", seconds: true }
- { name: festivalDay, type: datetime, all_day: true } | Parameter | Type | Default | What it does |
|---|---|---|---|
zone | IANA zone | the site’s, then the locale’s, then UTC | The zone the editor offers first. A value may be in any zone. |
min | RFC 3339 instant | none | The earliest instant accepted. |
max | RFC 3339 instant | none | The latest instant accepted. |
all_day | boolean | false | Offers an all-day switch. An all-day value renders no time. Without this parameter an all-day value is refused. |
seconds | boolean | false | Edits to the second instead of the minute. |
| Code | Message |
|---|---|
zone_empty | Choose a time zone |
zone_unknown | Unknown time zone “X” |
datetime | Enter a date and time |
min_date / max_date | Must be on or after / before the bound |
all_day | This field does not accept all-day values |
Stored value:
{ "local": "2026-03-29T10:00", "zone": "Europe/Paris", "utc": "2026-03-29T08:00:00Z", "allDay": false } The wall clock (local) and the zone are what the editor chose; the UTC instant is recomputed from them on every save. In a seed file or an import you may give local and zone alone, or a bare RFC 3339 string, which is read as UTC. With index: true the instant is projected, so lists sort by it.
Template value:
| Property | What it is |
|---|---|
local | The wall-clock time in its own zone. |
utc | The same instant in UTC. |
zone | The IANA zone. |
iso | The instant in RFC 3339. |
date | The local day as YYYY-MM-DD. |
isAllDay | Whether the editor chose all day. |
inZone(name) | The instant in another zone. |
<time datetime="{{ entry.doorsOpen|iso }}">{{ entry.doorsOpen|datetime:"long short" }}</time>
{% if entry.festivalDay.isAllDay %}{{ entry.festivalDay|date:"full" }}{% endif %} |date, |datetime, |time, |relative and |iso all accept the value directly (Filters). The article type’s core publishedAt is a datetime.
recurrence
An event’s schedule: a first start and duration in a zone, an optional repeat rule (RFC 5545 RRULE), extra dates, removed dates, and changes to single instances. The core event type’s occurrences field is a recurrence, and a custom type may declare one too.
# config/schema/event.yaml
extends: event
fields:
- { name: occurrences, label: { en-GB: Dates and times } } # restate the core field to relabel it # a custom type
fields:
- { name: sessions, type: recurrence, zone: Europe/London, all_day: false } | Parameter | Type | Default | What it does |
|---|---|---|---|
zone | IANA zone | the site’s | The zone the editor offers first. |
all_day | boolean | true | Offers the all-day switch. |
The editor offers once, daily, weekly on chosen days, monthly on a date or an ordinal weekday, yearly, an end by date or count, and a custom rule for anything else, and shows the next instances as the rule is edited.
Stored value:
{
"start": "2026-03-13T19:00",
"duration": "PT2H",
"zone": "Europe/London",
"allDay": false,
"rrule": "FREQ=WEEKLY;BYDAY=FR",
"rdates": ["2026-07-04T19:00"],
"exdates": ["2026-04-03"],
"overrides": {
"5": { "status": "cancelled", "note": "Bank holiday" },
"6": { "start": "2026-04-25T20:00" }
}
} | Key | What it is |
|---|---|
start | The first start, wall clock YYYY-MM-DDTHH:MM in zone. Required. |
duration | ISO 8601 duration, such as PT2H, PT90M or P1D. |
zone | The IANA zone. Required. |
allDay | Whether instances are whole days. |
rrule | The RRULE value without the RRULE: prefix. Empty for a one-off. A rule with no end is expanded to the end of the window below. |
rdates | Extra starts, wall clock. |
exdates | Days removed entirely, YYYY-MM-DD. |
overrides | Changes to single instances, keyed by the instance’s number in the rule’s own sequence (extra dates are numbered from 100000, so adding one renumbers nothing). Each takes start, duration, status (cancelled or moved) and note. |
Validation reports each problem at its part: zone (Choose a time zone), start (Enter the first date and time), duration, rrule (The repeat rule is not valid: …), rdates[n], exdates[n] and overrides.<n>.
Occurrences. On save the engine expands the schedule into individual occurrences in local time, then converts each to UTC, for a rolling window around today: 90 days back and 540 days ahead by default (events.occurrence_window in engine.yaml), rolled forward daily. A cancelled instance keeps its row and is marked cancelled; a removed date has none.
Template value. A template never sees the rule’s expansion logic, only the materialised occurrences of the published document:
| Property | What it is |
|---|---|
next(n) | The next n occurrences that have not ended, cancelled ones left out. |
upcoming() | Every occurrence that has not ended, cancelled ones left out. |
past() | Every occurrence that has ended, cancelled ones left out. |
between(a, b) | Occurrences starting between two times, cancelled ones left out. |
all | Every occurrence in the window, cancelled ones included. |
count | How many there are in all. |
start, zone, isAllDay, rrule, duration, repeats | The schedule’s own facts; repeats is true when there is a rule or extra dates. |
Each occurrence has startsAt, endsAt (UTC), local, localEnd (wall clock), zone, date (YYYY-MM-DD), isAllDay, isCancelled, isMoved, status, note and ord (its number in the sequence).
<ul>
{% for o in entry.occurrences.next(5) %}
<li><time datetime="{{ o.startsAt|iso }}">{{ o.local|datetime:"full short" }}</time>{% if o.note %} ({{ o.note }}){% endif %}</li>
{% endfor %}
</ul> Occurrences are read for a top-level recurrence field, and they are the document’s: give a type one recurrence field. How event pages and lists read occurrences is covered in Templates.
Boolean and select
The boolean field and the select field (one option or several from a declared list, each option with a stable id and per-locale label), with parameters, validation, stored values and what templates receive.
Places
The geo field (a point), the geo_shape field (an area boundary as GeoJSON) and the address field (a country-aware postal address), with parameters, validation, stored shapes and template values.