{
  "name": "AI Daily Standup Digest",
  "nodes": [
    {
      "parameters": {
        "content": "## AI Daily Standup Digest: Summarize Updates from Sheets to Slack\n\nAutomate your daily standup by having team members submit updates to a Google Sheet, then get an AI-generated summary posted to Slack highlighting blockers and progress.\n\n**Setup time:** 15 minutes  ·  **Level:** Intermediate\n\n**You'll need accounts/credentials for:** Google Sheets, OpenAI, Slack\n\n**Replace these before running:**\n- `REPLACE_WITH_SPREADSHEET`\n- `REPLACE_WITH_CHANNEL`\n\n**Setup steps:**\n1. Connect Google Sheets, OpenAI, and Slack in their respective nodes.\n2. Replace REPLACE_WITH_SPREADSHEET with your Google Sheet ID.\n3. Replace REPLACE_WITH_CHANNEL with your target Slack channel.\n4. Adjust the AI prompt in the OpenAI node to match your team's update format.\n5. Activate the workflow and have team members start submitting updates to the sheet.\n\nFree templates and updates: navkhan103.github.io",
        "height": 300,
        "width": 400,
        "color": 4
      },
      "id": "sticky-setup",
      "name": "Setup — read me first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        -120
      ]
    },
    {
      "parameters": {
        "content": "### How it runs\n\nStarts from **Schedule Trigger**.\n\n1. A Schedule Trigger runs every weekday morning to capture yesterday's updates.\n2. A Google Sheets node reads rows from your team update spreadsheet.\n3. An OpenAI node summarizes the updates, prioritizing blockers and issues.\n4. A Slack node posts the AI-generated digest to your team channel.\n5. The summary includes all blockers first, then progress highlights.",
        "height": 300,
        "width": 400,
        "color": 7
      },
      "id": "sticky-how",
      "name": "How it runs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        220
      ]
    },
    {
      "parameters": {
        "content": "### If something breaks\n\nThe read and AI nodes here **retry 3× automatically** (5s apart), which absorbs rate limits and brief timeouts.\n\nNodes that **send or write deliberately do not retry** — a send that reached the far end but lost its response would otherwise fire twice, so you'd get duplicate messages or duplicate rows.\n\nTo be told when one of those fails, build a workflow starting with an **Error Trigger** node that posts to Slack or email, then set it under **three-dot menu > Settings > Error Workflow**. Do it once and point every workflow at it.",
        "height": 380,
        "width": 400,
        "color": 3
      },
      "id": "sticky-fail",
      "name": "If something breaks",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        560
      ]
    },
    {
      "parameters": {
        "rule": {
          "times": [
            {
              "hour": 9,
              "minute": 0
            }
          ],
          "daysOfMonth": "*",
          "months": "*",
          "weekdays": [
            2,
            3,
            4,
            5,
            6
          ]
        }
      },
      "id": "f9a0b1c2-0001-4000-8000-000000000001",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_SPREADSHEET"
        },
        "sheetName": "Updates",
        "range": "A:Z"
      },
      "id": "f9a0b1c2-0002-4000-8000-000000000002",
      "name": "Read Updates",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Filter for updates from yesterday\nconst yesterday = new Date();\nyesterday.setDate(yesterday.getDate() - 1);\nconst yesterdayStr = yesterday.toISOString().split('T')[0];\n\nconst filteredRows = $input.all().filter(item => {\n  const dateStr = item.json.date || item.json.Date;\n  return dateStr && dateStr.startsWith(yesterdayStr);\n});\n\nif (filteredRows.length === 0) {\n  return [{ json: { message: 'No updates found for yesterday' } }];\n}\n\n// Format the updates for AI processing\nlet updatesText = 'Team updates for ' + yesterdayStr + ':\n';\nfor (const item of filteredRows) {\n  const row = item.json;\n  const name = row.name || row.Name || 'Anonymous';\n  const update = row.update || row.Update || 'No update provided';\n  updatesText += '\n' + name + ': ' + update;\n}\n\nreturn [{ json: { updates: updatesText } }];"
      },
      "id": "f9a0b1c2-0003-4000-8000-000000000003",
      "name": "Filter Yesterday",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.6-luna"
        },
        "messages": {
          "values": [
            {
              "content": "=Summarize the following team updates. First, list all blockers/issues mentioned (if any). Then summarize progress made. Output in this JSON format: {\"blockers\": [\"list\", \"of\", \"blockers\"], \"progress\": [\"list\", \"of\", \"progress\"]}\n\nTeam updates:\n{{ JSON.stringify($input.all().map(i => i.json)) }}",
              "role": "user"
            }
          ]
        }
      },
      "id": "f9a0b1c2-0004-4000-8000-000000000004",
      "name": "Summarize Updates",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        660,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Parse the AI response and format for Slack\ntry {\n  const aiResponse = $input.first().json.message.content;\n  const parsed = JSON.parse(aiResponse);\n  \n  let slackMessage = ':calendar: *Daily Standup Digest* for ' + new Date(Date.now() - 86400000).toLocaleDateString() + '\\n\\n';\n  \n  if (parsed.blockers && parsed.blockers.length > 0) {\n    slackMessage += '*:blocker: BLOCKERS:*\\n';\n    for (const blocker of parsed.blockers) {\n      slackMessage += `- ${blocker}\\n`;\n    }\n    slackMessage += '\\n';\n  } else {\n    slackMessage += '*:white_check_mark: No blockers reported*\\n\\n';\n  }\n  \n  if (parsed.progress && parsed.progress.length > 0) {\n    slackMessage += '*:rocket: PROGRESS:*\\n';\n    for (const progress of parsed.progress) {\n      slackMessage += `- ${progress}\\n`;\n    }\n  }\n  \n  return [{ json: { text: slackMessage } }];\n} catch (error) {\n  return [{ json: { text: 'Error parsing AI response: ' + error.message } }];\n}"
      },
      "id": "f9a0b1c2-0005-4000-8000-000000000005",
      "name": "Format for Slack",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_CHANNEL"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {}
      },
      "id": "f9a0b1c2-0006-4000-8000-000000000006",
      "name": "Post Digest",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1100,
        0
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Read Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Updates": {
      "main": [
        [
          {
            "node": "Filter Yesterday",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Yesterday": {
      "main": [
        [
          {
            "node": "Summarize Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Updates": {
      "main": [
        [
          {
            "node": "Format for Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format for Slack": {
      "main": [
        [
          {
            "node": "Post Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}