Microsoft Project Online retires September 30, 2026, migrate to a modern platform before it's too late.Start migration
Back to Blog
Microsoft Integration

Microsoft To Do Sync With Onplana: Bi-Directional, Per-User, On Your Phone

How Onplana's bi-directional Microsoft To Do sync works, title, due, and completion mirror both ways; description and priority push from Onplana; tasks land in a per-org 'Onplana, <org>' list. With diagrams of the sync flow, status mapping, and the 30-second loop suppression.

Onplana TeamApril 29, 20268 min read

Microsoft To Do Sync With Onplana

Project management lives on a desktop. Personal task management lives on a phone. The bridge between the two is the half-hour every PM loses each day re-typing their actually-priority-this-week tasks into their personal task list, then re-typing their progress back into the project tool.

Onplana's bi-directional Microsoft To Do sync removes that bridge. Tasks assigned to a user mirror to their personal Onplana, <org> list in Microsoft To Do automatically; checking one off in To Do moves it to DONE in Onplana; editing the title in either place updates the other.

This post covers what mirrors, what doesn't, why we drew the line where we did, and the operational details (loop suppression, multi-org, opt-in flow) that make bi-directional sync actually work in practice.

Bi-directional sync: Onplana ↔ Microsoft To Do, three fields both ways, two outbound only

The conservative bi-directional scope (and why)

Most "bi-directional sync" features fall into one of two failure modes, they sync everything (and create constant merge conflicts whenever both sides edit the same field) or they sync only one way (and fail to deliver the actual user benefit, which is editing in either place).

Onplana's To Do sync is deliberately middle-ground:

Field Both ways Onplana → To Do only Not synced
Title
Due date
Completion status
Description
Priority / importance
Subtasks
Categories
File attachments

The reasoning per row:

  • Title, due, status are the fields a user actually changes on their phone. Mirror both ways or the feature doesn't help.
  • Description and priority belong to the project record. Stakeholders write the description in Onplana; the mobile UI's typing flow isn't the right surface for a 200-word description. We push so the field is visible in To Do; we don't pull because the originating intent is on the Onplana side.
  • Subtasks are flat in To Do (one level only) and arbitrarily deep in Onplana. There's no reasonable mapping; we'd corrupt one side or the other. Skipped.
  • Categories are 25-named-list-per-plan in Microsoft Planner and org-scoped tags in Onplana. The data shapes don't reconcile cleanly. Skipped.
  • File attachments in To Do are stored as URLs to OneDrive or external services. Onplana stores them as workspace documents. The cross-mapping would be brittle. Skipped.

That's the design rationale. The blog comment thread on this post is the right place to argue for a different line; we're not married to the current scope.

The architecture (one diagram)

Bi-directional flow, Onplana mutation → push → Microsoft Graph; Microsoft change → webhook → Onplana

Two paths:

Outbound (Onplana → To Do). Every task create/update on the Onplana side fires a fire-and-forget call to pushTaskToTodo(taskId). The push function:

  1. Loads the task and verifies the assignee has To Do sync enabled and the project has To Do sync enabled (per-user + per-project flags both default sensibly)
  2. Finds-or-creates the user's Onplana, <org> list via Microsoft Graph
  3. POSTs (new task) or PATCHes (existing mirror) the To Do task with the title/due/status/description/priority payload
  4. Stamps lastSyncedAt = now() on the TodoMirror row

If the user has no Microsoft connection or the tasks scope is missing, the push no-ops silently. Outbound is non-blocking, the Onplana task save never fails because the To Do mirror failed.

