Skip to content

Sync Rules

github-actions[bot] edited this page Aug 27, 2026 · 3 revisions

Sync Rules

A sync rule mirrors events from a source calendar onto a target calendar, optionally with filtering and transformation applied.

Anatomy of a rule

Field Meaning
Name Free-form label
Source calendar Where events are read from
Target calendar Where mirrored events are written
Direction one_way or bidirectional
Primary side Tie-breaker for bidirectional conflicts (source or target)
Category Optional pin (color/group on planner)
Visibility mode One of four Reclaim-style presets — see below
All-day mode sync_all / only_busy / skip
Working-hours only Mirror only events whose start lies in the source account's Working hours
Filter Title regex / color / attendee / free-busy / hour bounds
Backfill days Walk N days of history at save time (0 = forward-only)
Enabled Disabled rules are inert until you re-enable them

Visibility modes

The visibility preset drives the engine's transform. Picking a preset replaces the old per-field Transform JSON form.

Preset Mirror title Attendees Description Visibility
Personal Commitment for all "Personal Commitment" stripped stripped private
Busy for all (default) "Busy" stripped stripped private
Details for you, busy for others preserved stripped preserved private
Details for you and those with access preserved preserved preserved default

extendedProperties.private.skulidManaged="1" is always set, so the loop guard works regardless of preset.

All-day handling

A separate radio independent of the visibility preset:

  • sync_all — every all-day event mirrors (default).
  • only_busy — skip all-day events marked transparent (holidays).
  • skip — never mirror all-day events.

Working-hours only

When checked, the engine looks up the source account's Working hours and drops any event whose start time falls outside them. Useful for "only sync work meetings, not after-hours invites".

Filters

Filters are AND'd together — every condition must match. An empty filter matches every non-cancelled event. The all-day mode is a separate radio (see above) and replaces what used to be a filter field.

Filter Effect
Title regex Go regexp matched against event.Summary
Color IDs Comma-separated; matches event.ColorId
Attendee any Mirror only if any attendee email matches one of these
Free/busy busy = opaque only; free = transparent only
Start hour ≥ Local-time hour bound (rejects all-day events)
Start hour < Upper local-time bound

Cancelled events are always rejected by the filter — but they still trigger deletion of any existing mirror. So if you cancel an event on the source, the mirror disappears too.

Transforms (legacy)

Earlier versions exposed individual transform knobs (title template, strip attendees, etc.). Those are now derived from the Visibility mode preset above. Time fields, recurrence, and the original location are always preserved on the mirror.

Direction

one_way

Source → target. Updates and deletes propagate; updates on the target are not mirrored back. This is the safe default.

bidirectional

Source ↔ target. The rule engine processes events from both calendars. Two safeguards prevent infinite loops:

  1. Loop guard via extendedProperties — every mirror skulid writes carries skulidManaged=1, and the rule engine refuses to re-process those events.
  2. Etag dedup — if a webhook fires for an event whose etag matches what we last saw, the rule skips the no-op update.

Bidirectional rules use a synthetic key (rev:<event_id>) for the reverse direction's event_link row, so forward and reverse passes don't collide on the unique constraint.

Backfill

By default, rules only mirror new changes — events that already existed on the source before the rule was created are ignored. To pull in history:

  1. Set Backfill last N days when saving the rule.
  2. After save, click Backfill Nd on the rule row.

Backfill walks the source calendar from now - N days and runs every event through the rule engine once, in the background — the page comes straight back while it runs. The rule's backfill_done flag is set when complete, which swaps the button for Re-run backfill.

Re-running a backfill

Editing a rule does not reset backfill_done. Neither does changing Backfill last N days and saving again — the update statement doesn't touch the column. Once a backfill has completed, Backfill Nd is replaced by Re-run backfill, which clears the flag and walks history again in one step. It asks for confirmation first, because:

  • The second pass runs under the rule's current configuration, not the one the first pass used. A filter you have since narrowed will delete the mirrors that no longer match (audited as filter_drop).
  • It walks the full N days again against a rate-limited API. On a busy calendar with a long window that is not a cheap button.

Events that still match are updated in place rather than duplicated: the rule keeps its event_link rows across a re-run, so the engine knows which mirror belongs to which source event.

One limit worth knowing: the pass only visits events inside the new window. If you shorten Backfill last N days, mirrors created by an earlier, longer run fall outside it and are left alone — narrow the filter instead if you want them cleaned up, or delete them by hand.

Manual sync

The Sync now button on a rule enqueues an incremental sync for the source calendar's account+calendar combo. This is the same mechanism the polling fallback uses, but on demand.

Audit trail

Every action the rule engine takes lands in the audit log with kind="rule", the rule ID, the source event ID, and the action:

  • create — a new mirror was inserted
  • update — an existing mirror was updated
  • delete — the source was cancelled or the mirror no longer matches the filter
  • filter_drop — the source no longer matches the filter
  • error — the engine hit an error (message attached)
  • backfill_complete — a backfill pass finished

See Operations for how to use this.

Common patterns

"Mirror my work calendar to my personal one as 'Busy' blocks"

  • Source: work calendar
  • Target: personal calendar
  • Direction: one_way
  • Visibility mode: Busy for all — titles become "Busy", attendees and description are stripped, visibility private

"Sync events between two calendars I both edit"

  • Direction: bidirectional
  • Primary side: pick whichever you trust more (breaks the tie on conflicting concurrent edits — rare in practice)
  • Visibility mode: Details for you and those with access, so the mirror keeps everything

"Only mirror confirmed customer meetings"

  • Filter: title regex ^\\[CUST\\], attendee any = cust@example.com
  • Visibility mode: Details for you, busy for others — you keep the title and description, attendees are stripped, visibility private

"Pull only my morning events"

  • Filter: start hour ≥ 6, start hour < 12
  • All-day mode: skip

Limitations

  • No conflict UI. If a bidirectional rule sees concurrent edits on both sides, the most recent wins.
  • No event chunking. Very large calendars (tens of thousands of events) during a backfill could slow the worker.
  • Cross-account rules work but use the source account's token to read and the target account's token to write — both must be connected.

Clone this wiki locally