feat: rrule support for automations#2756
Conversation
Greptile SummaryThis PR adds RRULE scheduling as an alternative to cron expressions for automations. It introduces a new
Confidence Score: 4/5Safe to merge with one fix: saveTriggerKind silently overwrites the stored schedule time when the user toggles the trigger-kind picker back to rrule. The blur-save path correctly preserves DTSTART through buildRRuleTriggerExpr, but the kind-switch path in saveTriggerKind bypasses that logic and re-anchors the schedule to the current wall-clock time. A user who accidentally clicks away from Custom and back will silently shift their automation firing time. useAutomationSettingsAutoSave.ts — saveTriggerKind needs to use the stored DTSTART from automation.triggerConfig when switching back to rrule mode.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/renderer/features/automations/useAutomationSettingsAutoSave.ts | saveTriggerKind uses raw rruleExpr instead of buildRRuleTriggerExpr, silently re-anchoring the stored DTSTART (schedule time-of-day) to the current moment whenever the user toggles the trigger-kind picker back to rrule. |
| apps/emdash-desktop/src/shared/core/automations/validation.ts | Core of the feature: adds rrule parsing, normalization (DTSTART injection), next-run computation, and validation. DST two-pass offset averaging and year zero-padding were addressed from previous threads; cron fallback of formatTriggerScheduleLabel returns raw pattern instead of human-readable label. |
| apps/emdash-desktop/src/renderer/features/automations/rrule-form-utils.ts | New utility: getStoredDtstartLine, getEditableRRuleExpr, and buildRRuleTriggerExpr correctly preserve stored DTSTART when serializing user edits back to the canonical rrule string. |
| apps/emdash-desktop/src/renderer/features/automations/useAutomationFormState.ts | Adds triggerKind and rruleExpr state; triggerConfig computed value correctly goes through buildRRuleTriggerExpr to preserve DTSTART on blur-saves from the settings panel. |
| apps/emdash-desktop/src/main/core/automations/repo.ts | Replaces Cron-based getNextRunAt and assertValidCronTrigger with the unified getNextTriggerRunAt and assertValidTrigger; adds normalizeTriggerConfig on create/update paths as a server-side safety net. |
| apps/emdash-desktop/src/renderer/lib/CronPicker/CronPicker.tsx | Extends CronPicker with an optional custom slot (selected state + content node); hides period-specific controls and parse-error banner when custom is active; clean extension with no breaking changes to existing callers. |
| apps/emdash-desktop/src/renderer/features/automations/components/AutomationSettingsFields.tsx | Injects the 'Custom' rrule option into CronPicker via the new custom prop; wires setTriggerKind, onTriggerKindChange, and onRRuleExprBlur correctly to the parent handlers. |
| apps/emdash-desktop/src/shared/core/automations/validation.test.ts | New test file covering normalization, next-run computation, DST timezone handling, expired finite schedules, label formatting, and invalid rrule rejection — good coverage of the happy and failure paths. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as AutomationSettingsFields
participant FS as useAutomationFormState
participant AS as useAutomationSettingsAutoSave
participant V as validation.ts
participant Repo as repo.ts (main)
Note over UI,Repo: User edits RRULE expression and blurs field
UI->>AS: "onRRuleExprBlur -> saveCurrentTrigger()"
AS->>FS: read triggerConfig via buildRRuleTriggerExpr (preserves stored DTSTART)
AS->>V: normalizeTriggerConfig(triggerConfig)
AS->>V: assertValidTrigger(activeTrigger)
AS->>Repo: "updateSettings({ triggerConfig })"
Repo->>V: normalizeTriggerConfig - no-op if DTSTART present
Repo-->>AS: saved Automation
Note over UI,Repo: User switches trigger kind to RRULE via dropdown
UI->>FS: setTriggerKind('rrule') [state update queued]
UI->>AS: "onTriggerKindChange('rrule') -> saveTriggerKind('rrule')"
AS->>FS: read raw rruleExpr (no DTSTART)
AS->>V: "normalizeTriggerConfig injects DTSTART=now()"
AS->>Repo: "updateSettings({ triggerConfig })"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as AutomationSettingsFields
participant FS as useAutomationFormState
participant AS as useAutomationSettingsAutoSave
participant V as validation.ts
participant Repo as repo.ts (main)
Note over UI,Repo: User edits RRULE expression and blurs field
UI->>AS: "onRRuleExprBlur -> saveCurrentTrigger()"
AS->>FS: read triggerConfig via buildRRuleTriggerExpr (preserves stored DTSTART)
AS->>V: normalizeTriggerConfig(triggerConfig)
AS->>V: assertValidTrigger(activeTrigger)
AS->>Repo: "updateSettings({ triggerConfig })"
Repo->>V: normalizeTriggerConfig - no-op if DTSTART present
Repo-->>AS: saved Automation
Note over UI,Repo: User switches trigger kind to RRULE via dropdown
UI->>FS: setTriggerKind('rrule') [state update queued]
UI->>AS: "onTriggerKindChange('rrule') -> saveTriggerKind('rrule')"
AS->>FS: read raw rruleExpr (no DTSTART)
AS->>V: "normalizeTriggerConfig injects DTSTART=now()"
AS->>Repo: "updateSettings({ triggerConfig })"
Reviews (3): Last reviewed commit: "fix(automations): preserve RRULE DTSTART" | Re-trigger Greptile
Description
adds RRULE automation schedules
keeps existing cron schedules compatible
tracks cron vs rrule in telemtry
this enables more flexible automation schedules
Screenshot/Recording (if applicable)
https://cap.link/vs9dpc0qjqffy1b
Checklist
messages and, when possible, the PR title