Skip to content

[21379] [Bug fix] Todoist bug fix#21421

Open
vetrivigneshwaran wants to merge 9 commits into
masterfrom
Todoist-bug-fix
Open

[21379] [Bug fix] Todoist bug fix#21421
vetrivigneshwaran wants to merge 9 commits into
masterfrom
Todoist-bug-fix

Conversation

@vetrivigneshwaran

@vetrivigneshwaran vetrivigneshwaran commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

closes: #21379

Describe the bug
The Todoist New Completed Task source (todoist-completed-task) does not emit an event when a recurring task is marked done. Non-recurring (one-off) task completions still emit correctly, so the trigger looks like it works, but every recurring completion is silently missed.

To Reproduce
Reproduced live against the Todoist API v1 (verified 2026-07-14). Using an authenticated Todoist account:

Create a recurring task: POST /api/v1/tasks with {"content": "recurring test", "due_string": "every day"}. Response confirms due.is_recurring: true.
Create a non-recurring control task: POST /api/v1/tasks with {"content": "normal test"} (due: null).
Complete both: POST /api/v1/tasks/{id}/close for each.
Re-fetch each task:
Recurring (GET /api/v1/tasks/{id}): checked: false, completed_at: null, completed_count: 1, and due.date has advanced to the next day. The task is NOT in a completed state — it was rescheduled.
Control (GET /api/v1/tasks/{id}): checked: true, completed_at set.
Query the exact endpoint the source polls: GET /api/v1/tasks/completed/by_completion_date?since=&until=.
Result: items contains only the control task. The recurring completion is absent, even though completed_count incremented.
For completeness, GET /api/v1/tasks/completed/by_due_date?since=...&until=... over the occurrence's due date also returns items: [] for the recurring completion.
Net: the recurring task was completed (Todoist recorded it), but it appears in neither v1 REST completed-task endpoint, so the source never emits it.

Expected behavior
Marking a recurring Todoist task as done should emit a "New Completed Task" event, matching the pre-v1-migration behavior.

Screenshots
N/A (verified via raw API responses; can attach the request/response bodies if helpful).
Additional context
Add any other context about the problem here.

Checklist

Please check the following items before your PR can be reviewed:

Versioning

  • All components updated in this PR had their version updated (0.0.1 for new ones)
  • The app updated in this PR had its package.json's version updated

New app

If this is a new app, please submit an app integration request - the PR will only be reviewed after the app is integrated.

  • The app updated in this PR is already integrated

CodeRabbit review

After the PR is opened, and if new changes are pushed, CodeRabbit will automatically review it. Do not 'mark as resolved' CodeRabbit's comments, but reply to them instead, whether you agree (and update the PR accordingly) or disagree.

  • I have addressed or acknowledged all of CodeRabbit's review comments

Summary by CodeRabbit

  • New Features
    • Completed-task events now also include recurring task completions.
    • Added activity-log retrieval support to the Todoist integration.
  • Bug Fixes
    • Improved completion polling reliability, deduplication, and emitted event metadata to reduce missed or repeated events.
    • Recurring completion collection now better tolerates activity-log access limitations.
  • Chores
    • Updated the Todoist integration package and versioned the affected Todoist actions and sources.

…ng tasks

Describe the bug
The Todoist New Completed Task source (todoist-completed-task) does not emit an event when a recurring task is marked done. Non-recurring (one-off) task completions still emit correctly, so the trigger looks like it works, but every recurring completion is silently missed.

To Reproduce
Reproduced live against the Todoist API v1 (verified 2026-07-14). Using an authenticated Todoist account:

Create a recurring task: POST /api/v1/tasks with {"content": "recurring test", "due_string": "every day"}. Response confirms due.is_recurring: true.
Create a non-recurring control task: POST /api/v1/tasks with {"content": "normal test"} (due: null).
Complete both: POST /api/v1/tasks/{id}/close for each.
Re-fetch each task:
Recurring (GET /api/v1/tasks/{id}): checked: false, completed_at: null, completed_count: 1, and due.date has advanced to the next day. The task is NOT in a completed state — it was rescheduled.
Control (GET /api/v1/tasks/{id}): checked: true, completed_at set.
Query the exact endpoint the source polls: GET /api/v1/tasks/completed/by_completion_date?since=<today>&until=<tomorrow>.
Result: items contains only the control task. The recurring completion is absent, even though completed_count incremented.
For completeness, GET /api/v1/tasks/completed/by_due_date?since=...&until=... over the occurrence's due date also returns items: [] for the recurring completion.
Net: the recurring task was completed (Todoist recorded it), but it appears in neither v1 REST completed-task endpoint, so the source never emits it.

Expected behavior
Marking a recurring Todoist task as done should emit a "New Completed Task" event, matching the pre-v1-migration behavior.

Screenshots
N/A (verified via raw API responses; can attach the request/response bodies if helpful).
Additional context
Add any other context about the problem here.
Changes in the completed task for supporting recurring task completion
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pipedream-docs-redirect-do-not-edit Ignored Ignored Jul 20, 2026 6:00pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Todoist completed-task source now detects recurring completions through the Activity Log, combines them with standard completed tasks, caps and emits events, and advances polling state only after successful emission. Shared constants and a REST client method support the polling flow.

