Todoist Cleanup - Batch Review

SkillProductivity

Use when David wants to clean up, review, or triage his Todoist tasks. Batch review process - 4 tasks at a time, David decides, AI executes.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Todoist Cleanup - Batch Review skill

What this skill tells your AI

The instructions your AI receives, as published by matyasstoch/david-skills in skills/organize-tasks/SKILL.md and read by ahel’s review.

Goal: Review every active task with David. He decides - AI presents and executes.

How David Uses Todoist

The Inbox is a brain dump - tasks, ideas, frustrations, observations, notes all land here. This is by design, not a problem. Triage is where signal gets separated from noise. Never comment on this pattern or suggest changing it.

Rules

  • No bulk-deleting, auto-triaging, or judgment calls. AI has <1% of context.
  • Present tasks exactly as written. David says keep, delete, complete, move, or delegate.
  • TXT reminders: When converting a Todoist task to a .txt reminder file, ALWAYS delete the Todoist task immediately after confirming the file was created. One atomic operation.
    • Business reminders -> business/reminders/
    • Personal reminders -> personal/reminders/

Steps

Step 1: Confirm scope

Fetch scope is defined by David at session start. Ask if unclear. Examples:

  • "Inbox only" -> project_id <INBOX_PROJECT_ID>, no date filter.
  • "Today across 4 main projects" -> Personal, Business, Wisdom, David, due == today.
  • "Today + overdue everywhere" -> all 5 projects, due <= today.

Project IDs and the fetch pattern live in the todoist skill. Never assume scope - confirm first.

Track a session set of task contents David has said "keep/skip" on - these stay in Todoist but should not be re-presented.

Step 2: Fetch + present 4

Fetch the scope from Todoist. Filter out anything in the session's kept/skipped set. Sort: P4 first, then P3, P2, P1.

Show progress (handled this session / total remaining in scope), then the current batch (numbered 1-4), a separator, then a preview of the next batch (numbered 5-8). Use identical formatting for both - David reads them side by side and any styling difference makes the preview harder to scan:

**12 handled, 28 remaining**

**Current batch:**

1. **P4** task text
2. **P2** task text
3. **P1** task text
4. **P4** task text

---

**Preview of next batch:**

5. **P4** task text
6. **P3** task text
7. **P2** task text
8. **P1** task text
  • Restart numbering from 1 each batch; preview continues from 5-8.
  • No project name. Priority + task text only.
  • David responds only to 1-4. Preview is for him to start thinking about the next batch in parallel.

Step 3: Execute David's calls

For each of the 4, David gives a disposition. Execute immediately in Todoist:

  • Keep / Skip: No Todoist changes. Add task content to session kept/skipped set.
  • Delete: Delete from Todoist.
  • Complete: Close task in Todoist.
  • Move: Move to target project in Todoist.
  • Delegate: Move to team member's project.
  • Reprioritize: Update priority in Todoist.
  • Set due date: Update due date in Todoist.
  • Convert to reminder: Create .txt file, delete Todoist task.
  • Add label: Update labels in Todoist (e.g. "labels": ["reminder"]). Common label: reminder for self-nudge tasks.
  • Compound: David often gives multiple actions at once, e.g. "into Business, p2, today" or "into Personal, p3, today, label 'reminder'" = move + set priority + set due date + add label. Execute all parts together - update priority, due date, and labels first, then move (move changes the task ID).

Step 4: Re-fetch and advance

Once all 4 are handled, re-fetch the scope from Todoist (fresh state, latest IDs), filter out the kept/skipped set, present the next 4 plus preview. No confirmation needed. Repeat until no tasks remain in scope.

ALWAYS re-fetch before every batch - David often edits Todoist directly in the web/mobile app between batches (adds, deletes, reprioritizes, reschedules). Never present from cached data. One fresh fetch per batch is non-negotiable.

Step 5: Wrap up

Summarize totals (kept, deleted, completed, moved, delegated). Report final task count remaining in Todoist.

Execution Discipline (LEARNED THE HARD WAY - READ EVERY TIME)

Every operation MUST go through a script file (write_file to /tmp/todoist_*.sh, then bash). NEVER run inline curl/jq in the terminal.

  1. Auth-token masking corrupts inline commands. A security filter rewrites the literal string Authorization: Bearer $TOKEN and can silently break the command. Workaround that survives the filter:

    TK=$(grep -E '^TODOIST_API' .env | cut -d= -f2- | tr -d '"' | tr -d "'" | tr -d ' ')
    WORD="Bea""rer"
    HDR="Authorization: ${WORD} ${TK}"
    curl -s ... -H "$HDR" ...
    

    After write_file, read_file the script to confirm the auth line is intact.

  2. curl exits 0 even on 401/auth failure. Capture the response body and check it with jq. A success body has the task object; a failure has an error/null. Never report a disposition as done without seeing the updated object.

  3. Don't invent priority errors. P3 = API value 2. If David says P3 and you send priority:2, that is correct. Re-check the Priority Trap table before fixing anything.

