-
Notifications
You must be signed in to change notification settings - Fork 0
Sync Rules
A sync rule mirrors events from a source calendar onto a target calendar, optionally with filtering and transformation applied.
| 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 |
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.
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.
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 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.
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.
Source → target. Updates and deletes propagate; updates on the target are not mirrored back. This is the safe default.
Source ↔ target. The rule engine processes events from both calendars. Two safeguards prevent infinite loops:
-
Loop guard via
extendedProperties— every mirror skulid writes carriesskulidManaged=1, and the rule engine refuses to re-process those events. - 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.
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:
- Set Backfill last N days when saving the rule.
- 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.
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.
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.
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.
- Source: work calendar
- Target: personal calendar
- Direction:
one_way - Visibility mode: Busy for all — titles become "Busy", attendees and description are stripped, visibility private
- 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
- 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
- Filter: start hour ≥
6, start hour <12 - All-day mode:
skip
- 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.