WorkflowsMCP

Sprint retro & kickoff docs: Linear cycles → Notion

Detect a Linear cycle rollover, draft retro and kickoff docs in Notion, post links to Slack.

Linear's own cycle graph shows completion percentage and a capacity dial, but it doesn't produce a document a team can open at a retro, and it doesn't leave Linear — that gap is the whole workflow. This watches for a cycle rollover, pulls the closed cycle's completion rate, scope changes and trailing velocity from the GraphQL API, instantiates a retro page in Notion from the team's own database template, drafts a kickoff brief for the new cycle from the same numbers, and posts both doc links to the team channel in Slack.

How it flows

  1. 01

    Daily poll checks Linear for a cycle rollover

    team.activeCycle and team.cycles(filter:{isPrevious:{eq:true}}) come back from the same query; Linear's own cycle filters mean there is no date math to do locally.

  2. 02

    Cycle-id comparison flags the rollover

    Compare the returned previous-cycle id to the last id this workflow processed. A changed id means a cycle just closed; the same id means nothing to do yet — which is what makes a daily poll idempotent without any extra bookkeeping.

  3. 03

    Closed cycle's metrics pulled

    progress, scopeHistory, completedIssueCountHistory and uncompletedIssuesUponClose come off the poll query; issues whose addedToCycleAt is later than the cycle's startsAt are flagged as scope added mid-cycle.

  4. 04

    Trailing velocity computed

    A second query (cycles(filter: { completedAt: { neq: null } }, last: 3, orderBy: createdAt) — isPrevious only returns one cycle, so the trend needs its own call) pulls the three most recently completed cycles' progress and issue-count history for the comparison line — the same window Linear's own capacity dial uses — so the retro doc's trend matches what the team already sees inside Linear.

  5. 05

    Retro doc instantiated in Notion

    POST /v1/pages with the "Sprint Retro" template id fills in the blank skeleton; the pulled metrics go into page properties in the same synchronous response, ahead of the template's own asynchronous block fill-in.

  6. 06

    Kickoff brief drafted for the new cycle

    A second page, built directly from blocks rather than a template, carrying the new activeCycle's number and dates, the trailing-velocity trend, and the list of issues carried over from the closed cycle.

  7. 07

    Links posted to the team channel

    Both Notion URLs go into one Slack message so the team can open the retro doc before the meeting starts.

Set up each app

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

Linear

Detects the cycle rollover and supplies the metrics

  1. 01

    Create a personal API key

    Linear → Settings → Security & access → Personal API keys → New API key (linear.app/settings/account/security). Send it as the raw Authorization header value — no "Bearer " prefix, that is for OAuth access tokens and the single most common 400 here. Reading cycles, issues and users this way needs no special scope — this workflow only polls, so no admin-scoped webhook registration is needed.

  2. 02

    Bootstrap: team id and cycle settings

    Run this once per team and store the ids. cyclesEnabled tells you whether the team even runs cycles before you build anything on top of one.

    Bootstrap query
    query Bootstrap {
      teams {
        nodes {
          id
          name
          cyclesEnabled
          cycleDuration
          cycleStartDay
          activeCycle { id number startsAt endsAt }
        }
      }
    }
  3. 03

    Set the polling cadence

    Default to once a day: query the team's activeCycle alongside cycles(filter:{isPrevious:{eq:true}}) — Linear's own isPrevious/isFuture filters do the date math, so there is no local calendar logic to get wrong, and it holds whether the team lets cycles auto-rollover or someone ends one early with "End cycle today". The same query pulls addedToCycleAt on each issue, which is later than startsAt for anything added after the cycle started — that is the scope-change signal the retro doc uses. Linear lists Cycles as a webhook resource, but its docs never say a rollover itself produces a webhook event with a completion flag, so treat webhooks as a future latency upgrade, not the initial build — polling is the simplest thing that works and it is what this marketplace's other Linear workflow already relies on.

    Poll: current cycle + closed-cycle metrics
    query CycleMetrics($teamId: String!) {
      team(id: $teamId) {
        activeCycle {
          id
          number
          startsAt
          endsAt
          progress
          scopeHistory
          completedIssueCountHistory
        }
        cycles(filter: { isPrevious: { eq: true } }, first: 1) {
          nodes {
            id
            number
            progress
            scopeHistory
            completedScopeHistory
            uncompletedIssuesUponClose { nodes { id identifier title assignee { name } } }
          }
        }
      }
    }
  4. 04

    Query the trailing three-cycle velocity

    isPrevious only returns the single cycle that just closed — it cannot give the three-cycle trend the retro doc needs. Pull the three most recently completed cycles instead: filter on completedAt being set, and take the last three in creation order (the cycles connection only supports ordering by createdAt or updatedAt, not by cycle number, so last: 3 with orderBy: createdAt gives the three most recently completed).

    Trailing velocity (last 3 completed cycles)
    query TrailingVelocity($teamId: String!) {
      team(id: $teamId) {
        cycles(filter: { completedAt: { neq: null } }, last: 3, orderBy: createdAt) {
          nodes {
            id
            number
            progress
            completedIssueCountHistory
            completedScopeHistory
          }
        }
      }
    }