Content Matching with Special Characters

David's brain-dump tasks often contain smart quotes, em-dashes, and ellipses. Exact full-content jq matching is fragile. Match on a safe ASCII prefix with startswith, not the full string.

findid() { echo "$INBOX" | jq -r --arg s "$1" '.results[] | select(.content | startswith($s)) | .id' | head -1; }
ID=$(findid "Team member is right")
ID=$(findid "list out all things I")

Recurring Tasks via due_string

When David asks for a recurring disposition ("every 14 days", "every 4 days"), set it through due_string, NOT a separate field. Combine recurrence + start date in one string:

  • "due_string":"every 14 days starting Monday" -> recurs, first due Monday
  • "due_string":"every 10 days starting today" -> recurs, first due today
  • "due_string":"every 4 days starting today" -> recurs from today

Verify the response body's .due.string echoes the recurrence and .due.date is the correct first occurrence. This is part of a compound action - set priority/labels/due_string before the move.

Priority Mapping (THE PRIORITY TRAP)

API priority is inverted vs UI. Always convert:

API priorityUI LabelMeaning
4P1Highest (red)
3P2High (orange)
2P3Medium (blue)
1P4Lowest (no color)

Before presenting ANY batch: Verify the API value matches the UI label.

Recurring Tasks (due_string syntax)

David frequently sets recurring reminders during triage. Set recurrence via the due_string field on the update call:

  • "due_string":"every 4 days starting today" -> first occurrence today
  • "due_string":"every 14 days starting Monday" -> first occurrence next Monday
  • "due_string":"every 6 days" -> Todoist defaults first occurrence to today

When David says "every N days, due date today" he means recurrence every N days, first occurrence today -> "every N days starting today".

Task IDs Change When Moved

When a task is moved to a different project, Todoist assigns a new task ID. Match by content (task text), not by id.

Execution order for compound actions: Always update priority and due date before moving. The move changes the task ID, so later updates using the old ID will silently fail.

Team-Prefixed Tasks in Inbox

When David asks to auto-categorize remaining Inbox tasks between main projects, tasks prefixed with a team member's name (e.g. assistant -- ...) are not candidates for self-categorization - they belong delegated to that person's project. If David's stated task count does not match the actual Inbox count, the diff is usually team-prefixed tasks.

Name-only-not-prefixed tasks (e.g. "assistant errand") are different. They are not a team-prefixed delegation pattern. If David scopes categorization to Personal/Business/Wisdom only, ask: "Assistant's project, or Personal?"

Stale ID Trap

ALWAYS re-fetch task IDs immediately before any delete/move/update operation. Never reuse IDs from an earlier fetch - they go stale when tasks are moved or recreated. The API returns 204 on already-deleted IDs with no error, so stale IDs fail silently.

Resuming an Interrupted Batch

If a compound-action run gets interrupted mid-batch, do not re-execute the whole list.

  1. Re-fetch the source project fresh.
  2. For each task on the original job list, try content-match against the fresh source fetch.
  3. NOT FOUND on the source means likely already moved successfully. Verify by fetching the intended target project and confirming expected priority and due date.
  4. Only re-execute jobs where the task is still in the source project.

This avoids double-moving, duplicate creates, or silently leaving the half-finished task in the wrong state.

API Reference

Uses the todoist skill for all API operations. Key endpoints:

  • Delete: DELETE /api/v1/tasks/{id}
  • Complete: POST /api/v1/tasks/{id}/close
  • Move: POST /api/v1/tasks/{id}/move with {"project_id": "TARGET_ID"}
  • Update priority: POST /api/v1/tasks/{id} with {"priority": API_VALUE}
  • Recurring delete: Clear recurrence first ({"due_string": ""}), then delete.
  • NEVER use update endpoint to move - it silently ignores project_id.

Auth: $TODOIST_API_TOKEN from .env file. Always source .env first.

Signals

GitHub stars
60
Forks
11
Last commit
Jun 2026
Advanced
Catalog kind
skill
Gateway key
organize-tasks
Source
github.com/matyasstoch/david-skills