WorkflowsMCP

Meeting prep briefs: Calendar → Notion + Linear → Slack

Poll Calendar for meetings starting soon, match Notion docs and Linear issues, DM the pre-read.

@workflowsmcpVerified meetingsaction-itemsproject-managementautomation

Thirty minutes before a meeting, most people haven't opened the doc or checked the tracker. This polls Google Calendar for meetings starting soon, matches the title against Notion docs and Linear issues, and DMs each attendee a pre-read — doc links, issue status, or an explicit "checked, found nothing" instead of silence. It's for internal team meetings, not a CRM; a sales-call brief is separately scoped. Notion Calendar overlays items next to Calendar, but that's a view you open — this pushes the brief to DMs unasked.

How it flows

  1. 01

    Poll for meetings starting soon

    Every 5–10 minutes, events.list on the primary calendar with timeMin=now, timeMax=now+30min, singleEvents=true, orderBy=startTime. Both bounds are exclusive and must carry a timezone offset — a bare timestamp gets rejected.

  2. 02

    Dedupe by event id

    The same meeting shows up across several poll windows before it starts; track which event ids were already processed so the pre-read is not sent twice. Unlike the Notion-status or Gmail-label write-backs other polling workflows in this collection use as their idempotency marker, this cannot be written back onto the Calendar event itself — that would need a wider scope than the read-only one Setup step 3 deliberately keeps. Keep the processed-id set in whatever runs the poll job (a local table or key-value store), with entries expiring after a day — well past the 30-minute window any single event can appear in.

  3. 03

    Search Notion for related docs

    Title-substring search on the meeting title. Zero hits fall back to any Notion or Linear URL already pasted into the meeting description — a stronger signal than retrying with a wider, noisier title.

  4. 04

    Search Linear for related issues

    searchIssues against the meeting title (full-text + vector search); if the title reads as a project name, searchProjects first and pull that project's issues instead. Throttled to Linear's 30-requests/minute searchIssues limit when several meetings land in the same poll window.

  5. 05

    Assemble the pre-read

    Notion doc links, Linear issue identifiers and status, and the meeting's own time and attendee list, combined into one message.

  6. 06

    Resolve attendees to Slack user ids

    Each attendee email goes through users.lookupByEmail. External guests and any email that does not match a workspace member come back users_not_found — skip that attendee's DM rather than failing the whole run, and keep a tally of who was not matched.

  7. 07

    DM the pre-read to each resolved attendee

    chat.postMessage with channel set to the resolved Slack user id — no conversations.open first.

  8. 08

    Degrade instead of going silent when nothing matched

    If both Notion and Linear came back empty, the DM still goes out — with a line saying the workflow checked and found nothing, so the attendee knows to look manually instead of assuming it did not run.

Set up each app

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

Google Calendar

Trigger — spots meetings starting soon and who is invited

  1. 01

    Enable the Calendar API and configure OAuth consent

    Google Cloud Console → APIs & Services → Library → search "Google Calendar API" → Enable. Then APIs & Services → OAuth consent screen → set App name and User type (Internal only works inside a Workspace domain; External needs a Google review to distribute widely, but External + Testing mode is enough for a team running this for itself).

  2. 02

    Create a Web application OAuth client

    Credentials → Create Credentials → OAuth client ID → Web application — not Desktop app. The official Desktop quickstart only walks through a one-off local interactive authorization; it does not cover persisting a refresh token for an unattended poller, which is what this needs. Build the authorization URL with access_type=offline — omit it and the access token has no way to renew itself silently once it expires.

  3. 03

    Scope to read-only events, nothing wider

    Use calendar.events.readonly (full value in the snippet) — Google's own scope list describes it as "View events on all your calendars", narrower than the calendar.readonly scope most tutorials default to, which also grants calendar metadata and settings this workflow never touches.

    The scope value
    https://www.googleapis.com/auth/calendar.events.readonly
  4. 04

    Poll a narrow time window — this is not the anti-pattern Google warns about

    Google's own quota guide tells you not to repeatedly poll a calendar and to use push notifications instead — but that advice is about watching for content changes (events added or edited). This workflow needs something Calendar has no push mechanism for at all: a "meeting starts in 30 minutes" alert. reminders.overrides only fires to email or popup, never a webhook, so there is no native way to be told when wall-clock time crosses a threshold. Polling a 30-minute window every 5–10 minutes is the only path, not a shortcut around the documented best practice. Both timeMin and timeMax are exclusive bounds and both must carry a timezone offset — a bare timestamp is rejected. singleEvents=true is required before orderBy=startTime is accepted; without it, a recurring meeting comes back as one recurrence rule instead of the specific instance starting soon.

    GET /calendar/v3/calendars/primary/events
    GET https://www.googleapis.com/calendar/v3/calendars/primary/events
      ?timeMin=2026-08-08T09:30:00-04:00
      &timeMax=2026-08-08T10:00:00-04:00
      &singleEvents=true
      &orderBy=startTime
      &maxResults=10
    Authorization: Bearer {access_token}

