n8n-nodes-icloud-caldav
v0.3.1
Published
n8n community node for CalDAV (RFC 4791) calendars — iCloud-first, HTTP Basic auth
Maintainers
Readme
n8n-nodes-caldav
CalDAV node for n8n: essential calendar operations against any RFC 4791–compliant server, with
Apple iCloud supported first-class. Authentication is plain HTTP Basic (username +
password / app-specific password) — no OAuth, no vendor SDK. The node speaks
PROPFIND/REPORT/PUT/DELETE directly and builds/parses iCalendar itself; the only runtime
dependency is fast-xml-parser.
Also works with Fastmail, Nextcloud, Radicale, SOGo, Synology, and other RFC 4791 servers.
Naming. The plain
n8n-nodes-caldavis taken on npm, so this is published asn8n-nodes-icloud-caldav(an unscoped name — n8n derives a node's type frompackageName.nodeName, so a dot in the package name breaks type resolution). Minimal and auditable by design, because it holds an iCloud app-specific password. Self-hosted n8n only.
Install (self-hosted n8n)
Simplest — n8n UI: Settings → Community Nodes → Install → n8n-nodes-icloud-caldav
(requires N8N_COMMUNITY_PACKAGES_ENABLED=true, the default on self-hosted).
Custom-extensions folder (from npm):
mkdir -p ~/.n8n/custom && cd ~/.n8n/custom
npm init -y # once, if there's no package.json yet
npm install n8n-nodes-icloud-caldav
# Restart n8n. The node appears as "CalDAV".Or point N8N_CUSTOM_EXTENSIONS at that directory.
Docker: install into the mounted ~/.n8n volume, or bake it into a custom image:
FROM docker.n8n.io/n8nio/n8n:latest
USER root
RUN mkdir -p /home/node/.n8n/custom && cd /home/node/.n8n/custom \
&& (npm init -y || true) && npm install n8n-nodes-icloud-caldav \
&& chown -R node:node /home/node/.n8n
USER nodeFrom source (development): npm install && npm run build, then npm link it into
~/.n8n/custom.
iCloud setup
- Server URL:
https://caldav.icloud.com - Username: your Apple ID email address
- Password: an app-specific password. Generate one at account.apple.com → Sign-In and Security → App-Specific Passwords. Never use your main Apple ID password — with two-factor enabled it will not work, and you should not hand it to anything anyway.
After authentication iCloud points at a partition host (https://pXX-caldav.icloud.com/<dsid>/…).
The node discovers principal and calendar-home URLs at runtime and follows them verbatim — no
hardcoded paths.
Credentials (CalDAV API)
| Field | Default | Notes |
| ---------- | --------------------------- | ----------------------------------------------------------- |
| Server URL | https://caldav.icloud.com | Base URL of the CalDAV server |
| Username | — | For iCloud: Apple ID email |
| Password | — | For iCloud: app-specific password — never the main password |
The credential Test runs a PROPFIND for current-user-principal and passes on
207 Multi-Status, so a green check means the server URL and login actually work.
Operations
Calendar → Get Many
Runs the discovery cascade (principal → calendar-home → PROPFIND Depth: 1) and returns each
VEVENT-capable calendar: displayName, url (collection href), ctag, and supported
components. This also feeds the calendar dropdown used by all event operations.
Event → Create
Fields: calendar (dropdown), summary, start, end, allDay, and reminders (DISPLAY
alarms, minutes before — defaults to one 15-minute reminder). description, location,
timezone (IANA), rrule, and the optional uid override (idempotent creates) live under
Additional Fields. The event is PUT with If-None-Match: * — create-only, it can
never overwrite an existing object. Returns uid, href, and etag.
Example — a bill-due reminder: summary Pay electricity, start 2026-08-01T09:00:00Z, end
2026-08-01T09:15:00Z, reminder 15 → timed event with a 15-minutes-before popup alarm.
Event → Get
By href (direct GET) or by uid (calendar-query REPORT with an exact, case-sensitive
i;octet match). Returns parsed fields plus etag and the raw ICS.
Event → Get Many
calendar-query REPORT on a calendar with an optional start/end time-range filter. Returns
one item per event: parsed fields, href, etag, raw ICS.
Example — this week's events: start 2026-07-06T00:00:00Z, end 2026-07-13T00:00:00Z.
Event → Update (ETag / If-Match contract)
CalDAV has no partial update — a PUT replaces the whole object (RFC 4791 §8.2.2). Update
therefore: fetches the current ICS from href, patches only the fields you set (attendees,
extra alarms, X- properties, VTIMEZONE survive untouched), and PUTs it back with
If-Match: <etag> using the etag you pass in — the one you got from Create/Get/Get Many.
If someone changed the event in the meantime, the server answers 412 Precondition Failed
instead of silently overwriting: re-fetch the event (fresh etag), reapply your change, retry.
The ETag field is optional — leaving it empty uses the freshly fetched ETag instead (your
fields still win, but concurrent-edit detection is weaker), so pass it when you have it.
Example — retitle an event: pass href + etag from a previous Get, set summary
Pay electricity (paid).
Event → Delete
DELETE on the event href, with If-Match when you supply the etag. Deleting an event that
is already gone (404) counts as success — deletes are idempotent and safe to retry. Enable
Fail on Missing if you want a 404 to error instead.
Timezone behavior
- Default: UTC. Start/end are converted to
DTSTART:…Z— always valid, on every server. - With
timezoneset (e.g.America/Los_Angeles) the event is written asDTSTART;TZID=…:…without an embedded VTIMEZONE component. Servers implementing RFC 7809 (time zones by reference — iCloud among them) accept this. Known limitation: strict RFC 5545 servers that insist on an inline VTIMEZONE may reject such events — use UTC there. The node never generates VTIMEZONE definitions. - A naive start like
2026-07-06T09:00:00withtimezoneset means wall-clock time in that zone; aZ/offset timestamp is converted into it. - All-day events use date-only values (
YYYY-MM-DD);endis the inclusive last day (the node emits RFC 5545's exclusive DTEND internally and maps it back on read).
Recurrence (RRULE)
rrule is passed through verbatim as a raw RFC 5545 value, e.g. FREQ=MONTHLY;BYMONTHDAY=1.
Combine it with timezone so occurrences stay on local wall time across DST changes.
Tests
Unit (offline, no credentials):
npm run test:unitCovers the pure parts: ICS building/parsing (escaping, 75-octet folding, all-day vs timed, VALARM) and the XML/href-resolution helpers.
End-to-end (real server, real account):
CALDAV_SERVER=https://caldav.icloud.com \
[email protected] \
CALDAV_PASSWORD=your-app-specific-password \
CALDAV_TEST_CALENDAR="Some Calendar" \
npm run test:e2eCALDAV_TEST_CALENDAR(optional) picks the target calendar by display name; otherwise the first VEVENT-capable calendar is used. Prefer a scratch calendar.- Exercises the real protocol modules through seven stages: auth (207), discovery, list
calendars, create (a timed and an all-day event, each with a 15-minute reminder), read-back
(time-range REPORT + UID-filtered REPORT), update (If-Match, then a deliberate stale-ETag
PUTthat must fail with 412), delete (then a repeat DELETE that must return 404). - Creates only events titled
n8n-caldav e2e … (safe to delete)and deletes them afterwards — cleanup runs even when a stage fails. The password / Authorization header is never printed. - For iCloud this needs a real account with an app-specific password (see iCloud setup).
Second-server run (Radicale in Docker) to prove generality beyond iCloud:
docker run --rm -p 5232:5232 python:3.12-alpine sh -c '
pip install --quiet radicale &&
printf "e2e:e2e-password\n" > /tmp/users &&
exec python -m radicale --server-hosts 0.0.0.0:5232 \
--auth-type htpasswd --auth-htpasswd-filename /tmp/users \
--auth-htpasswd-encryption plain'A fresh Radicale user has no calendars: open http://localhost:5232, log in as
e2e / e2e-password, create a calendar in the built-in web UI, then:
CALDAV_SERVER=http://localhost:5232 \
CALDAV_USERNAME=e2e \
CALDAV_PASSWORD=e2e-password \
npm run test:e2eTroubleshooting
| Symptom | Likely cause / fix |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401 Unauthorized | iCloud: you used the Apple ID password — generate an app-specific password at account.apple.com. Also check the username is the full email. |
| No calendars found | Wrong server URL (scheme, host, or a stray path/trailing slash), or the account has no VEVENT calendars. iCloud: exactly https://caldav.icloud.com. |
| 412 Precondition Failed | ETag conflict — the event changed since you fetched it. Re-fetch to get the current etag and retry. On Create it means the object already exists. |
| 507 Insufficient Storage | Server-side quota exceeded — free up space in the account. |
License
MIT — © 2026 Howie Lin.
