`rules.yaml` configuration reference
meridian validate --config rules.yaml runs the full parse + reference + CEL/template compilation this page describes, without touching any provider or credential.
Top level
The root of rules.yaml: instance identity, sync interval, operator notifications, provider accounts, and sync rules. Parsing is strict — unknown fields are startup errors — and secrets never appear here: account fields name environment variable identifiers, resolved at startup from wherever the deployment sources them (a Kubernetes Secret, .env locally).
instance- This Meridian instance’s identity, embedded in every
ownership marker it writes. Must be unique across any instances
sharing a destination, and stable for the instance’s lifetime —
changing it orphans every shadow the instance previously wrote (they
become invisible to it;
meridian wipe --instance <old>cleans them up). interval- The time between reconciliation cycles.
notifications
Configures the operator notification channel.
webhookURLEnv- Names the env var holding a webhook URL to POST
notifications to, as
{"content": "..."}— Discord’s incoming-webhook shape, so one of those URLs needs nothing in front of it. Empty/unset disables notifications, which is the normal setup under Kubernetes: guard triggers and auth failures emit metrics and logs either way, and those are what an alerting stack is already watching.
guards
Tunes the hardening guards. Everything here governs detection, not blocking: a guard records what it saw and the cycle proceeds.
massDeleteFraction- Triggers the mass-delete guard (meridian_guard_triggers_total{guard=“mass_delete”}) when a rule deletes more than this fraction of its shadows in one cycle. The trigger also sends a notification when one is configured, but the metric and the log fire either way. 0 disables the guard.
accounts
One provider connection. Type-specific fields are validated per type; setting a field that belongs to the other type is a startup error.
name- The account’s unique name, referenced in rules as
<account>/<calendar>. type- Selects the provider: google or caldav.
endpoint- The CalDAV server’s base URL.
usernameEnv- Names the env var holding the CalDAV Basic Auth username.
passwordEnv- Names the env var holding the CalDAV Basic Auth password (an app-specific password, if the provider requires 2FA).
clientIDEnv- Names the env var holding the Google OAuth client ID.
clientSecretEnv- Names the env var holding the Google OAuth client secret.
refreshTokenEnv- Names the env var holding a long-lived Google OAuth
refresh token. Get one via
meridian oauth. identitiesThe calendar addresses this server knows the account owner by, used to read the owner’s own RSVP off an invitation. Google needs none — the API marks the owner’s attendee entry — so this is CalDAV-only, where iCalendar has no such marker. Usually one address;
meridian identities <account>asks the server for it.Only addresses belonging to THIS account. A calendar mirrored in from elsewhere carries the owner’s address on that other system, which the server won’t report; add it here by hand to read RSVP off such a calendar.
accounts[].calendars
Names one calendar of an account. The logical reference
used in rules is <account>/<calendar>.
name- Unique within the account; combines with the account name
as
<account>/<name>in rules. path- The CalDAV collection path.
id- The Google Calendar ID: “primary”, or “xxxx@group.calendar.google.com” from Settings -> Integrate calendar.
rules
One sync rule (compiled into sync.Rule). A destination calendar can be targeted by any number of rules simultaneously — ownership is scoped by instance + rule ID, so rules never fight over each other’s shadows.
id- Unique across the whole config, embedded in every shadow’s ownership marker. Renaming a rule is equivalent to deleting it and creating a new one: old shadows get swept as orphans, new ones get created fresh.
from- The source
<account>/<calendar>reference; must be a configured calendar. to- Lists destination
<account>/<calendar>references; none may equal From. filter- Selects which source events this rule mirrors. Unset matches every event in the sync window.
transform- Computes the shadow’s content. Unset mirrors every field faithfully.
filter
Selects source events. All set fields AND together: an event must satisfy every one to match.
weekdays- A subset of mon..sun. All-day events are matched by their date against this list alone — never subject to Window.
window- A daily time-of-day range (“HH:MM-HH:MM”, must not cross midnight), evaluated in Timezone. An event matches if it overlaps the window on a matching weekday.
timezone- An IANA zone name, required whenever Window or Weekdays is set — event times are UTC instants internally, so this is what makes the wall-clock window meaningful.
skipTransparent- Skips events marked transparent (“free”) at the source.
skipAllDay- Skips all-day events entirely.
skipDeclined- Skips invitations the calendar owner declined. Events the owner was never invited to are unaffected, so this never touches ordinary non-meeting events. CalDAV sources need account.identities for the owner’s response to be readable at all.
when- A CEL expression over the CELEvent schema, ANDed with every other set field. Compiled and type-checked at config load — a bad expression is a startup error, never a mid-sync surprise.
CEL event schema
The documented, versioned event schema exposed to filter.when expressions. Field names come from the cel tags; this is a public API — extend, never rename.
event.title- The event’s title.
event.description- The event’s description.
event.location- The event’s location.
event.start- The event’s start instant.
event.end- The event’s end instant.
event.durationMinutes- End minus Start, in minutes.
event.allDay- True for all-day (DATE-valued) events.
event.transparent- True when the source marks the event as free.
event.status- The event’s status (e.g. confirmed, tentative, cancelled).
event.visibility- The event’s disclosure class: public, private, confidential, or empty when the source inherits its calendar default.
event.organizer- The event organizer’s identifier.
event.attendees- Lists attendee identifiers.
event.rsvp- The calendar owner’s own response to an invitation: needsAction, accepted, declined or tentative. Empty when the owner has no attendee record, which is every event that isn’t an invitation — test for a specific value, never for “not accepted”, or the expression also catches ordinary non-meeting events.
transform
Computes the shadow’s content. Every field left unset copies the corresponding source value (faithful mirror by default); it only needs to name what should differ. Deliberately not expression-powered the way filter.when is: a bad filter mis-selects events, but a bad transform corrupts calendar data, so string fields get templating and nothing more.
title- A literal string or a Go template (https://pkg.go.dev/text/template) over the source event; the literal “drop” empties it instead of copying or templating it.
description- Follows the same rules as Title.
location- Follows the same rules as Title.
transparent- Forces the shadow’s opacity regardless of the source event’s own transparency; unset copies the source’s value.
transparentForRSVP- Lists the responses (needsAction, accepted,
declined, tentative) whose copies stop blocking time, so a meeting
still sitting unanswered shows up without holding the slot. Any other
response keeps the source’s own transparency, and an event the owner
was never invited to is untouched. Mutually exclusive with
transparent, which forces one answer for every event. reminders- A list of minutes before start; an empty list means no reminders. Unset copies the source’s reminders.
color- Has no source value to copy, so unset means no override at all, not “mirror the source’s color”. Encoding is destination-provider-specific — see the rules cookbook.
visibility- Sets who may read the shadow: public, private or confidential. Unset mirrors the source. Independent of Transparent, which governs whether the shadow blocks time rather than who sees it. Not templated: a closed set is worth more here than an expression free to produce a value no provider accepts.