209 lines
5.4 KiB
Markdown
209 lines
5.4 KiB
Markdown
|
||
|
||
Project Summary & Learnings | March 2026
|
||
|
||
|
||
# What We Built
|
||
|
||
A one-way automation that syncs incomplete tasks from Apple Reminders ("To Do" list) to a Notion database, with deduplication to prevent duplicate entries.
|
||
|
||
|
||
|
||
## Final Architecture
|
||
|
||
Reminders app closed
|
||
|
||
→ Shortcuts automation triggers
|
||
|
||
→ Scriptable script runs (EventKit)
|
||
|
||
→ Fetches all incomplete tasks from "To Do" list
|
||
|
||
→ POST to n8n webhook
|
||
|
||
→ Code node parses + structures tasks
|
||
|
||
→ For each task:
|
||
|
||
→ Query Notion by iCloud UID
|
||
|
||
→ If not found → Create Notion page
|
||
|
||
→ If found → Skip (no duplicate)
|
||
|
||
|
||
|
||
## Data Flow Per Task
|
||
|
||
- UID (unique identifier from EventKit)
|
||
|
||
- Title
|
||
|
||
- Due date (ISO 8601 format)
|
||
|
||
- Notes
|
||
|
||
- Status → hardcoded as "To Do" on creation
|
||
|
||
|
||
|
||
|
||
## Completion Sync (Reminders → Notion)
|
||
|
||
A second Scriptable script checks for tasks completed in the last 24 hours and updates their Notion status to "Done" via a separate n8n code node routing on action: "complete".
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# Approaches Tried
|
||
|
||
|
||
|
||
## Option A — iCloud CalDAV Polling (abandoned)
|
||
|
||
The original plan was to poll iCloud's CalDAV server from n8n every 5 minutes. This is reliable and device-independent but hit a hard wall:
|
||
|
||
- Apple migrated modern Reminders lists to CloudKit (iOS 13+)
|
||
|
||
- CalDAV only exposes legacy lists — flagged as "Reminders ⚠️" in the calendar listing
|
||
|
||
- Custom lists like "To Do" created in the modern format are completely invisible to CalDAV
|
||
|
||
- No workaround exists — this is an Apple architectural decision
|
||
|
||
|
||
|
||
|
||
## Option B — Apple Shortcuts + n8n Webhook (partial)
|
||
|
||
Shortcuts has native access to modern Reminders but hit several limitations:
|
||
|
||
- No "when reminder is added" automation trigger exists in iOS
|
||
|
||
- Shortcuts bundles list variables into one concatenated string instead of firing per item
|
||
|
||
- Workarounds (Text actions, Set Variable, loop structures) all produced the same bundling behaviour
|
||
|
||
- Date + title pairing by index breaks when tasks have no due date — lists become misaligned
|
||
|
||
|
||
|
||
|
||
## Option C — Scriptable + EventKit (final solution)
|
||
|
||
Scriptable is a free iOS app that exposes native iPhone APIs including EventKit. Writing ~20 lines of JavaScript gave full programmatic access to Reminders with:
|
||
|
||
- Correct title per task
|
||
|
||
- ISO 8601 due date per task (null if not set)
|
||
|
||
- Unique identifier per task (essential for deduplication)
|
||
|
||
- Notes per task
|
||
|
||
|
||
Triggered via Shortcuts automation on Reminders app close.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# Technical Learnings
|
||
|
||
|
||
|
||
## Apple / iOS
|
||
|
||
- Modern Reminders (iOS 13+) use CloudKit — NOT CalDAV. CalDAV only works for legacy unconverted lists.
|
||
|
||
- Shortcuts cannot trigger on "reminder added" — only on app open/close or time-based.
|
||
|
||
- Shortcuts visual variable system is unreliable for extracting typed properties from list objects — it bundles values instead of iterating correctly.
|
||
|
||
- Scriptable + EventKit is the correct tool for programmatic Reminders access on iOS.
|
||
|
||
- EventKit's Reminder.allIncomplete([list]) returns full objects with all properties including identifier, dueDate, notes, completionDate.
|
||
|
||
|
||
|
||
|
||
## n8n
|
||
|
||
- n8n Cloud 2.9.4 Code nodes do not support $http, fetch, or this.getCredentials() — use helpers.httpRequest() instead.
|
||
|
||
- helpers.httpRequest() auto-parses JSON responses — do not wrap in JSON.parse().
|
||
|
||
- Test webhook URL (webhook-test/) only stays open for one call after clicking Execute Workflow — use production URL + activate toggle for persistent workflows.
|
||
|
||
- Notion node with 0 results outputs nothing downstream — IF nodes cannot branch on empty. Use a Code node with direct API calls to handle both search and create in one step.
|
||
|
||
- Custom HTTP methods like REPORT and PROPFIND are not available in the HTTP Request node dropdown — use a Code node with helpers.httpRequest() instead.
|
||
|
||
|
||
|
||
|
||
## Notion API
|
||
|
||
- Deduplication requires storing the source system's UID (iCloud UID) as a rich_text property in Notion.
|
||
|
||
- Status fields use { status: { name: "To Do" } } format — not select or text.
|
||
|
||
- Date fields require ISO 8601 format: { date: { start: "2026-03-10T00:00:00.000Z" } }.
|
||
|
||
- Integration must be explicitly connected to each database via Connections in Notion UI — otherwise API returns 401.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# Final Stack
|
||
|
||
|
||
|
||
- Reminders access: Scriptable (iOS)
|
||
|
||
- Automation trigger: Apple Shortcuts
|
||
|
||
- Workflow engine: n8n Cloud 2.9.4
|
||
|
||
- Task database: Notion API (2022-06-28)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# Future Extensions
|
||
|
||
- Notion → Reminders bidirectional sync (requires n8n polling + push notification trigger e.g. ntfy.sh or Pushover)
|
||
|
||
- Optimise to only sync tasks created/modified since last run (store last sync timestamp)
|
||
|
||
- Migrate to self-hosted n8n on Hetzner VPS (~€5/month) — migration is a half-day job: export JSON, re-authenticate credentials, update webhook URLs
|
||
|
||
- Add error handling and Slack/email alerts for failed syncs
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
Key credentials & config
|
||
|
||
n8n webhook: https://weisstrevor.app.n8n.cloud/webhook/c68fdc4a-0e56-4ae2-8c5f-14e339846cbe
|
||
|
||
Notion DB: 2872f572ad7d8089beb4f24380180a12
|
||
|
||
iCloud CalDAV: p25-caldav.icloud.com / Apple ID: 2004413068 |