Read-later digest: Gmail → Notion → Todoist
Label or alias saves mail into a Notion queue; Mondays overwrite one Todoist reading task.
This workflow deliberately skips the star as its capture signal — a dedicated Gmail label and alias instead. Shares Gmail with Personal GTD, but a different trigger: star = action item, label/alias = content to save. Gmail's Snooze (built into Gmail) reshelves one email; it cannot rule-capture into a durable reading queue or nudge three reads this week — that complementary gap is what this fills. New items land in Notion keyed by Gmail message id; every Monday the oldest three unread overwrite one recurring Todoist task.
How it flows
- 01
New content hits the alias or matches a filter
Forwarded links and subscribed newsletters both land with the ReadLater label — one by hand, one by rule.
- 02
Hourly poll finds unlabeled-as-captured ReadLater mail
Query label:ReadLater -label:ReadLaterCaptured newer_than:30d — a separate signal from starring, so it never overlaps Personal GTD.
- 03
Each message is deduped, then queued
data_sources/query on Gmail Message ID skips anything already filed; new mail becomes a Notion page (Status: Unread) with title, link, and that id.
- 04
Source message marked ReadLaterCaptured
ReadLaterCaptured goes on; ReadLater stays. The label makes the poll safe to re-run — same idempotent pattern as Captured, different namespace.
- 05
Every Monday, the oldest three Unread items are picked
Sorted by Captured At, oldest first, so nothing ages out unread indefinitely.
- 06
The fixed Todoist task gets this week's three
One POST overwrites description on the same task_id from setup — recurring handles when it reappears; this handles what it says. due_string is left untouched.
- 07
Picked pages are marked Queued-this-week
Keeps them out of next Monday's pick without guessing whether you actually read them — marking Read is left to you in Notion, on purpose.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
Gmail
Capture layer — label and alias, never the star
- 01
Create the ReadLater and ReadLaterCaptured labels
Gmail's left sidebar → Labels → Create new label → name it ReadLater, then create a second label ReadLaterCaptured (the poller's idempotency mark — same idea as Personal GTD's Captured, different namespace). Scripting setup instead, POST twice to /gmail/v1/users/me/labels with a name field. Creating labels and, in the next step, filters needs the gmail.labels and gmail.settings.basic scopes — neither is covered by the gmail.readonly scope the poller uses later, and gmail.readonly 403s on any write. Applying the ReadLaterCaptured mark after filing needs gmail.modify.
POST https://gmail.googleapis.com/gmail/v1/users/me/labels{ "name": "ReadLater" } - 02
Wire two capture paths; know what Snooze already does
Gear → See all settings covers aliases (username+readlater@gmail.com — every + variant lands in the same inbox). For ad-hoc links, forward or CC that alias; for newsletters you already subscribe to, search box → Show search options → set From to the sender → Create filter → apply the ReadLater label so those never need a manual tap. Gmail already ships a 'later' gesture: Snooze removes one message from the inbox, parks it in Snoozed, and returns it to the top at the time you picked. Use it when one email should resurface; this workflow is the complement — rule-based capture, a durable queue outside Gmail, and a Monday digest Snooze never builds. Caveat: some newsletter signup forms reject the + character, so treat the alias as the ad-hoc path and the From filter as the reliable one for real subscribe forms. Aliases and filters are documented on separate help pages — pairing a filter's To condition with an alias is a lawful combination of two shipped features, not a single official recipe, so re-check the criteria field name against the API reference before scripting it. Label_ReadLater_ID in the body below is the id returned by the labels.create call in the previous step — Gmail label ids are not the label name.
POST https://gmail.googleapis.com/gmail/v1/users/me/settings/filters{ "criteria": { "to": "you+readlater@gmail.com" }, "action": { "addLabelIds": ["Label_ReadLater_ID"] } } - 03
Set the poll query on its own label namespace
Poll hourly with the query below — volume stays far under Gmail quota. Same mailbox can run beside Personal GTD without colliding: that workflow polls is:starred -label:Captured and treats the star as 'act on this'; this one polls a dedicated label and alias for 'save this to read later'. The -label:ReadLaterCaptured exclusion keeps the query idempotent the same way Captured does, in a completely separate label namespace — one gesture never fires both.
Gmail search querylabel:ReadLater -label:ReadLaterCaptured newer_than:30d
Notion
The reading queue — source of truth for what is unread
- 01
Create the "Read Later" database
New page → Table (or /database inside an existing page). Add exactly these properties, names matter because the API writes to them by name: Title (Title), URL (URL), Source (Select: Newsletter / Article / Forwarded), Status (Select: Unread / Queued-this-week / Read), Captured At (Date), Gmail Message ID (Text) — that last field is the dedupe key the poller checks before writing anything.
- 02
Create an internal connection and share the database with it
app.notion.com/developers/connections → Build → Internal connections → Create a new connection → name it, pick the workspace → Configuration tab → copy the Installation access token. Then open the database → ••• (top right) → Connections → + Add connection → your connection → confirm access, and grant Can edit — the same token both queries for duplicates and writes new pages, so read-only access is not enough.
- 03
Record the data source id; query and create against it
Notion-Version 2026-03-11 is current. Retrieve the database once and keep its data_source_id — POST /v1/databases/{database_id}/query has been deprecated since the 2025-09-03 API version; the live endpoint for the dedupe check is data_sources/query below, and new pages parent on data_source_id too. This is also why the Notion → Todoist leg lives outside Notion: the database's Automations panel offers three triggers (Page added, Property edited, Every {frequency}) and eight actions (Edit property, Add page to a database, Edit pages in a database, Send notification, Send mail, Send webhook, Send Slack notification on Plus/Business/Enterprise, Define variables) — nothing in that list creates a Todoist task, so picking the week's three items and pushing them out cannot be a native Automation.
POST https://api.notion.com/v1/data_sources/{data_source_id}/query · Notion-Version: 2026-03-11{ "filter": { "property": "Gmail Message ID", "rich_text": { "equals": "18f2c9a4b7e1" } } } - 04
Shape of a new queue page
When the dedupe query returns no match, POST /v1/pages with the same Notion-Version header. Parent on the data_source_id from the previous step; set Status to Unread, set Captured At to now (Monday's pick sorts on this), and store the Gmail message id so a later poll of the same mail is a no-op.
POST https://api.notion.com/v1/pages · Notion-Version: 2026-03-11{ "parent": { "data_source_id": "{data_source_id}" }, "properties": { "Title": { "title": [{ "text": { "content": "Why Focus Time Beats Time Blocking" } }] }, "URL": { "url": "https://example.com/article" }, "Gmail Message ID": { "rich_text": [{ "type": "text", "text": { "content": "18f2c9a4b7e1" } }] }, "Status": { "select": { "name": "Unread" } }, "Captured At": { "date": { "start": "2026-08-09" } } } }
Todoist
The Monday nudge — one task, rewritten, not one task per article
- 01
Get your API token and a home for the task
Todoist → Settings → Integrations → Developer → copy the API token. Create a 'Read Later' project, or reuse Inbox with a read-later label — either is fine, since this workflow only ever writes to one fixed task, never one task per article. Use the v1 path only: REST v2 returns 410 Gone.
- 02
Create one recurring task by hand and keep its id
Create a single task — content "This week's reading", due_string "every monday at 9am" — and save the task_id from the response; this is the only task this workflow ever touches. Completing a Todoist recurring task only reschedules its next due date; it does not change content or description — same task object, same text, just a later day. Recurring alone gives you the Monday cadence; it does not put this week's three picks inside the task, so every Monday POST /api/v1/tasks/{task_id} must overwrite description on that same id. The body below deliberately omits due_string — resending it may perturb the recurring rule already set at creation. That risk is inferred from how recurring tasks behave, not confirmed for this exact field combination, so test one overwrite against a throwaway token before trusting it in production.
POST https://api.todoist.com/api/v1/tasks/{task_id}{ "description": "1. Why Focus Time Beats Time Blocking — https://example.com/a\n2. ...\n3. ..." }