Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/code-mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@
"description": "Time tracking HTTP API endpoints",
"source": ["src/api/TimeTrackingController.ts"],
"docs": ["docs/HTTP_API.md"]
},
{
"id": "caldav-sync",
"description": "Two-way CalDAV VTODO task sync",
"source": [
"src/services/CalDavSyncService.ts",
"src/services/CalDavClient.ts",
"src/services/CalDavSecretStore.ts",
"src/services/caldav/**",
"src/settings/tabs/caldavSection.ts"
],
"docs": ["docs/features/caldav-sync.md"]
}
]
}
117 changes: 117 additions & 0 deletions docs/features/caldav-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# CalDAV Sync

TaskNotes can keep your tasks in sync with a CalDAV task list in both directions. Tasks you create in Obsidian appear on your phone, and tasks you tick off elsewhere are reflected back in your vault.

This works with any CalDAV server that stores tasks, including Nextcloud, Apple Reminders (iCloud), Radicale and Baikal.

## How it differs from the calendar integrations

TaskNotes has two separate ways of talking to calendar servers, and they do different jobs:

- **[Calendar integration](calendar-integration.md)** (Google, Microsoft) exports tasks *as calendar events* on a time grid, and is one-way.
- **CalDAV sync** treats tasks as tasks. They land in the task list of your phone or desktop client — Reminders, Nextcloud Tasks, and so on — with a due date, a status and a priority, and changes flow both ways.

You can use both at once; they do not interfere with each other.

## Setup

1. Open `Settings -> TaskNotes -> Integrations` and turn on **Enable CalDAV sync**.
2. Select **Add account**.
3. Fill in the **Server URL**, **Username** and **Password**. Any address on your server will do — TaskNotes works its way up to the account root if you paste something more specific.
- For Nextcloud, the server URL usually looks like `https://cloud.example.com/remote.php/dav`.
- For iCloud, use `https://caldav.icloud.com` and an app-specific password generated in your Apple account settings.
- Credentials are only ever sent over `https://`, except to `localhost` for local testing.
4. Select **Discover** to find the task lists this account can reach. TaskNotes only offers lists that can actually hold tasks, so event-only calendars, read-only subscriptions and deleted calendars are filtered out. If more than one list qualifies, pick the one you want from **Selected task list**.
5. Select **Preview** under **First sync** to see what would change.
6. Turn on **Sync this account**.

Passwords are stored in Obsidian's secret storage, which is encrypted at rest where the operating system supports it. They are never written to the plugin's `data.json`.

## The first sync

The first sync is the one moment where a wrong setting is expensive, so nothing is written until you confirm it. The preview reports four numbers:

- **to upload** — tasks in your vault that the server has never seen.
- **to import** — tasks on the server with no counterpart in your vault.
- **already matching** — tasks that are linked and in agreement.
- **changed on both sides** — tasks that differ, and will be resolved by the rule below.

If those numbers look wrong — a much larger import than expected, for instance — cancel, correct the task list or the filter, and preview again.

## What gets synced

| Task property | CalDAV field |
|---|---|
| Title | `SUMMARY` |
| Due date | `DUE` |
| Scheduled date | `DTSTART` |
| Status | `STATUS` |
| Priority | `PRIORITY` |
| Completed date | `COMPLETED` |
| Tags | `CATEGORIES` |
| Recurrence | `RRULE` |
| Projects (parents) | `RELATED-TO;RELTYPE=PARENT` |
| Blocked by | `RELATED-TO` with the dependency type |
| Reminders | `VALARM` |

**The note body is not synced.** Anything already in a task's description on the server is left untouched, and your Markdown body stays in Obsidian. The same is true of attachments and any other field your other client sets that TaskNotes does not model — those are preserved exactly as they were.

### Subtasks and dependencies

In TaskNotes a subtask is a task whose **Projects** field points at its parent, and that is what gets sent as the standard `RELATED-TO` link. Your task hierarchy shows up as a real hierarchy in Nextcloud Tasks, Apple Reminders and anything else that understands subtasks.

