{
  "name": "Send engineering daily digest to Slack with Jira, GitHub and OpenAI",
  "nodes": [
    {
      "id": "8b8b9beb-e001-4641-9634-be79a499e10f",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        80
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Engineering Daily Digest\n\n### How it works\n\nThis workflow runs every weekday at 9 AM, loads engineering digest settings, and collects Jira and GitHub signals in parallel. It finds recent Jira activity, stale in-progress Jira tickets, recent GitHub pull request activity, and stale pull requests without reviews, then normalizes and merges those findings. The combined data is summarized with OpenAI, formatted into Slack blocks, posted to Slack, and optionally logged to Google Sheets for velocity tracking.\n\n### Setup steps\n\n- Configure the weekday schedule trigger, including the intended timezone for the 9 AM run.\n- Set the Config node values such as jiraBaseUrl, jiraProjectKey, staleTicketDays, staleReviewHours, repository list, Slack channel, and any sheet logging flag used by the workflow.\n- Add Jira credentials with permission to search issues in the configured project.\n- Add GitHub credentials or an API token with access to the repositories and pull request/search endpoints used by the workflow.\n- Add OpenAI credentials and confirm the summarization model/prompt are appropriate for engineering digest content.\n- Add Slack credentials with permission to post messages to the target channel.\n- If sheet logging is enabled, add Google Sheets credentials and configure the target spreadsheet, sheet, and appended row fields.\n\n### Customization\n\nAdjust staleTicketDays, staleReviewHours, repository lists, Jira queries, the OpenAI summary prompt, Slack block formatting, and the Google Sheets logging toggle/columns to match the team’s reporting needs."
      },
      "typeVersion": 1
    },
    {
      "id": "fdcecf8e-bba4-4fff-936a-75c9bb5945d0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Schedule and config\n\nStarts the workflow on weekday mornings and defines shared settings used by the Jira, GitHub, Slack, and logging branches."
      },
      "typeVersion": 1
    },
    {
      "id": "d5e40617-2d2f-467d-9636-bed6150f8e90",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 304,
        "content": "## Jira stale tickets\n\nQueries Jira for stale in-progress issues and normalizes them into a digest-friendly format."
      },
      "typeVersion": 1
    },
    {
      "id": "3c5b3384-8235-4ca3-8ebc-db6a88994d20",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1296,
        800
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Jira recent activity\n\nFetches Jira activity from the last 24 hours and converts the results into a consistent normalized structure."
      },
      "typeVersion": 1
    },
    {
      "id": "986896ab-2cef-424d-a2c3-4efb288d3c7a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2128,
        80
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## Merge Jira findings\n\nCombines the normalized recent and stale Jira result streams into one Jira digest payload."
      },
      "typeVersion": 1
    },
    {
      "id": "4fc9cda0-004d-4598-b5e3-43d16efe28dc",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1344,
        1152
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## Repository fan-out\n\nExpands the configured repository list so GitHub checks can run for each repository."
      },
      "typeVersion": 1
    },
    {
      "id": "debb08cb-4bd2-415c-b2d1-5ec794420ba4",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        656
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## GitHub recent activity\n\nRetrieves recent pull request activity from GitHub, flattens repository-level results, and normalizes them for the digest."
      },
      "typeVersion": 1
    },
    {
      "id": "e564036f-ecf8-410e-8b71-e030ec76a9bc",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        1600
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## GitHub stale reviews\n\nSearches GitHub for stale pull requests without reviews, flattens the API response, and normalizes the findings."
      },
      "typeVersion": 1
    },
    {
      "id": "38313b12-be69-4c21-9ff4-baf887514460",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2400,
        256
      ],
      "parameters": {
        "color": 7,
        "height": 368,
        "content": "## Merge GitHub findings\n\nCombines the normalized recent pull request activity with the stale review findings into a single GitHub digest stream."
      },
      "typeVersion": 1
    },
    {
      "id": "9ad1bf9c-444f-4f7a-92a8-97ebe799b7f4",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3216,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 656,
        "content": "## Summarize and format\n\nMerges Jira and GitHub data, generates an AI-written digest summary, parses the model output, and builds Slack message blocks."
      },
      "typeVersion": 1
    },
    {
      "id": "0415b5f3-670e-48c2-b95c-c8c048b5b205",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4048,
        624
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## Post Slack digest\n\nPublishes the formatted engineering digest to the configured Slack destination."
      },
      "typeVersion": 1
    },
    {
      "id": "069015a1-0e29-4905-a848-d50dc36cbe46",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4720,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 528,
        "content": "## Optional sheet logging\n\nChecks whether spreadsheet logging is enabled, appends a velocity row to Google Sheets when enabled, or exits through a no-op branch when disabled."
      },
      "typeVersion": 1
    },
    {
      "id": "2be2f2bb-fee5-4f53-8f1c-98c9460ead84",
      "name": "When Weekday 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        784,
        352
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "98779315-d2dd-4b36-8b33-4b1db9a343cd",
      "name": "Set Config Fields",
      "type": "n8n-nodes-base.set",
      "notes": "Single source of truth for everything the digest needs to run. Edit values here, nowhere else.",
      "position": [
        1008,
        352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "jiraBaseUrl",
              "type": "string",
              "value": "https://intuz.atlassian.net"
            },
            {
              "id": "cfg-2",
              "name": "jiraProjectKey",
              "type": "string",
              "value": "PTN"
            },
            {
              "id": "cfg-3",
              "name": "staleTicketDays",
              "type": "number",
              "value": 3
            },
            {
              "id": "cfg-4",
              "name": "staleReviewHours",
              "type": "number",
              "value": 48
            },
            {
              "id": "cfg-5",
              "name": "repos",
              "type": "array",
              "value": "=[{\"owner\": \"IntuzDiyaB\", \"name\": \"Testing-n8n\"}]"
            },
            {
              "id": "cfg-6",
              "name": "slackChannel",
              "type": "string",
              "value": "#eng-daily-digest"
            },
            {
              "id": "cfg-7",
              "name": "userMap",
              "type": "object",
              "value": "={ \"jira-account-id-example\": \"U0123ABC\", \"github-login-example\": \"U0456DEF\" }"
            },
            {
              "id": "cfg-8",
              "name": "sheetEnabled",
              "type": "boolean",
              "value": true
            },
            {
              "id": "cfg-9",
              "name": "sheetId",
              "type": "string",
              "value": "your-google-sheet-id"
            },
            {
              "id": "cfg-10",
              "name": "sheetTabName",
              "type": "string",
              "value": "Velocity"
            },
            {
              "id": "cfg-11",
              "name": "openAiModel",
              "type": "string",
              "value": "gpt-4o-mini"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "544a162b-2a40-4bac-b279-28a9dc369303",
      "name": "Fetch Jira Recent Activity",
      "type": "n8n-nodes-base.jira",
      "notes": "JQL: project = {jiraProjectKey} AND updated >= -1d ORDER BY updated DESC",
      "position": [
        1344,
        960
      ],
      "parameters": {
        "options": {
          "jql": "=project = {{ $json.jiraProjectKey }} AND updated >= -1d ORDER BY updated DESC"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "1913330c-0dfa-4ce1-a3a9-a807c280a812",
      "name": "Fetch Jira Stale Tasks",
      "type": "n8n-nodes-base.jira",
      "notes": "JQL: project = {jiraProjectKey} AND status = \"In Progress\" AND updated <= -{staleTicketDays}d ORDER BY updated ASC",
      "position": [
        1520,
        416
      ],
      "parameters": {
        "options": {
          "jql": "=project = {{ $json.jiraProjectKey }} AND status = \"In Progress\" AND updated <= -{{ $json.staleTicketDays }}d ORDER BY updated ASC"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "0e510306-abb4-481b-954a-a120a2700d4c",
      "name": "Normalize Jira Recent Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        960
      ],
      "parameters": {
        "jsCode": "const baseUrl = $('Set Config Fields').item.json.jiraBaseUrl;\nconst issues = $input.all().map(item => {\n  const issue = item.json;\n  return {\n    key: issue.key,\n    summary: issue.fields?.summary,\n    status: issue.fields?.status?.name,\n    assignee: issue.fields?.assignee?.displayName || 'Unassigned',\n    assigneeAccountId: issue.fields?.assignee?.accountId || null,\n    updated: issue.fields?.updated,\n    url: `${baseUrl}/browse/${issue.key}`\n  };\n});\nreturn [{ json: { recentIssues: issues } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "2e8da8ef-48a6-4412-9387-250809ff912f",
      "name": "Normalize Jira Stale Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1760,
        416
      ],
      "parameters": {
        "jsCode": "const baseUrl = $('Set Config Fields').item.json.jiraBaseUrl;\nconst now = Date.now();\nconst issues = $input.all().map(item => {\n  const issue = item.json;\n  const updated = new Date(issue.fields?.updated).getTime();\n  const daysStale = Math.floor((now - updated) / 86400000);\n  return {\n    key: issue.key,\n    summary: issue.fields?.summary,\n    status: issue.fields?.status?.name,\n    assignee: issue.fields?.assignee?.displayName || 'Unassigned',\n    assigneeAccountId: issue.fields?.assignee?.accountId || null,\n    updated: issue.fields?.updated,\n    daysStale,\n    url: `${baseUrl}/browse/${issue.key}`\n  };\n});\nreturn [{ json: { staleIssues: issues } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "4e6dfe1e-639f-4b54-8661-401ed83e99e7",
      "name": "Merge Jira Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        2176,
        256
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": true
        },
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "bf1ef184-71c7-469b-9723-8c223378165e",
      "name": "Split Repository List",
      "type": "n8n-nodes-base.code",
      "notes": "Fans out Config.repos into one item per repo so the GitHub HTTP Request nodes run once per repo automatically.",
      "position": [
        1392,
        1328
      ],
      "parameters": {
        "jsCode": "const config = $json;\nconst sinceISO = new Date(Date.now() - 24*60*60*1000).toISOString();\nconst staleHours = config.staleReviewHours || 48;\nconst staleDateISO = new Date(Date.now() - staleHours*60*60*1000).toISOString().split('T')[0];\nreturn (config.repos || []).map(repo => ({\n  json: {\n    owner: repo.owner,\n    name: repo.name,\n    fullName: `${repo.owner}/${repo.name}`,\n    sinceISO,\n    staleDateISO\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "3e861fab-fb55-47b0-91ff-7a85c1d7b91b",
      "name": "Fetch Stale GitHub PRs",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "GET /search/issues?q=repo:{owner}/{name}+is:pr+is:open+review:none — the review:none qualifier is the cheap way to find PRs nobody has reviewed, no per-PR calls needed.",
      "position": [
        1888,
        1728
      ],
      "parameters": {
        "url": "https://api.github.com/search/issues",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "=repo:{{ $json.fullName }} is:pr is:open review:none"
            },
            {
              "name": "per_page",
              "value": "50"
            }
          ]
        },
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "a60d9b50-b3ec-400e-880f-9f43133f24e7",
      "name": "Flatten GitHub PR Activity",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        784
      ],
      "parameters": {
        "jsCode": "const results = [];\nfor (const item of $input.all()) {\n  const body = item.json;\n  if (Array.isArray(body)) {\n    for (const pr of body) {\n      if (pr && pr.number != null) results.push({ json: pr });\n    }\n  } else if (body && body.number != null) {\n    results.push({ json: body });\n  }\n}\nreturn results.length ? results : [{ json: { number: null } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "782da25f-dcd4-4ba8-98da-54fa6a98711e",
      "name": "Flatten GitHub Stale PRs",
      "type": "n8n-nodes-base.code",
      "position": [
        2112,
        1728
      ],
      "parameters": {
        "jsCode": "const results = [];\nfor (const item of $input.all()) {\n  const body = item.json;\n  const list = Array.isArray(body?.items) ? body.items : [];\n  for (const issue of list) {\n    if (issue && issue.number != null) results.push({ json: issue });\n  }\n}\nreturn results.length ? results : [{ json: { number: null } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "52a1284b-a39f-4669-9e58-094c12737d79",
      "name": "Normalize GitHub Recent Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        784
      ],
      "parameters": {
        "jsCode": "const now = Date.now();\nconst cutoff = now - 24*60*60*1000;\nconst prs = $input.all().map(i => i.json).filter(pr => pr && pr.number != null);\nconst opened = [], merged = [], closed = [];\nfor (const pr of prs) {\n  const createdAt = new Date(pr.created_at).getTime();\n  const mergedAt = pr.merged_at ? new Date(pr.merged_at).getTime() : null;\n  const closedAt = pr.closed_at ? new Date(pr.closed_at).getTime() : null;\n  const entry = {\n    number: pr.number,\n    title: pr.title,\n    author: pr.user?.login,\n    url: pr.html_url,\n    repo: pr.base?.repo?.full_name\n  };\n  if (createdAt >= cutoff) opened.push(entry);\n  if (mergedAt && mergedAt >= cutoff) merged.push(entry);\n  if (closedAt && !mergedAt && closedAt >= cutoff) closed.push(entry);\n}\nreturn [{ json: { prsOpened: opened, prsMerged: merged, prsClosed: closed } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "d5083785-305e-490b-8e09-d201209c5d6a",
      "name": "Normalize GitHub Stale Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2336,
        1728
      ],
      "parameters": {
        "jsCode": "const staleReviewHours = $('Set Config Fields').item.json.staleReviewHours || 48;\nconst cutoffMs = staleReviewHours * 60 * 60 * 1000;\nconst now = Date.now();\nconst prs = $input.all().map(i => i.json).filter(pr => pr && pr.number != null);\nconst stale = prs\n  .filter(pr => (now - new Date(pr.created_at).getTime()) > cutoffMs)\n  .map(pr => ({\n    number: pr.number,\n    title: pr.title,\n    author: pr.user?.login,\n    url: pr.html_url,\n    repo: (pr.repository_url || '').split('/repos/')[1],\n    hoursOpen: Math.floor((now - new Date(pr.created_at).getTime()) / 3600000)\n  }));\nreturn [{ json: { prsAwaitingReview: stale } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "63a8011d-5aba-4e13-831f-cf1886cc6cf9",
      "name": "Merge GitHub Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        2448,
        464
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": true
        },
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "4293cf7f-7a93-4636-80e1-6bf93c63841a",
      "name": "Merge Overall Digest",
      "type": "n8n-nodes-base.merge",
      "position": [
        3264,
        448
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": true
        },
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "81673de3-b001-4b65-a7da-f543ec8ee25a",
      "name": "Parse LLM Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        3648,
        800
      ],
      "parameters": {
        "jsCode": "const response = $json;\nconst digest = $('Merge Overall Digest').item.json;\nlet parsed;\ntry {\n  const content = response.choices[0].message.content;\n  parsed = JSON.parse(content);\n} catch (e) {\n  parsed = {\n    whatMoved: 'Summary unavailable — the LLM response could not be parsed.',\n    whatShipped: '',\n    whatIsBlocked: '',\n    needsReview: '',\n    sprintHealth: 'Unknown — check the digest workflow run for errors.'\n  };\n}\nreturn [{ json: { summary: parsed, digest } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "7c10fac2-504d-4bab-8b0a-7cd8db1fba74",
      "name": "Build Slack Message Blocks",
      "type": "n8n-nodes-base.code",
      "position": [
        3872,
        800
      ],
      "parameters": {
        "jsCode": "const { summary, digest } = $json;\nconst userMap = $('Set Config Fields').item.json.userMap || {};\n\nfunction mention(idOrLogin) {\n  const slackId = userMap[idOrLogin];\n  return slackId ? `<@${slackId}>` : (idOrLogin || 'unassigned');\n}\n\nconst staleMentions = (digest.staleIssues || []).map(i => mention(i.assigneeAccountId || i.assignee));\nconst reviewMentions = (digest.prsAwaitingReview || []).map(p => mention(p.author));\nconst allMentions = [...new Set([...staleMentions, ...reviewMentions])].filter(m => m.startsWith('<@'));\n\nconst blocks = [\n  { type: 'header', text: { type: 'plain_text', text: '📋 Engineering Daily Digest' } },\n  { type: 'section', text: { type: 'mrkdwn', text: `*Sprint health:* ${summary.sprintHealth}` } },\n  { type: 'divider' },\n  { type: 'section', text: { type: 'mrkdwn', text: `*What moved*\\n${summary.whatMoved}` } },\n  { type: 'section', text: { type: 'mrkdwn', text: `*What shipped*\\n${summary.whatShipped}` } },\n  { type: 'section', text: { type: 'mrkdwn', text: `*What's blocked*\\n${summary.whatIsBlocked}` } },\n  { type: 'section', text: { type: 'mrkdwn', text: `*Needs review*\\n${summary.needsReview}` } },\n  { type: 'divider' },\n  { type: 'section', text: { type: 'mrkdwn', text: `*Stale tickets (${(digest.staleIssues||[]).length})*\\n` + ((digest.staleIssues||[]).map(i => `• <${i.url}|${i.key}> — ${i.summary} (${i.daysStale}d, ${mention(i.assigneeAccountId || i.assignee)})`).join('\\n') || '_none_') } },\n  { type: 'section', text: { type: 'mrkdwn', text: `*PRs awaiting review (${(digest.prsAwaitingReview||[]).length})*\\n` + ((digest.prsAwaitingReview||[]).map(p => `• <${p.url}|#${p.number} ${p.title}> — ${p.hoursOpen}h, ${mention(p.author)}`).join('\\n') || '_none_') } }\n];\n\nif (allMentions.length) {\n  blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: `cc ${allMentions.join(' ')}` }] });\n}\n\nreturn [{ json: { blocks, text: `Engineering Daily Digest — ${summary.sprintHealth}`, digest, summary } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "1f88e1b8-6cb7-4173-9a1e-fb32e9ac8735",
      "name": "If Logging to Sheet Enabled",
      "type": "n8n-nodes-base.if",
      "position": [
        4768,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "sheet-cond-1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Set Config Fields').item.json.sheetEnabled }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6273a267-6647-46a9-b5c1-2c7ea291e2ed",
      "name": "Append Row to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4992,
        688
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "ok",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ok",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "channel",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message_timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "message_timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 903721058,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yozEO983mOlmmCm0ujIVUsU46yop5D6HsN-VvL0dU2w/edit#gid=903721058",
          "cachedResultName": "Sheet4"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1yozEO983mOlmmCm0ujIVUsU46yop5D6HsN-VvL0dU2w",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yozEO983mOlmmCm0ujIVUsU46yop5D6HsN-VvL0dU2w/edit?usp=drivesdk",
          "cachedResultName": "n8n poc"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "8e9333ac-24d4-4698-893d-9b3da69630b1",
      "name": "Skip Sheet Logging",
      "type": "n8n-nodes-base.noOp",
      "position": [
        4992,
        880
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "bf4978bc-579e-4772-9fb9-540960bdbbf3",
      "name": "Fetch GitHub PR Activity",
      "type": "n8n-nodes-base.github",
      "notes": "Native GitHub node — repository.getPullRequests (state=all). Classify opened/merged/closed in the next Code node.",
      "position": [
        2096,
        784
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.owner }}"
        },
        "resource": "repository",
        "operation": "getPullRequests",
        "returnAll": true,
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.name }}"
        },
        "getRepositoryPullRequestsFilters": {
          "sort": "updated",
          "state": "all",
          "direction": "desc"
        }
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "3ab70b1d-5709-4337-9842-b3fa593991e6",
      "name": "Summarize Digest with OpenAI",
      "type": "n8n-nodes-base.openAi",
      "notes": "Native OpenAI Chat Complete — system + user prompts from Build LLM Prompt.",
      "position": [
        3424,
        800
      ],
      "parameters": {
        "prompt": {
          "messages": [
            {
              "role": "system",
              "content": "You are an engineering-ops assistant. You will be given JSON describing the last 24 hours of Jira and GitHub activity for a software team. Produce a concise daily digest as STRICT JSON only, no markdown, no code fences, matching exactly this shape:\n{\n  \"whatMoved\": \"string, 2-4 sentences\",\n  \"whatShipped\": \"string, 2-4 sentences\",\n  \"whatIsBlocked\": \"string, 2-4 sentences, mention specific ticket keys\",\n  \"needsReview\": \"string, 2-4 sentences, mention specific PR numbers/titles\",\n  \"sprintHealth\": \"one sentence overall read, e.g. On track / At risk / Needs attention, with a short reason\"\n}\nOnly use the data given. If a category is empty, say so briefly. Do not invent tickets or PRs."
            },
            {
              "content": "={{ JSON.stringify({ staleTicketThresholdDays: $('Set Config Fields').item.json.staleTicketDays, staleReviewThresholdHours: $('Set Config Fields').item.json.staleReviewHours, recentIssues: $json.recentIssues, staleIssues: $json.staleIssues, prsOpened: $json.prsOpened, prsMerged: $json.prsMerged, prsClosed: $json.prsClosed, prsAwaitingReview: $json.prsAwaitingReview }) }}"
            }
          ]
        },
        "options": {
          "maxTokens": 2000,
          "temperature": 0.2
        },
        "resource": "chat",
        "chatModel": "={{ $('Set Config Fields').item.json.openAiModel }}",
        "requestOptions": {},
        "simplifyOutput": false
      },
      "typeVersion": 1.1
    },
    {
      "id": "f8567a22-7479-4b2a-ad86-c88a9f1423bf",
      "name": "Post Digest to Slack",
      "type": "n8n-nodes-base.slack",
      "notes": "Posts Block Kit JSON built upstream (messageType=block).",
      "position": [
        4096,
        800
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "select": "channel",
        "blocksUi": "={{ JSON.stringify($json.blocks) }}",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0C0UEMPD5E",
          "cachedResultName": "n8n-for-poc-purposes"
        },
        "messageType": "block",
        "otherOptions": {}
      },
      "typeVersion": 2.3
    }
  ],
  "connections": {
    "Merge Jira Data": {
      "main": [
        [
          {
            "node": "Merge Overall Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Weekday 9AM": {
      "main": [
        [
          {
            "node": "Set Config Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge GitHub Data": {
      "main": [
        [
          {
            "node": "Merge Overall Digest",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse LLM Summary": {
      "main": [
        [
          {
            "node": "Build Slack Message Blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Config Fields": {
      "main": [
        [
          {
            "node": "Fetch Jira Recent Activity",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Jira Stale Tasks",
            "type": "main",
            "index": 0
          },
          {
            "node": "Split Repository List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Overall Digest": {
      "main": [
        [
          {
            "node": "Summarize Digest with OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Digest to Slack": {
      "main": [
        [
          {
            "node": "If Logging to Sheet Enabled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Repository List": {
      "main": [
        [
          {
            "node": "Fetch Stale GitHub PRs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch GitHub PR Activity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Jira Stale Tasks": {
      "main": [
        [
          {
            "node": "Normalize Jira Stale Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Stale GitHub PRs": {
      "main": [
        [
          {
            "node": "Flatten GitHub Stale PRs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GitHub PR Activity": {
      "main": [
        [
          {
            "node": "Flatten GitHub PR Activity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten GitHub Stale PRs": {
      "main": [
        [
          {
            "node": "Normalize GitHub Stale Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Jira Stale Data": {
      "main": [
        [
          {
            "node": "Merge Jira Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Build Slack Message Blocks": {
      "main": [
        [
          {
            "node": "Post Digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Jira Recent Activity": {
      "main": [
        [
          {
            "node": "Normalize Jira Recent Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten GitHub PR Activity": {
      "main": [
        [
          {
            "node": "Normalize GitHub Recent Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Jira Recent Data": {
      "main": [
        [
          {
            "node": "Merge Jira Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Logging to Sheet Enabled": {
      "main": [
        [
          {
            "node": "Append Row to Google Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Sheet Logging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize GitHub Stale Data": {
      "main": [
        [
          {
            "node": "Merge GitHub Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Normalize GitHub Recent Data": {
      "main": [
        [
          {
            "node": "Merge GitHub Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Digest with OpenAI": {
      "main": [
        [
          {
            "node": "Parse LLM Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}
