The Todoist calendar-sync duplicate loop

The short answer

If you use Todoist's two-way Google Calendar sync AND subscribe a calendar as an assignment feed, you can build an accidental loop: the calendar sync mirrors dated tasks into the calendar as events, the feed subscription re-imports those events as brand-new tasks, and your list floods with duplicates of your own assignments. We have tripped this loop ourselves — twice — while building Margin, which is why Margin detects and skips events it originally created. If it happens to you: disconnect one direction of the loop first, then clean up the duplicates.

What you see

Both times we hit this while building Margin, the pattern was the same. You set up a tidy system: assignments land in Todoist, Todoist pushes them onto your calendar so you can see the week, and a calendar subscription pulls dated things back into Todoist so nothing gets lost. It works for a day. Then the same assignment appears twice, then four times, then a scrolling wall of near-identical tasks sharing one due date. In our case the titles drifted a little with each pass, because the tool writing them stamps a source label on the front of the task name and the round-trip re-stamped it ("D2L · X" becoming "D2L · D2L · X"). Completing one duplicate did nothing, because the next sync cycle minted more.

The tell we went on: the duplicates were copies of our own tasks, not of anything a school had published. A feed problem on the school's side produces duplicates that all trace back to the school's calendar. A loop produces duplicates that trace back to you.

Why it happens

Two syncs that are each reasonable on their own point at each other.

Todoist's calendar integration is bidirectional. Its help documentation describes tasks with a date, a time and a duration syncing to the connected calendar as events — and tasks carrying only a date syncing too, once "Sync all-day tasks" is turned on. Task name maps to event name; task date maps to event start. So a dated Todoist task becomes a calendar event.

The second leg is a feed subscription. Calendar services publish a secret iCal address for a calendar, and task tools can subscribe to that address to import dated items. If the calendar you subscribe to is the same calendar Todoist is writing your tasks into, the tool reads Todoist's mirrored event and, having no reason to think otherwise, treats it as a new item and writes it back into Todoist. That new task is itself dated, so Todoist mirrors it to the calendar, and the cycle repeats. Each pass around the loop adds another copy.

Nothing here is a malfunction in either product. Each half does exactly what it says. The loop only exists because the output of one is quietly wired into the input of the other, and neither end can tell that the item it just received is a reflection of something it sent.

A related detail: Todoist's own project calendar feed is explicitly one-way. Its documentation states that the project calendar feed "creates a one-way sync between a specific Todoist project and your calendar," so subscribing to that feed from your calendar app does not by itself create a loop. The loop on this page needs the two-way calendar integration on one side and a feed-consuming task tool on the other. That is not the only shape a loop can take, though: a one-way feed still closes a circle if whatever consumes it writes the imported items back into the same project the feed reads from. The rule is about the wiring, not about which feature is one-way — if output reaches input, it loops.

What you can do about it

Break the circuit before you clean up. If you delete duplicates while both syncs are live, the next cycle replaces them and you will do the work twice. Pick one leg and cut it:

Then clean up. Todoist's search and filters will find the duplicates by their shared name; clear them together rather than one at a time. Check completed tasks too, if you had been completing duplicates as they arrived.

Then rebuild deliberately, with one direction per calendar. The safe shape is a dedicated calendar for each direction: let Todoist mirror into a calendar nothing subscribes to, and subscribe only to calendars Todoist never writes into — your school's published feed, for instance. Mixing both roles into one calendar is what closes the circle.

Evidence

Todoist Help Center, "Use the Calendar integration" (retrieved 2026-07-30): the calendar integration syncs tasks that have a future date, time and duration; tasks with only a date sync when "Sync all-day tasks" is enabled. Field mapping: task name to event name, task date to event start date, task time and duration to event start and end. Edits propagate in both directions, with documented exceptions for single-instance event edits and complex custom recurrences.

Todoist Help Center, "Add a Todoist calendar feed" (retrieved 2026-07-30): "The project calendar feed creates a one-way sync between a specific Todoist project and your calendar." Completed tasks are removed from the feed. Issuing a new API token turns off all Todoist calendar feeds.

Margin-repo, extension/sync-ical.js (retrieved 2026-07-30): the iCal ingest path carries a provenance guard that refuses to re-ingest Margin's own writes. Events matching Margin's origin marker are skipped entirely — not imported, not written back, and not counted in the completion diff — which breaks the loop at the one point Margin's own writes would re-enter. The predicate lives in extension/sanitize.js as marginOriginVia.

Lived experience, not theory: the Margin project hit iCal feedback loops in its own Todoist account twice during development. Both were traced, severed, and cleaned up; the guard above is the fix that came out of them.