₹.
Authorization header (no Bearer prefix). Notion uses an internal integration token from [notion.so/my-integrations](https://developers.notion.com/), and you must share the target parent page with that integration or the create-page call returns 404. Claude uses an Anthropic API key with the x-api-key header.
https://api.linear.app/graphql. You filter issues by cycle.id and a completed state. The HTTP node body:
{
"parameters": {
"method": "POST",
"url": "https://api.linear.app/graphql",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "={{ $credentials.linearApi.apiKey }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ query: 'query($cycleId: ID!) { issues(filter: { cycle: { id: { eq: $cycleId } }, completedAt: { null: false } }) { nodes { identifier title estimate completedAt startedAt assignee { name } } } }', variables: { cycleId: $json.cycle_id } }) }}"
},
"name": "Linear Completed Issues",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2
}
Verify this node by running it against a finished cycle and counting the returned nodes[] against Linear's cycle view in the UI. If counts differ, your state filter is wrong — Linear distinguishes "completed" from "cancelled," and you usually want completed only.
## Step 3: Group by engineer and compute cycle time
Node 4 is a Code node that buckets issues per assignee and computes median cycle time from startedAt to completedAt. This is the math the team lead used to eyeball:
// n8n Code node — group by engineer
const issues = $json.data.issues.nodes;
const byEngineer = {};
for (const i of issues) {
const name = i.assignee?.name || 'Unassigned';
if (!byEngineer[name]) byEngineer[name] = { count: 0, points: 0, cycleHours: [] };
byEngineer[name].count += 1;
byEngineer[name].points += i.estimate || 0;
if (i.startedAt && i.completedAt) {
const h = (new Date(i.completedAt) - new Date(i.startedAt)) / 36e5;
byEngineer[name].cycleHours.push(h);
}
}
const median = (arr) => {
if (!arr.length) return 0;
const s = [...arr].sort((a, b) => a - b);
const m = Math.floor(s.length / 2);
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
};
const rows = Object.entries(byEngineer).map(([name, d]) => ({
name, count: d.count, points: d.points,
medianCycleHours: Math.round(median(d.cycleHours))
}));
return [{ json: { rows } }];
## Step 4: Build the Notion page
Node 9 assembles Notion block objects, and node 11 creates the page. Notion's create-page endpoint takes a parent and a children array of block objects. The create-page node body:
{
"parameters": {
"method": "POST",
"url": "https://api.notion.com/v1/pages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "=Bearer {{ $credentials.notionApi.apiKey }}" },
{ "name": "Notion-Version", "value": "2022-06-28" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({\n parent: { page_id: $env.NOTION_RETRO_PARENT },\n properties: { title: { title: [ { text: { content: 'Sprint Retro — ' + $json.cycle_name } } ] } },\n children: $json.blocks\n}) }}"
},
"name": "Create Notion Retro",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2
}
Notion caps a single create-page call at 100 child blocks, so if your retro has more than that, create the page first and append the rest with the blocks endpoint in node 12 — that is exactly why the workflow splits create and append into two nodes.
- Linear personal API key in the Authorization header (no Bearer)
- Notion parent page shared with the integration
- Claude API key with a low
max_tokensfor the summary - Cycle ID resolved from the cycle name or schedule
- State filter set to completed, not cancelled
- Block count under 100 per create call, append for the rest
- Slack channel ID for the retro link post
pageInfo.hasNextPage, so add a Loop node, or your retro silently drops issues past the first page. Third, an empty cycle (a sprint where nothing closed) — add an IF after the Linear node that skips Notion creation and posts "no completed issues this cycle" to Slack instead of creating a blank doc.
$1 per million input tokens and $5 per million output. A sprint's worth of issue titles plus a 200-word summary is well under ₹2 per run. Cap max_tokens at 400 so a runaway response cannot surprise you.
₹2 per sprint. Across a year of two-week sprints that is about ₹52 in Claude tokens, plus the flat Hetzner box. Here is the per-run cost breakdown:
The economics are obvious: the workflow pays for itself the first time it runs, because two hours of a senior engineer's time costs far more than a year of Claude tokens. Our AI and automation team sets these up as part of larger developer-productivity engagements.
## When should you not build this?
If your team does not use Linear cycles (some teams run continuous flow with no sprints), there is no cycle boundary to trigger on — use a date range instead, or skip this. If your retros are already short and data-light, automating the data pull adds little. And if nobody reads the retro doc, do not generate it; a doc nobody opens is worse than no doc because it signals the ritual is dead.
## Real example: the Hyderabad product team
The client's team lead spent the last two hours of every two-week sprint compiling the retro by hand, which meant the lead's own sprint work shrank. After we shipped this flow, the doc is ready before the retro meeting starts, and the lead got those two hours back per sprint — about 52 hours a year. The cycle-time-per-engineer number, which nobody used to compute, now surfaces quietly bad patterns like one engineer consistently sitting on large issues. We applied the same Linear-to-doc spine when building internal tooling around ExamReady's release cycle.
Computing median cycle time per engineer was something we always meant to do and never did. The workflow does it every sprint without anyone asking, and that is where the real insight came from.
pageInfo.hasNextPage and accumulates nodes, or the retro silently drops everything past the first page. This is the single most common bug in DIY versions of this flow.
### Does it overwrite the previous sprint's retro?
No. Each run creates a new Notion page titled with the cycle name, under a shared parent page. Past retros stay as a browsable history, which teams find useful for trend-spotting.
### How long does it take to build?
A working v1 takes 4 to 6 working days, including resolving the Notion sharing setup and agreeing what the retro doc template should contain with your team lead.
Want this sprint-retro auto-drafter wired to your Linear and Notion?
We ship a self-hosted n8n developer-productivity workflow in 6 working days. Typical cost: ₹45,000–₹70,000. Suitable if your team runs Linear cycles and someone burns an afternoon compiling retros by hand. No slides — show us your Linear workspace and we will draft the template.