Notion

Where the retro and kickoff docs live

  1. 01

    Create an internal integration

    app.notion.com/my-integrations → New integration. Capabilities: Insert content and Read content only — this workflow never comments and never needs to @-mention anyone, so leave user information at "No user information". Copy the token.

  2. 02

    Share the retro database and kickoff page

    Open your "Sprint Retro" database and your "Cycle Kickoff" page, ••• menu → Add connections → pick the integration, on both. A 404 from the API on a page you can plainly see almost always means this step was skipped.

  3. 03

    Look up the retro database's data source id

    Every snippet below that references a data_source_id needs one, and it is not the id visible in the database's URL — that is the database id. Call GET /v1/databases/{database_id} with the integration token and read data_sources[0].id from the response (a database has exactly one data source unless someone has explicitly split it). Store that id; every "Sprint Retro" data_source_id used below refers to this value.

  4. 04

    Configure a real database template

    In "Sprint Retro", use the database's own template mechanism, not a plain page: New (top right) → the dropdown arrow next to it → New template. Build the "What shipped / What didn't / Action items" skeleton there. Notion's template only produces that blank skeleton — it has no way to reach into Linear for this cycle's numbers, so the flow still writes completion rate, velocity and carryover counts itself; do not try to make the template do more than hold the constant prompts. Look up its id at runtime rather than hardcoding it, so renaming or rebuilding the template does not silently break the workflow.

    GET data source templates
    GET https://api.notion.com/v1/data_sources/{data_source_id}/templates?name=Retro
    Authorization: Bearer {NOTION_INTEGRATION_TOKEN}
    Notion-Version: 2026-03-11
  5. 05

    Test instantiating the retro page from the template

    POST to /v1/pages with the template_id from the previous step. The response comes back with a page id right away, but the template's block content is applied by Notion asynchronously — do not PATCH /v1/blocks/{page_id}/children immediately after this call to add more content, it will race Notion's own fill-in and leave interleaved or duplicate blocks. Put every computed number in properties instead, which land in the same synchronous response; keep the template's block content limited to the fixed prompts that need no per-cycle value.

    POST /v1/pages with template
    POST https://api.notion.com/v1/pages
    Authorization: Bearer {NOTION_INTEGRATION_TOKEN}
    Notion-Version: 2026-03-11
    Content-Type: application/json
    
    {
      "parent": { "data_source_id": "2f9a1c4b-...-sprint-retro" },
      "properties": {
        "Name": { "title": [{ "text": { "content": "Cycle 41 Retro" } }] },
        "Cycle number": { "number": 41 },
        "Completion %": { "number": 0.83 },
        "Carryover issues": { "number": 4 }
      },
      "template": { "type": "template_id", "template_id": "3b7e8f1a-...-retro-template" }
    }

Slack

Tells the team both docs are ready

  1. 01

    Add an incoming webhook

    api.slack.com/apps → your app → Incoming Webhooks → Activate → Add New Webhook to Workspace → pick #eng-cycles (or your team's channel).

  2. 02

    Store the webhook URL as a secret

    Anyone holding the URL can post as your app in that channel, so keep it out of source control and prose — put it in the environment and reference the variable name only.

  3. 03

    Send both doc links in one message

    One post per rollover, not one per document — the retro link and the kickoff link belong in the same message so the channel gets a single, scannable update instead of two.

    Incoming webhook payload
    POST https://hooks.slack.com/services/T000/B000/XXXX
    Content-Type: application/json
    
    {
      "text": "Cycle 41 retro and Cycle 42 kickoff are ready",
      "blocks": [
        { "type": "section", "text": { "type": "mrkdwn",
          "text": "*Cycle 41* wrapped at 83% completion, 4 issues carried over.\n<https://notion.so/Cycle-41-Retro|Open the retro doc> · <https://notion.so/Cycle-42-Kickoff|Open the kickoff brief>" } }
      ]
    }