Changes

Todoist completion event flow

Layer / File(s) Summary
Activity Log API and polling contracts
components/todoist/common/constants.mjs, components/todoist/todoist.app.mjs
Adds polling constants and exposes the Activity Log REST endpoint.
Completion retrieval and normalization
components/todoist/sources/completed-task/completed-task.mjs
Separately paginates non-recurring completions and recurring Activity Log entries, then maps both into normalized events.
Polling, deduplication, and watermark emission
components/todoist/sources/completed-task/completed-task.mjs
Uses buffered polling windows, merges and caps results, emits metadata and raw payloads, and persists the watermark after successful emission.
Component release metadata
components/todoist/package.json, components/todoist/actions/*/*.mjs, components/todoist/sources/*/*.mjs
Updates package, action, and source version metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: ashwins01

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes many unrelated version-only bumps across other Todoist actions/sources beyond the fix. Limit the PR to the completed-task source, app helper, and only necessary version bumps for touched components.
Title check ❓ Inconclusive The title is related to the Todoist fix, but it's too generic to convey the main change. Rename it to mention recurring completed-task polling, e.g. "Fix recurring Todoist completed-task events".
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the recurring-completion fix while preserving non-recurring behavior, matching #21379.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The description matches the template and includes summary, repro steps, expected behavior, and checklist; only the package.json version checkbox is still unchecked.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Todoist-bug-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/todoist/sources/completed-task/completed-task.mjs`:
- Around line 94-97: Update the catch block around getActivityLogs in the
completed-task source to return an empty list only for the documented
premium-plan 403 error. Re-throw network failures, authentication errors, rate
limits, and other unexpected errors so they remain visible to the polling flow,
while preserving the existing premium-restriction message for the handled case.
- Around line 99-145: Update getSyncResult to sort allResults by ascending
timestamp before applying MAX_RESULTS_PER_POLL, so capped retains the oldest
events in the window. Keep _nextLastDate based on the last capped event,
ensuring the watermark advances only past returned events and newer results
remain available for the next poll.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea72e9d5-f168-4de4-8d7e-844f56b5907b

📥 Commits

Reviewing files that changed from the base of the PR and between c622c98 and 34188fb.

📒 Files selected for processing (4)
  • components/todoist/common/constants.mjs
  • components/todoist/package.json
  • components/todoist/sources/completed-task/completed-task.mjs
  • components/todoist/todoist.app.mjs

Comment thread components/todoist/sources/completed-task/completed-task.mjs
Comment thread components/todoist/sources/completed-task/completed-task.mjs
code cleanup and adding correct comments

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/todoist/sources/completed-task/completed-task.mjs (1)

138-145: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Watermark gets stuck at the last event's timestamp unless the cap is reached.

If capped.length > 0 but we processed all available results (i.e. allResults.length <= MAX_RESULTS_PER_POLL), setting the watermark to the newest event's timestamp (capped[capped.length - 1].ts) prevents the watermark from advancing to the end of the polling window (dateTo). On the next poll, dateFrom will start at that old event's timestamp again, continually re-fetching it. This causes the polling window (dateTo - dateFrom) to grow indefinitely during inactive periods, which can eventually violate API date-range limits and cause redundant data fetching.

To maintain continuity (as described in the constants.mjs comments), the watermark should advance to dateTo when all events in the window have been processed, and should only be held back at an event's timestamp if the batch was capped (leaving unprocessed events behind).

🐛 Proposed fix: only hold the watermark back if the cap was exceeded
-      this._nextLastDate = capped.length
+      this._nextLastDate = allResults.length > MAX_RESULTS_PER_POLL
         ? new Date(capped[capped.length - 1].ts).toISOString()
         : dateTo;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/todoist/sources/completed-task/completed-task.mjs` around lines
138 - 145, Update the _nextLastDate assignment in the completed-task polling
flow to use dateTo whenever allResults.length is at or below
MAX_RESULTS_PER_POLL, including when capped contains events. Only use the newest
capped event timestamp when the result set exceeded the cap and unprocessed
events remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@components/todoist/sources/completed-task/completed-task.mjs`:
- Around line 138-145: Update the _nextLastDate assignment in the completed-task
polling flow to use dateTo whenever allResults.length is at or below
MAX_RESULTS_PER_POLL, including when capped contains events. Only use the newest
capped event timestamp when the result set exceeded the cap and unprocessed
events remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e2dcf0c4-d786-4441-a282-c29f673a3bed

📥 Commits

Reviewing files that changed from the base of the PR and between 9dd7e5a and ef0d418.

📒 Files selected for processing (3)
  • components/todoist/common/constants.mjs
  • components/todoist/package.json
  • components/todoist/sources/completed-task/completed-task.mjs

