WorkflowsMCP

Personal GTD: Gmail → Todoist → Calendar time-blocking

Starred mail becomes a task, and every task over 30 minutes gets a block on your calendar.

@workflowsmcpJan 15, 2026Verified Aug 2026personal-productivitygtdinbox-zerotime-blocking

A single-player workflow. Starring an email turns it into a Todoist task with the thread link in the description, so your inbox stops being a to-do list. Then, each evening, tasks due tomorrow that are estimated over 30 minutes get real blocks on your calendar in the gaps between meetings — because a task list with no time attached is a wish list.

How it flows

  1. 01

    You star an email

    One gesture, works on every Gmail client.

  2. 02

    Poller picks it up within 5 minutes

    Query excludes anything already labelled Captured, so it is safe to run often.

  3. 03

    Task created in Todoist

    Subject as the content, thread permalink in the description, a due string and a duration.

  4. 04

    Message labelled Captured

    The star stays — the label is what stops a second capture.

  5. 05

    Evening pass blocks tomorrow

    Tasks over 30 minutes get placed in real gaps as focus-time events, at most three a day.

  6. 06

    Completed work releases its block

    Stale focus blocks are deleted, matched back to their task by the URL in the description, so the calendar stays believable.

Set up each app

Work through these in order — later apps usually need a token or an id from an earlier one.

Gmail

Star is the capture gesture

  1. 01

    Use starring as the only trigger

    Poll every 5 minutes with the query below. One gesture, available on every Gmail client including the phone — no add-on, no forwarding address to remember. Polling is a deliberate trade: it keeps setup to a single token, where push would want a Cloud project and Pub/Sub. If five minutes is too slow for you, the recruiting workflow in this collection shows the push route.

    Gmail search query
    is:starred -label:Captured newer_than:14d
  2. 02

    Label after capture, do not unstar

    Add the Captured label with users.messages.modify rather than removing the star. Unstarring fights your own muscle memory; the label is what makes the query idempotent. This is the one call that needs gmail.modify — gmail.readonly is enough to find the starred mail but 403s on the label write.

  3. 03

    Keep a deep link to the thread

    Build the permalink from the thread id so the task can take you straight back to the mail. Google documents no URL scheme for this, so treat the form below as convention rather than contract. The part that will bite you is /u/0: it means whichever account signed in first, not a particular mailbox. If you run two Google accounts, hard-code the right index or pass authuser.

    Thread permalink — undocumented form
    https://mail.google.com/mail/u/0/#all/18f2c9a4b7e1
    https://mail.google.com/mail/u/1/#all/18f2c9a4b7e1
    https://mail.google.com/mail/?authuser=you@acme.com#all/18f2c9a4b7e1

Todoist

The single list of what is next

  1. 01

    Get your API token

    Todoist → Settings → Integrations → Developer → copy the API token. Create one project called Inbox Zero and note its id from GET /api/v1/projects, which is cursor-paginated. Use the v1 path and nothing older: REST v2 is not deprecated-but-working, it returns 410 Gone, and the unified v1 API replaced it.

  2. 02

    Create the task with a due string

    due_string accepts natural language and is parsed in your account timezone, which is far less error-prone than hand-building a date — a bare due_string creates a floating due date, so pass a fixed timezone if you travel. Use the project id v1 handed you: v1 ids are base32 strings, not the numeric ones v2 returned. On priority, take the numeric convention here as authoritative — 4 is P1/urgent in the UI and 1 is P4/natural, and Todoist's REST schema text says the opposite of its own Sync section. duration does nothing at all unless duration_unit rides along with it. Send X-Request-Id as Todoist's own examples do, but the documented idempotency guarantee is on the Sync API's command uuid, so treat it as best-effort and dedupe on the Gmail message id too.

    POST /api/v1/tasks
    curl -X POST https://api.todoist.com/api/v1/tasks \
      -H "Authorization: Bearer $TODOIST_TOKEN" \
      -H "Content-Type: application/json" \
      -H "X-Request-Id: gmail-18f2c9a4b7e1" \
      -d '{
        "content": "Reply to Dana about the Q3 contract",
        "description": "https://mail.google.com/mail/u/0/#all/18f2c9a4b7e1",
        "project_id": "6X6WMMqgq2PWxjCX",
        "labels": ["from-email"],
        "priority": 3,
        "due_string": "tomorrow at 9am",
        "duration": 45,
        "duration_unit": "minute"
      }'
    
    # The response carries url — https://app.todoist.com/app/task/6XR4GqQQCW6Gv9h4 —
    # and that is what the calendar block links to. Do not compose it yourself: the
    # old showTask?id= form was removed and now 404s.
  3. 03

    Set a duration, always

    Default to 30 minutes when the email gives you nothing to go on. The duration field is what the calendar step reads, and it is silently void without duration_unit beside it. Get either wrong and nothing gets blocked — the second half of this workflow just quietly does nothing.

Google Calendar

Turns intentions into defended time

  1. 01

    Read tomorrow's tasks and your free gaps

    Run at 18:00. GET /api/v1/tasks/filter?query=tomorrow — v1 dropped the filter parameter from the task list and gave filtering its own endpoint, which takes one query and does not accept comma-joined filters. It is cursor-paginated, so page through next_cursor before concluding tomorrow has nothing over 30 minutes. Keep the tasks with duration over 30 minutes, then list calendar events for tomorrow to find the gaps between 09:00 and 17:00.

    Blocking rules
    work window        09:00-17:00 local
    min gap to use     45 min (30 min task + 15 min buffer)
    max blocks/day     3          # beyond this the day is fiction
    skip               days with >4h of existing meetings
    order              priority desc, then longest task first
  2. 02

    Insert it as focus time, not a plain busy block

    transparency "opaque" is already the default and only marks you busy — it declines nothing, so it does not actually stop a colleague booking over the block. Focus time is the feature that does: eventType "focusTime" with autoDeclineMode set to declineOnlyNewConflictingInvitations turns away conflicting invitations while it runs, and sets your chat status too. Primary calendar only, and never all-day. colorId 8 (graphite) keeps focus blocks visually distinct from real meetings. Worth knowing why this is custom at all: Todoist's own Calendar integration already mirrors dated tasks two-way, but it mirrors every one of them — this workflow exists for what it does not do, which is choosing which gap, capping the day at three blocks, and skipping meeting-heavy days.

    POST /calendar/v3/calendars/primary/events
    {
      "summary": "Focus — Reply to Dana about the Q3 contract",
      "description": "Todoist: https://app.todoist.com/app/task/6XR4GqQQCW6Gv9h4",
      "start": { "dateTime": "2026-04-09T09:00:00-04:00" },
      "end":   { "dateTime": "2026-04-09T09:45:00-04:00" },
      "eventType": "focusTime",
      "focusTimeProperties": {
        "chatStatus": "doNotDisturb",
        "autoDeclineMode": "declineOnlyNewConflictingInvitations",
        "declineMessage": "In focus time — I will come back to you this afternoon."
      },
      "transparency": "opaque",
      "colorId": "8",
      "reminders": { "useDefault": false }
    }
  3. 03

    Clean up what you did not do

    At 17:30 delete future focus blocks whose Todoist task is already complete — GET /api/v1/tasks/completed/by_due_date is the list to check them against. The block carries the task URL in its description, so the task id parsed out of it is your join key; store the pair at write time if you would rather not parse. A calendar full of stale blocks is one you stop believing, and then the whole system is dead.