Blocking relationships travel the same way, keeping their type and any offset, so two vaults syncing through the same list see the same dependencies.

A link can only be sent once both tasks exist on the server. If a parent is a plain note rather than a task, is archived, or belongs to a different account, the link is simply left out — nothing in your vault is changed, the hierarchy just is not visible on the server.

### Reminders

Reminders become alarms on the server, so a reminder set in Obsidian can notify you on your phone. A reminder attached to the due date fires relative to that date, one attached to the scheduled date relative to that.

TaskNotes only ever rewrites the alarms it created itself. An alarm you add in another app is left exactly as it is.

### Statuses and priorities

TaskNotes lets you define your own statuses and priorities, while CalDAV has a fixed set. TaskNotes maps between them automatically: a status marked as completed becomes `COMPLETED`, one marked as skipped becomes `CANCELLED`, and everything else becomes `NEEDS-ACTION`. Priorities are spread across the CalDAV 1–9 scale by their configured weight, and a priority with no weight is sent as no priority at all.

## Choosing which tasks sync

Each account can carry a filter, using the same conditions as the FilterBar. A task syncs to the first account whose filter it matches, so a task is never uploaded twice. An account with no filter takes every task.

This is how you keep separate lists separate: give one account a `#work` filter and another a `#personal` filter, and each syncs to its own task list on the server.

Archived tasks are never uploaded.

## Changes made in two places at once

If you edit a task in Obsidian and someone edits the same task on the server before the next sync, TaskNotes notices — it remembers the version it last saw, and the server tells it when that version is out of date. The more recently changed side wins, and the change that lost is written to the debug log so you can recover it.

Because this compares a timestamp from your computer against one from the server, it works best when both have a roughly accurate clock.

## When a task is deleted on the server

You choose what happens, per account:

- **Archive the note** (default) — the note is archived and stops syncing. Nothing is lost.
- **Keep the note and stop syncing it** — the note stays exactly as it is and is unlinked from the server.
- **Delete the note** — the note is moved to trash.

Deleting a task in Obsidian always deletes it on the server.

## Sync timing

Local edits are sent within a couple of seconds by default. You can turn that off with **Push changes immediately**, in which case they go out on the next scheduled sync.

Changes made on the server are picked up on the interval you set per account, 15 minutes by default. Each check starts by asking the server a single question — has anything in this list changed? — and stops there when the answer is no. Task lists often share a calendar with hundreds of ordinary events, and this keeps those out of the way entirely.

If a change cannot be sent because the server is unreachable, it is queued and retried in the background rather than lost.

Two commands are available from the command palette: **Sync tasks with CalDAV now**, and **Unlink all tasks from CalDAV**, which detaches every task without deleting anything. Note that the link is also what stops a task syncing twice, so syncing the same list again after unlinking gives you a second copy of every task.

## Troubleshooting

**"The server rejected those credentials."** Check the username and password. Many providers require an app-specific password rather than your account password.

**No task lists found.** The account may only have event calendars. Confirm that a task list exists on the server, and that the server URL points at the DAV endpoint rather than the web interface.

**Tasks are not syncing.** Confirm that both the global **Enable CalDAV sync** toggle and the per-account **Sync this account** toggle are on, and that a task list has been selected. Turn on debug logging under `Settings -> TaskNotes -> Misc` to see what the sync is doing.
11 changes: 11 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l

## Added