Inbound (To Do → Onplana). When the user changes a To Do task (mobile, desktop: Outlook, watch, any client), Microsoft fires a Graph webhook to Onplana's public ingress:

  1. The webhook endpoint validates the clientState secret (32-byte random, set at subscription create), forged notifications are dropped silently
  2. Acknowledges 202 within 30s (Graph's hard timeout)
  3. Looks up the TodoMirror row by (userId, todoListId, todoTaskId)
  4. Loop suppression check, if lastSyncedAt is within 30 seconds, the change is the one we just pushed; skip
  5. Fetches the current state from Graph and updates the Onplana task's title/due/status only (not description, not priority, those are outbound-only)

A single user has one webhook subscription per org they're in, scoped to the Onplana, <org> list. A user in two orgs has two subscriptions and two lists.

Loop suppression, the 30-second window

The most subtle piece of the sync is making sure outbound and inbound don't echo into infinite loops. The naïve implementation:

  1. PM updates task title in Onplana → pushTaskToTodo PATCHes Microsoft → Microsoft fires updated webhook → Onplana sees title change → updates Onplana → which fires pushTaskToTodo again → which PATCHes Microsoft → which fires another webhook → …

Onplana solves this with a 30-second window:

Loop suppression, push timestamp + 30s window protects against echo

Every TodoMirror row carries lastSyncedAt, stamped on every outbound push. The webhook handler checks now() - lastSyncedAt:

  • < 30 seconds → the change Microsoft is reporting is almost certainly our own echo. Skip.
  • >= 30 seconds → the change came from the user genuinely editing in To Do. Apply.

30 seconds is conservative on purpose. Microsoft Graph webhook latency is typically 1–5 seconds end-to-end. A user who updates the same task in both surfaces inside 30s is rare; if it happens, the second update is dropped as an echo (the user can re-apply). The cost of an occasional dropped legitimate change is much lower than the cost of an infinite-loop runaway that PATCHes Microsoft 100 times per minute.

The same window catches retry storms. If our inbound handler crashes mid-update and Microsoft retries, the second delivery sees lastSyncedAt = now() from our just-completed push and skips, idempotent.

Status mapping, the asymmetric one

To Do has five status states; Onplana has five. They don't line up cleanly.

Outbound (Onplana → To Do):

Onplana To Do
TODO notStarted
IN_PROGRESS inProgress
REVIEW inProgress (To Do has no review)
DONE completed
BLOCKED waitingOnOthers

Inbound (To Do → Onplana), conservative:

To Do Onplana
notStarted TODO
inProgress (skip, don't overwrite)
completed DONE (+ progress=100)
waitingOnOthers BLOCKED
deferred (skip, don't overwrite)

The asymmetry is intentional. Inbound inProgress is dropped because To Do users routinely tap a task as "started" while running for the train; that shouldn't flip the Onplana task out of REVIEW or BLOCKED states a PM has carefully set. We only let inbound override Onplana when the To Do state carries clear authoritative meaning, not started, completed, or blocked.

The trade-off: a user who does genuinely move a task to in-progress on their phone won't see Onplana update. They'll find it stuck on TODO. We surface this in the docs (and in the blog you're reading); if it becomes a real friction point we'll revisit.

Opt-in flow, what users actually do

Three steps, one minute total:

Opt-in flow, connect Microsoft, toggle sync, automatic seeding

Step 1, connect Microsoft (skip if you already imported a Planner plan)

Settings → Accounts → Connect Microsoft → tick Tasks. The same scope that powers Planner import; users who already have a Microsoft connection for Planner already have it.

Step 2, flip the To Do sync toggle

Settings → Accounts → scroll to the Microsoft To Do sync panel. Click Enable sync. Onplana:

  1. Verifies the Tasks.ReadWrite scope is present (412 with re-consent prompt if not)
  2. Flips the user-level todoSyncEnabled flag to true
  3. Registers a Microsoft Graph subscription on the user's Onplana, <Org Name> list (creating the list if it doesn't exist)
  4. Runs the bootstrap pass, every task currently assigned to the user across the org seeds into the To Do list (capped at 500 to keep the pass quick)

If subscription registration fails for any reason, Onplana rolls back the flag, better to require a retry than have the user think sync is on while no webhook is registered.

Step 3, there is no step 3

After enable, sync runs automatically. New tasks assigned to the user push to To Do within seconds. Changes in To Do mirror back. The user does not have to do anything else.

Multi-org behaviour

Each org gets its own list. A user who works in two Onplana orgs (say Acme and Globex) and enables sync in both sees:

  • Onplana, Acme in their Microsoft To Do
  • Onplana, Globex in their Microsoft To Do

Two separate Graph subscriptions, two separate lists, complete isolation. Cross-org leakage is impossible at the subscription level, the inbound webhook handler looks up the subscription by id, finds the (userId, organizationId) pair, and scopes the mirror update to that org.

This matters for compliance. A user with confidential project data in Org A doesn't see those tasks leaking into a personal Microsoft account that's also signed into Org B's tenant. Each org is its own walled garden.

When sync stops

Three cases trigger automatic teardown:

  • User disables from Settings → Accounts, clean teardown. The Graph subscription is DELETEd, the local TodoSubscription row is removed, the existing To Do tasks stay (they become regular To Do tasks no longer mirrored).
  • Microsoft connection revoked, the renewal worker detects the dead connection on the next 6-hourly cycle. After 3 consecutive failures, the local subscription row is dropped. Re-connect Microsoft and re-enable sync to start fresh.
  • Subscription expires past Graph's 3-day cap, shouldn't happen because the renewal worker patches expiry every 6 hours, but if it does (extended outage), Graph 404s the subscription and the renewer drops the row.

In all three cases, the user's tasks in To Do stay, only the live mirror stops. The user can re-enable sync any time; the bootstrap pass on re-enable seeds whatever's currently assigned.

Stacking with the rest of the integration story

Microsoft To Do sync is the third surface in Onplana's Microsoft 365 deep integration:

The four stack: a project imported from Planner with live sync mirrors changes inbound; the per-task assignment surfaces on the assignee's phone via To Do sync; the team meets in Teams with the Onplana tab open. One Microsoft connection, one consent, four entry points.

Try it on yourself first

The fastest way to evaluate To Do sync is to enable it on your own account against a low-stakes project. Onplana's free tier covers 5 projects with full To Do sync, no upgrade needed for the test.

Things to verify in the first 10 minutes:

  • Open Microsoft To Do (mobile or desktop). Confirm the Onplana, <Org> list shows up
  • Mark a task complete in Onplana → verify it goes from notStarted to completed in To Do
  • Reassign a task to yourself in Onplana → verify it appears in To Do within seconds
  • Edit a task title in To Do → verify it updates in Onplana within ~10 seconds
  • Mark a To Do task complete → verify it goes to DONE in Onplana

If those five round-trip cleanly, sync is working as designed. If anything looks off, the What's New page is the integration changelog, and our team responds to support questions within one business day.

Create a free Onplana account · See all integrations


Related reading:

Microsoft To DoBi-directional SyncMicrosoft 365MobileOutlookOnplanaMicrosoft Graph

Ready to make the switch?

Start your free Onplana account and import your existing projects in minutes.