Appearance
date: 2026-06-20 tags: [todoist, webhook, scheduler, reconcile, reliability] status: active graduated_to:
Webhook-only state drifts silently — pair every webhook with a periodic reconcile
Symptom — A task created in Todoist showed in triage but was never classified (no labels, no priority, "0 min"), with no error and no notification. ("Came through but didn't get processed.")
Root cause — Classifying a Todoist-created task is triggered only by the item:added webhook. Any reason that webhook doesn't land or isn't accepted — a TODOIST_WEBHOOK_SECRET mismatch (401, dropped), a stalled queue worker, an outage at create time — leaves the task unprocessed forever. Nothing watched for the gap: BaseJob::failed only fires for a dispatched job that errors, so a never-delivered webhook leaves zero trace.
Fix — Added an hourly ReconcileTasksJob + ReconcileUnprocessedTasks action (PR #611) that finds every active Todoist task with no ProcessedTask ledger row, re-runs the pipeline, and pushes a notification when it catches any — so the gap surfaces instead of staying silent. The same shape already existed for the @someday count (RefreshSomedayCountJob): events flush on in-app changes, a periodic job re-fetches to catch out-of-app drift.
Guard — ReconcileUnprocessedTasksTest / ReconcileTasksJobTest. Convention: any state that depends solely on a Todoist webhook needs a periodic reconcile backstop (webhooks drop silently — see also 2026-06-14-paginate-todoist-list-absent-not-deleted.md). Now at two instances (RefreshSomedayCountJob, ReconcileTasksJob) — near graduation to php-laravel.md if a third appears.