Notion

One of two document/issue sources matched against the meeting

  1. 01

    Create the integration at the current URL

    The old notion.so/my-integrations now 301-redirects — create the connection at app.notion.com/developers/connections instead (Developer portal → Connections → New connection; requires workspace-owner permission). Under Capabilities, check only Read content; this workflow never writes to Notion.

  2. 02

    Add the connection to every page or database it needs

    A new connection starts with access to nothing. Open each project page or database to be matched against → "•••" menu → Add connections → search for and select the connection by name. There is no workspace-wide grant — repeat per page or database.

  3. 03

    (Optional) Record the project database's data source id

    If the team keeps a structured "Projects" database with a Relation or Select property per project, retrieve it once and keep its data_source_id — querying that by property filter is more reliable than the title search in the flow below, since it is not limited to substring matching.

  4. 04

    Know the shape of the title search

    query matches only page and data_source titles, not page body content, and it is a substring match with no fuzzy tolerance. Take the meeting title (or its core phrase, stripped of dates and meeting-series numbers) as query — a wider search only pulls back noise, it does not increase the chance of finding the right doc.

    POST https://api.notion.com/v1/search · Notion-Version: 2026-03-11
    {
      "query": "Q3 Roadmap Sync",
      "filter": { "value": "page", "property": "object" },
      "sort": { "direction": "descending", "timestamp": "last_edited_time" },
      "page_size": 5
    }

Linear

The other document/issue source matched against the meeting

  1. 01

    Create a personal API key

    Linear → Settings → Security & access → Personal API keys → New API key. Send it as the raw Authorization header value — no "Bearer " prefix, the most common source of a 400 here.

  2. 02

    Look up team ids once

    Run a one-off teams { nodes { id key name } } query and store the ids the polling job needs — every Linear GraphQL call is addressed by id, not by name.

  3. 03

    Use searchIssues, not the deprecated issueSearch

    The schema still defines issueSearch but flags it "[DEPRECATED] ... use searchIssues instead" — a pre-2025 tutorial using it is pointing at an endpoint on its way out. searchIssues runs full-text and vector search, optionally weighted with teamId, and carries its own rate limit — 30 requests/minute — separate from the account's general 5,000-requests/hour key limit. When several meetings land in the same poll window, queue their searchIssues calls rather than firing them concurrently; the 30/min ceiling bites well before the general limit does. If the meeting title is itself a project name, searchProjects first and pull that project's issues instead — it is more precise than a full-text search over every issue title.

    searchIssues
    query MeetingRelatedIssues($term: String!) {
      searchIssues(term: $term, first: 10) {
        nodes {
          identifier
          title
          state { name }
          url
        }
        totalCount
      }
    }
    
    # variables
    { "term": "Q3 Roadmap Sync" }

Slack

Delivers the pre-read as a direct message

  1. 01

    Create the app with two Bot Token Scopes

    api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes: chat:write and users:read.email. Skip im:write and channels:manage — chat.postMessage's channel parameter accepts a Slack user id directly and opens the DM itself, so there is no separate conversations.open call to make first.

  2. 02

    Install to workspace

    OAuth & Permissions → Install to Workspace → copy the xoxb- Bot User OAuth Token.

  3. 03

    Know the DM payload shape

    channel takes the Slack user id returned by users.lookupByEmail, not a channel id — Slack opens the DM conversation on the first post if one is not already open.

    POST https://slack.com/api/chat.postMessage
    {
      "channel": "U024BE7LH",
      "text": "Pre-read for Q3 Roadmap Sync (starts in 25 min)",
      "blocks": [
        { "type": "section", "text": { "type": "mrkdwn",
          "text": "*Q3 Roadmap Sync* — starts in 25 min\n\n*Notion docs:*\n• <https://notion.so/...|Q3 Roadmap Draft>\n\n*Linear issues:*\n• <https://linear.app/acme/issue/ROAD-88|ROAD-88> In Progress" } }
      ]
    }