- (#811) Added two-way CalDAV sync for tasks. TaskNotes can now keep a vault in sync with a CalDAV task list such as Nextcloud, Apple Reminders, Radicale or Baikal, in both directions. Configure accounts under Settings -> TaskNotes -> Integrations. See [CalDAV Sync](https://tasknotes.dev/features/caldav-sync/).
- Each synced task stores its CalDAV identifier in frontmatter, so disconnecting and reconnecting later picks up where it left off instead of creating duplicates.
- When a task is edited in both places at once, the more recent change wins, and the change that lost is recorded in the debug log.
- Choose what happens locally when a task is deleted on the server: archive the note (default), keep it without syncing, or delete it.
- Each account can be scoped with a filter, so separate task lists can hold separate sets of tasks.
- Subtasks and blocking relationships are carried across as standard CalDAV task links, so a task hierarchy built in TaskNotes shows up as a hierarchy in Nextcloud Tasks or Apple Reminders.
- Reminders become alarms on the server, so a reminder set in Obsidian can notify you on your phone. Alarms added in other apps are left untouched.
- Passwords are kept in Obsidian secret storage rather than in the plugin's data file, and are only sent over HTTPS.
- A change that could not be sent because the server was unreachable is queued and retried rather than lost.
- Added commands to sync with CalDAV on demand and to unlink every task from CalDAV.
- Thanks to @Archetype444 for the request.
- (#2147) Added context-menu actions for recording task completion today, on the scheduled date, on the due date, or on a chosen date. The actions can be grouped in a submenu from Appearance settings. See [Completing Tasks](https://tasknotes.dev/features/task-management/#completing-tasks).
- Rescheduling a recurring task can reactivate affected completed or skipped instances after confirmation. See [Recurring Tasks](https://tasknotes.dev/features/recurring-tasks/).
- Thanks to @renatomen for the contribution.
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import globals from "globals";
const englishLocaleSentenceCaseOptions = {
allowAutoFix: true,
brands: [
"Apple Reminders",
"Baikal",
"Bases",
"Brazil",
"CalDAV",
"CORS",
"Discord",
"Farsi",
Expand All @@ -32,11 +35,13 @@ const englishLocaleSentenceCaseOptions = {
"Model Context Protocol",
"Monday",
"N/A",
"Nextcloud",
"NLP",
"Obsidian",
"Outlook Calendar",
"Persian",
"Pomodoro",
"Radicale",
"RRULE",
"Saturday",
"Slack",
Expand All @@ -45,7 +50,9 @@ const englishLocaleSentenceCaseOptions = {
"Thursday",
"Tuesday",
"UID",
"URL",
"UTC",
"VTODO",
"Wednesday",
"Yahoo Calendar",
"YYMMDD",
Expand Down
63 changes: 63 additions & 0 deletions i18n.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,67 @@
"settings.integrations.autoExport.notices.exportSuccess": "bac3306a4c636ea65338c835a9a20f014f5d5906",
"settings.integrations.autoExport.notices.exportFailure": "68f10dec3894c0eca2f183297000b6e00b638e94",
"settings.integrations.autoExport.notices.serviceUnavailable": "1b69a3770528d48e5851443f5dc568a407c0e69f",
"settings.integrations.caldav.header": "2c1d650e718980f488377ac3543381c9e2fd750e",
"settings.integrations.caldav.description": "25e1ec574445d1f6d96ebb3a5f3bb6fec8a5fe7f",
"settings.integrations.caldav.enable.name": "d4c64688b41ace0bbc732d8df7be33a71aa2ab0a",
"settings.integrations.caldav.enable.description": "8e4629cff5c9781bc2244d6cf6085f5b38a8fe20",
"settings.integrations.caldav.pushOnChange.name": "49c00c64290de59f1a0939b784fb734289be1325",
"settings.integrations.caldav.pushOnChange.description": "d1b3c0d8272bd0f970dfa6a93f526629fe56adec",
"settings.integrations.caldav.addAccount.name": "98b0ed858ae45c510636bd66d9882afa7c99c042",
"settings.integrations.caldav.addAccount.description": "d8f4f5a33c14e2c1acb627f8901d0954ab7949a0",
"settings.integrations.caldav.addAccount.button": "98b0ed858ae45c510636bd66d9882afa7c99c042",
"settings.integrations.caldav.addAccount.defaultName": "d33d781dfbf7247d752f37f7e2f2ffd29148f87c",
"settings.integrations.caldav.account.name.name": "709a23220f2c3d64d1e1d6d18c4d5280f8d82fca",
"settings.integrations.caldav.account.name.description": "1a1398f91ca8d12b0a27d9b79857cc335abfdeef",
"settings.integrations.caldav.account.serverUrl.name": "1d5d1effa999db571869970c0ccf9ad86e9d96a9",
"settings.integrations.caldav.account.serverUrl.description": "316e15f8135317c92c2a15f2340be464fe508c38",
"settings.integrations.caldav.account.username.name": "84c29015de33e5d22422382a372caba5c58f8c01",
"settings.integrations.caldav.account.username.description": "38aba9d60697e681c6265ad15499a96d718eb9e0",
"settings.integrations.caldav.account.password.name": "8be3c943b1609fffbfc51aad666d0a04adf83c9d",
"settings.integrations.caldav.account.password.description": "8671e52bd5937b25bc61bfd3c7f503adef7bef98",
"settings.integrations.caldav.account.password.stored": "64b7152505708c08b08e4a96e6eaa02ea034a173",
"settings.integrations.caldav.account.password.placeholder": "9695086041c462c6598c6dd5e35d3a2c9cbbe5f8",
"settings.integrations.caldav.account.password.clear": "719ea396ad92e01b4757ec2b93bb1e5f270f771d",
"settings.integrations.caldav.account.discover.name": "91c40abdae9e4c8b6af1dad8d8fc60d2351b4d25",
"settings.integrations.caldav.account.discover.description": "676840443d2ba378d31073cfe01100bf19760d80",
"settings.integrations.caldav.account.discover.button": "4827ea22716a74aaf8bbe499fc314bb93f73c65e",
"settings.integrations.caldav.account.collection.name": "c73016fa9b30a26886ab8bfabb06323ce3714447",
"settings.integrations.caldav.account.collection.choose": "bee9475ddafab210a55e00cad718c8406c99ebcb",
"settings.integrations.caldav.account.interval.name": "6d5fdb0573998aae458d0c115eedfc6c77172209",
"settings.integrations.caldav.account.interval.description": "b2e8a4dd84a9b3df31d0ba946636c0ebc0d296f3",
"settings.integrations.caldav.account.deletionPolicy.name": "0a537ed71b7b3d1bd545a1d0c36c3001f7d089d3",
"settings.integrations.caldav.account.deletionPolicy.description": "f6bdf8eff8da651f748a034a7aac92b303596f4d",
"settings.integrations.caldav.account.deletionPolicy.archive": "12fdfb9484c14b5d3252a98829407afe614e41db",
"settings.integrations.caldav.account.deletionPolicy.unlink": "0f71ba0a4610b69519432b0ee93679d16132cdc3",
"settings.integrations.caldav.account.deletionPolicy.delete": "81a60ef22edfc82eb4e139169cd1e3f0a3c61fb5",
"settings.integrations.caldav.account.enable.name": "f025efdef3d9739f01535d4da256f1d8047cf6f3",
"settings.integrations.caldav.account.enable.description": "c5dbda373cd00838dd6176b05e20d9a6132b2091",
"settings.integrations.caldav.account.firstSync.name": "9273e1c1da660e643a5fdbb47b59d986189d6953",
"settings.integrations.caldav.account.firstSync.description": "b91de47f60a3b2a4de1d9e100d0a6e6500aaac71",
"settings.integrations.caldav.account.firstSync.button": "f1fbb2b43dca281d0138f4fcc92543ad143ef0b1",
"settings.integrations.caldav.account.remove.name": "e556b5329b346b99d0e21a78f4409bb3171d8a76",
"settings.integrations.caldav.account.remove.description": "cc97d6383c222534df4e9b6acdaf175746ca5205",
"settings.integrations.caldav.account.remove.button": "e963907dac5cd5c017869b4c96c18021c9bd058b",
"settings.integrations.caldav.firstSync.title": "71b8dc16334debd2e95cb3fffd1f5ede47ad080f",
"settings.integrations.caldav.firstSync.summary": "3bdbc9fe0b25d307886dac1beb7db6f2ee82ee00",
"settings.integrations.caldav.firstSync.confirm": "2b7d938e6787ea92dd55e165343965957112451b",
"settings.integrations.caldav.remove.title": "e556b5329b346b99d0e21a78f4409bb3171d8a76",
"settings.integrations.caldav.remove.message": "0d53619c7d955fbc8057d48652ff850752e4d4a6",
"settings.integrations.caldav.remove.confirm": "e963907dac5cd5c017869b4c96c18021c9bd058b",
"settings.integrations.caldav.notices.missingCredentials": "7bb9ca34b6eef4809db16a78721ec15b2841fb29",
"settings.integrations.caldav.notices.credentialsNotStored": "8f1d7a482345549ef2b894160af55cb222cba1b9",
"settings.integrations.caldav.notices.noCollections": "de824dabbb41a0ef73d04aa13474d8a186fe03f1",
"settings.integrations.caldav.notices.noCollectionSelected": "74c79b855418a79782480996ec726e9e758a6ea4",
"settings.integrations.caldav.notices.discovered": "9d46f14a6d309f232b4a7677a2dc03a560444b65",
"settings.integrations.caldav.notices.authFailed": "d5031df81963839bfa97556077d58d85046e6386",
"settings.integrations.caldav.notices.connectionFailed": "f2b81a5e02bf03f5dfa0b401efa56ebe8603a252",
"settings.integrations.caldav.notices.firstSyncComplete": "5a1bfdfa0d81346803cfa799c340e90dfa4f5200",
"settings.integrations.caldav.notices.reloadRequired": "50dc6ec366c913e5fc0e592161e6c6fdd1e622ed",
"settings.integrations.caldav.notices.syncComplete": "28a45280f986c1c86e7178c6616790a0e3f22526",
"settings.integrations.caldav.notices.unlinkedAll": "fdd79bed5323161db093623ddd4e0781ab62a0a8",
"settings.integrations.caldav.unlinkAll.confirmTitle": "25a592b928743b42ad364473f692b62e5184fe06",
"settings.integrations.caldav.unlinkAll.confirmMessage": "566dbb09df41e754cd2b08c9da9888141dfcf03a",
"settings.integrations.caldav.unlinkAll.confirmText": "0dc2913c6ee9143b2534f7f3a8fe46f8a6421167",
"settings.integrations.googleCalendarExport.header": "869c551781d06c3b2556af5856a42342c1d53b96",
"settings.integrations.googleCalendarExport.description": "7bf2b738d7caa0d4b7bda1fe3d2cb8229697d053",
"settings.integrations.googleCalendarExport.enable.name": "87000ca9a51b4d7f0b4494af98ceb923fec07091",
Expand Down Expand Up @@ -1406,6 +1467,8 @@
"commands.createOrOpenTask": "17dd392d243d955d9a4fdaf9a4c29066f26c408a",
"commands.createOrOpenTaskWithTracking": "eae087fd6478e527140061f67e199dcee752e00c",
"commands.rolloverOverdueScheduledTasks": "371dd90ae93e5e2bd3270bc26af3e491de9844b4",
"commands.caldavSyncNow": "de19b0b22272cf11f5fd75cb21d73d46549763ce",
"commands.caldavUnlinkAllTasks": "c07171704f3322341f388b4358c04ee17fb2f0de",
"modals.deviceCode.title": "1b248f8b8aea742005866f499359b67ada70b86e",
"modals.deviceCode.instructions.intro": "6f588f0b7cb402c539b8814f65a4798e8da218e0",
"modals.deviceCode.steps.open": "cf9b77061f7b3126b49d50a6fa68f7ca8c26b7a3",
Expand Down
Loading