Skip to content
Configuration

`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).

instanceRequired
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).
intervalOptional · Default: 5m
The time between reconciliation cycles.

notifications

Configures the operator notification channel.

webhookURLEnvOptional
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.

massDeleteFractionOptional · Default: 0.5
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.

nameRequired
The account’s unique name, referenced in rules as <account>/<calendar>.
typeRequired
Selects the provider: google or caldav.
endpointRequired (caldav only)
The CalDAV server’s base URL.
usernameEnvRequired (caldav only)
Names the env var holding the CalDAV Basic Auth username.
passwordEnvRequired (caldav only)
Names the env var holding the CalDAV Basic Auth password (an app-specific password, if the provider requires 2FA).
clientIDEnvRequired (google only)
Names the env var holding the Google OAuth client ID.
clientSecretEnvRequired (google only)
Names the env var holding the Google OAuth client secret.
refreshTokenEnvRequired (google only)
Names the env var holding a long-lived Google OAuth refresh token. Get one via meridian oauth.
identitiesOptional (caldav only)

The 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>.

nameRequired
Unique within the account; combines with the account name as <account>/<name> in rules.
pathRequired (caldav only)
The CalDAV collection path.
idRequired (google only)
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.

idRequired
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.
fromRequired
The source <account>/<calendar> reference; must be a configured calendar.
toRequired
Lists destination <account>/<calendar> references; none may equal From.
filterOptional
Selects which source events this rule mirrors. Unset matches every event in the sync window.
transformOptional
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.

weekdaysOptional
A subset of mon..sun. All-day events are matched by their date against this list alone — never subject to Window.
windowOptional
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.
timezoneOptional
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.
skipTransparentOptional · Default: false
Skips events marked transparent (“free”) at the source.
skipAllDayOptional · Default: false
Skips all-day events entirely.
skipDeclinedOptional · Default: false
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.
whenOptional
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.titleType: string
The event’s title.
event.descriptionType: string
The event’s description.
event.locationType: string
The event’s location.
event.startType: timestamp
The event’s start instant.
event.endType: timestamp
The event’s end instant.
event.durationMinutesType: int
End minus Start, in minutes.
event.allDayType: bool
True for all-day (DATE-valued) events.
event.transparentType: bool
True when the source marks the event as free.
event.statusType: string
The event’s status (e.g. confirmed, tentative, cancelled).
event.visibilityType: string
The event’s disclosure class: public, private, confidential, or empty when the source inherits its calendar default.
event.organizerType: string
The event organizer’s identifier.
event.attendeesType: list(string)
Lists attendee identifiers.
event.rsvpType: string
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.

titleOptional
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.
descriptionOptional
Follows the same rules as Title.
locationOptional
Follows the same rules as Title.
transparentOptional
Forces the shadow’s opacity regardless of the source event’s own transparency; unset copies the source’s value.
transparentForRSVPOptional
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.
remindersOptional
A list of minutes before start; an empty list means no reminders. Unset copies the source’s reminders.
colorOptional
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.
visibilityOptional
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.
Last updated on