Johannes Kaindl187 downloadsMirror CalDAV events and CardDAV contacts as notes; the server stays the source of truth.
🇬🇧 English · 🇩🇪 Deutsch
Mirror CalDAV events and tasks and CardDAV contacts as notes in your vault — the server stays the source of truth, and every change back to it goes through an explicit command.

Calendars, tasks and contacts usually live on a server you never see from inside Obsidian — not linkable, not queryable, not part of your vault. This plugin mirrors CalDAV calendars and CardDAV address books as Markdown notes, one note per event, task or contact, kept in sync on an interval. The mirror is read-only by convention: editing a note's frontmatter by hand does not write to the server. Changes go back through explicit commands — move an event, change a phone number, add an attendee — each shown as a diff before anything is sent.
%%dav:begin%%…%%dav:end%%
body block are kept in sync; everything else in the note — your own notes, links, whatever
you add — survives every sync untouched.app.plugins.plugins["calendar-notes"].api,
versioned) — events/contacts as data, commands as LLM tool definitions, a plan() →
execute() step for anything that writes. See docs/API.md.calendar-notes, otherwise as an .ics file you copy or save yourself.isDesktopOnly is not set).Updates then arrive like any other community plugin update.
AnySource Sideloader installs and updates plugins from any git forge, independent of the Community Store. Add this repository as a source: https://git.jkaindl.de/jkaindl/calendar-notes, then install Calendar and Contact Notes and enable it.
Download main.js, manifest.json and styles.css from the latest Forgejo release and copy them into your vault. Releases from 0.1.10 on also ship checksums.sha256, so you can verify what you downloaded with shasum -a 256 -c checksums.sha256.
cp main.js manifest.json styles.css "<your-vault>/.obsidian/plugins/calendar-notes/"
Then: Obsidian → Settings → Community plugins → reload → enable Calendar and Contact Notes.
git clone https://git.jkaindl.de/jkaindl/calendar-notes
cd calendar-notes && npm install && npm run build
# main.js manifest.json styles.css → <vault>/.obsidian/plugins/calendar-notes/
Repository: git.jkaindl.de/jkaindl/calendar-notes
Contacts (default), Events (default), writing to Contacts//Events/); under Profiles you can edit one as JSON,
import/export, or generate one from an existing note's frontmatter shape (Create from note,
or the command Create mapping profile from active note) — handy if your vault already has a contact/event structure you
want to keep.
If a collection's target folder already has notes — migrated from another system, written by
hand — run Adopt existing notes… (or the button next to a collection). The plugin proposes
matches between server entries and your notes (email/phone exact, name/title fuzzy) at three
confidence levels — sure and likely link by default, weak is left for you to confirm —
shown in a table you can override row by row before anything is written. Linked notes get their
dav_uid/dav_source fields set and start updating in place on the next sync instead of
generating a duplicate.
![]()
Click the preview for full size
The middle column is what you type in the command palette; on a German Obsidian the commands appear under their German names instead.
| Command | In the command palette | What it does |
|---|---|---|
| Sync everything | Sync all collections |
Runs a real sync across every enabled collection |
| Preview | Preview sync (dry run) |
Shows planned operations without writing |
| Sync one collection | Sync one collection… |
Picks a single collection to sync |
| Adopt existing notes | Adopt existing notes… |
Opens the adoption review for a collection |
| Profile from note | Create mapping profile from active note |
Derives a mapping profile from the active note's frontmatter |
| Change event/contact | Change event/contact… |
Suggests applicable commands for the active note, opens a form, shows a diff |
| New event | New event… |
Creates an event on the server (and its note) via a form |
| New contact | New contact… |
Creates a contact on the server (and its note) via a form |
| Undo | Undo last change |
Reverts the target's last recorded change |
| Sync tasks | Sync tasks with the server |
Shows every difference between your task notes and the server and asks per row which side wins |
| Push hand edits | Write hand edits to the server |
Diffs your manual frontmatter edits against the server and offers to write them |
![]()
Click the preview for full size
| Setting | What it does | Default |
|---|---|---|
| Accounts | Server URL, username; password in Obsidian's secret storage | — |
| Calendars & address books → Mirror as notes | Whether a discovered calendar/address book is synced | off |
| Calendars & address books → Profile | Which mapping profile a collection uses | the matching default profile |
| Calendars & address books → Different target folder | Overrides the profile's folder for this collection only | profile's folder |
| Gap between runs (desktop, minutes) | Minutes between automatic syncs | 15 |
| Gap between runs (mobile, minutes) | Minutes between automatic syncs on mobile | 60 |
| Days back | How far into the past events are mirrored (older ones are archived, not deleted) | 90 |
| Days ahead | How far into the future events are mirrored | 365 |
| Wait after startup (seconds) | Seconds after Obsidian loads before the first sync runs | 10 |
| Language of the plugin's texts | UI language: Automatic (follows Obsidian), English, German | Automatic |
The server is the source of truth; the note is a mirror. Every sync pulls the server's
current state — via sync-token or ctag/etag comparison, whichever the server supports — and
turns it into a plan: create a note, update its managed fields, archive it (fell outside the
sync window), or send it to Obsidian's trash (deleted on the server, unless the note has
backlinks or content you added, in which case it's marked dav_state: deleted instead so
nothing you wrote is silently lost). Editing a note's frontmatter does not write to the
server — there is no merge, in either direction, outside of an explicit command.
Writing goes the other way, deliberately narrow: a command takes the last-synced server
object, applies one mutation (move a date, change a field, add an attendee), and sends a PUT
with If-Match: <etag>. A conflicting change on the server (412) is surfaced as "sync the
fresh version and try again," never silently overwritten. Every command shows its diff before
it runs, and the previous state stays in a per-object history — Undo last change reverts
it. This is why editing a note by hand never reaches the server: a mirror that could be pushed
back by editing text would need a bidirectional merge, and this plugin deliberately doesn't
have one (see Limits).
Other plugins can read events/contacts, get the plugin's commands as LLM tool definitions, and
write through the same plan() → execute() step the UI uses — never bypassing the diff, and
the API itself never opens a modal (confirmation is the caller's job). Full reference,
versioning rules and a mail-transport integration contract: docs/API.md.
.ics file to send yourself.Write hand edits to the server is the one deliberate exception — a diff you review and confirm, not a silent
merge).npm install
npm run gate # lint + typecheck + unit tests + build — the full pre-commit check
npm test # unit tests only
npm run test:integration # starts a disposable Radicale via uvx, runs integration tests
Architecture, milestone-by-milestone status and the manual smoke procedure:
AGENTS.md. GUI smoke checklist (runs against a live, running Obsidian via CDP):
docs/SMOKE.md.
LICENSE).LICENSE-DOCS).ical.js (MPL-2.0, iCalendar/vCard
parsing), fast-xml-parser (MIT,
WebDAV multistatus parsing).THIRD-PARTY.md. A commercial license is
available on request if AGPL-3.0's copyleft doesn't fit your use case: LICENSING.md.Copyright © 2026 Johannes Kaindl.