Comment thread components/todoist/sources/completed-task/completed-task.mjs Outdated
Comment thread components/todoist/sources/completed-task/completed-task.mjs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
components/todoist/sources/completed-task/completed-task.mjs (3)

91-91: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Bound the recurring dedupe ID to 64 characters.

The constructed ${entry.object_id}-${entry.event_date} ID has no length guard or hash. Enforce the source contract with a stable bounded-ID helper so deduplication cannot silently stop working for longer API values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/todoist/sources/completed-task/completed-task.mjs` at line 91,
Update the dedupeId construction in the completed-task source to use the
project’s stable bounded-ID helper, ensuring the resulting ID is limited to 64
characters while remaining deterministic for the same entry.object_id and
entry.event_date values.

Source: Path instructions


45-45: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use completed_at here. The /tasks/completed/by_completion_date payload exposes completed_at; task.date_completed is undefined, so ts becomes NaN and the poll can fail when the watermark is written.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/todoist/sources/completed-task/completed-task.mjs` at line 45,
Update the timestamp assignment in the completed-task mapping to parse
task.completed_at instead of task.date_completed, ensuring the completed-task
poll writes a valid watermark.

Source: Path instructions


47-47: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include date_completed in the non-recurring dedupe ID. ${task.id}-completed keys off the task, so reopening and re-completing the same task can collapse later completions. Use task.id + date_completed for the event identity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/todoist/sources/completed-task/completed-task.mjs` at line 47,
Update the dedupeId construction in the completed-task mapping to include both
task.id and task.date_completed, while retaining the completed-event context, so
separate completions after reopening receive distinct identities.

Source: Path instructions

♻️ Duplicate comments (1)
components/todoist/sources/completed-task/completed-task.mjs (1)

135-155: ⚠️ Potential issue | 🔴 Critical

Filter eligible projects before capping and advancing the watermark.

The oldest-first sort fixes the previous newest-cap loss, but filterResults() still runs after the cap. If the first 20 events belong to unselected projects and a selected-project completion is 21st, the source emits nothing for that project while advancing lastDate past it, permanently skipping the event.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/todoist/sources/completed-task/completed-task.mjs` around lines
135 - 155, Update the completed-task fetch flow so project eligibility filtering
occurs before sorting, capping, and calculating this._nextLastDate. Reuse the
logic from filterResults() or extract it into a shared helper, ensuring only
selected-project completions influence MAX_RESULTS_PER_POLL and the watermark
while preserving the existing emitResults() flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@components/todoist/sources/completed-task/completed-task.mjs`:
- Line 91: Update the dedupeId construction in the completed-task source to use
the project’s stable bounded-ID helper, ensuring the resulting ID is limited to
64 characters while remaining deterministic for the same entry.object_id and
entry.event_date values.
- Line 45: Update the timestamp assignment in the completed-task mapping to
parse task.completed_at instead of task.date_completed, ensuring the
completed-task poll writes a valid watermark.
- Line 47: Update the dedupeId construction in the completed-task mapping to
include both task.id and task.date_completed, while retaining the
completed-event context, so separate completions after reopening receive
distinct identities.

---

Duplicate comments:
In `@components/todoist/sources/completed-task/completed-task.mjs`:
- Around line 135-155: Update the completed-task fetch flow so project
eligibility filtering occurs before sorting, capping, and calculating
this._nextLastDate. Reuse the logic from filterResults() or extract it into a
shared helper, ensuring only selected-project completions influence
MAX_RESULTS_PER_POLL and the watermark while preserving the existing
emitResults() flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b206f40-7cd7-4d5f-bfa5-f7f096aad58a

📥 Commits

Reviewing files that changed from the base of the PR and between ef0d418 and d2dcf2b.

📒 Files selected for processing (1)
  • components/todoist/sources/completed-task/completed-task.mjs


return allTasks.map((task) => ({
projectId: task.project_id,
ts: Date.parse(task.date_completed),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coderabbit was correct here. It should be task.completed_at.

Use completed_at here. The /tasks/completed/by_completion_date payload exposes completed_at; task.date_completed is undefined, so ts becomes NaN and the poll can fail when the watermark is written.

projectId: task.project_id,
ts: Date.parse(task.date_completed),
summary: `Completed task: ${task.content ?? task.id}`,
dedupeId: `${task.id}-completed`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per coderabbit:

Include date_completed in the non-recurring dedupe ID. ${task.id}-completed keys off the task, so reopening and re-completing the same task can collapse later completions. Use task.id + date_completed for the event identity.

projectId: entry.parent_project_id,
ts: Date.parse(entry.event_date),
summary: `Completed task: ${entry.extra_data?.content ?? entry.object_id}`,
dedupeId: `${entry.object_id}-${entry.event_date}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per coderabbit:

Bound the recurring dedupe ID to 64 characters.

The constructed ${entry.object_id}-${entry.event_date} ID has no length guard or hash. Enforce the source contract with a stable bounded-ID helper so deduplication cannot silently stop working for longer API values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Todoist "New Completed Task" source does not emit for recurring tasks

4 participants