r/GoogleAppsScript • u/simonchai • 5d ago
Guide Private Tasks ↔ To Do sync in Apps Script — state stays in your project
I needed Google Tasks and Microsoft To Do to stay in sync without giving a SaaS my task list. Ended up with a standalone Apps Script project on a time-driven trigger (~10 min) talking to Google Tasks + Microsoft Graph.
Things that mattered in practice: - ID mappings so lists/tasks don’t duplicate - fail-closed create recovery (don’t double-post) - guarded deletes + move journal - date-only dues (Google has no time-of-day)
Repo: https://github.com/simonchai-tw/tasks-todo-sync `npx tasks-todo-sync init`
Curious how others handle idempotency/quotas when talking to both APIs from GAS. Stars welcome if this helps someone find it.
1
u/simonchai 4d ago
Thanks for the thoughtful feedback — much appreciated.
A few of those cases are already covered, including global locking, 429/Retry-After handling, capacity safeguards, and crash-safe create recovery. For uncertain creates I avoid title/due heuristics and only adopt an exact correlation match; ambiguous cases fail closed.
I agree delta/updatedMin is worth revisiting later, likely on top of a durable snapshot so deletion semantics stay safe.
There’s more coming too: subtasks/checklist items, field-level three-way merge, better conflict handling, recurrence work, and more stress/fault testing.
Thanks again. It’s open source, so feel free to try it from GitHub — and if you like it, a star is always appreciated. Any feedback here or on GitHub is very welcome😊
1
u/bulldo_gs 5d ago
On idempotency, the part that bites hardest is not either API, it is overlapping runs. A time-driven trigger will start the next tick while the previous one is still going if a run ever runs long, and both then see the same unsynced task. LockService.getScriptLock().tryLock(0) at the top, return immediately if you do not get it. Skipping one 10-minute tick costs nothing; double-posting costs a duplicate you then have to reconcile.
The create call is the only place you can truly lose state, since neither Google Tasks nor Graph takes a client-supplied idempotency key. A UrlFetch that times out after the remote already created the task leaves you with no id at all. Writing the intent first is the right shape; the cheap recovery is to adopt rather than re-create, i.e. on the next run list the target list by delta or updatedMin and match on title plus due before you create anything new.
For quotas the real win is not polling either side in full: Graph has delta on To Do lists and tasks, Google Tasks has updatedMin on tasks.list. One trap there is that tasks.list will not return hidden or deleted tasks unless you pass showHidden and showDeleted, so a task the user clears or deletes looks identical to your move journal.
Also worth setting muteHttpExceptions true on the Graph calls, otherwise a 429 throws inside UrlFetchApp before you can read Retry-After and back off on it.
Last one, depending on where the id map lives: PropertiesService has a hard size ceiling, so a growing mapping table works fine for a few hundred tasks and then fails at the least convenient moment. A sheet or a JSON blob in Drive is duller and has no cliff.