AutomationFlowsData & Sheets › Send a Weekly Airtable Task Digest to Slack, Gmail and Sheets with Gpt-4.1-mini

Send a Weekly Airtable Task Digest to Slack, Gmail and Sheets with Gpt-4.1-mini

ByAvkash Kakdiya @itechnotion on n8n.io

This workflow automatically creates a weekly report of tasks. Every Friday morning, it collects all task details from Airtable, checks progress, and prepares a summary. It also highlights important or late tasks and sends the report to Slack, email, and saves it for record.…

Cron / scheduled trigger★★★★☆ complexityAI-powered18 nodesGmailSlackGoogle SheetsOpenAIAirtable
Data & Sheets Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #15872 — we link there as the canonical source.

This workflow follows the Airtable → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "68a4f080-284e-4457-9dc5-402b1f16f962",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2848,
        1104
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 704,
        "content": "## Step 6: Check Urgent Tasks & Alert\nThis step checks if there are any urgent or late tasks. If yes, it sends a special alert message. This alert reminds people to take quick action. If there are no urgent tasks, nothing extra is sent."
      },
      "typeVersion": 1
    },
    {
      "id": "822e5d7f-e3f3-43b6-aa7e-5f9b75334e20",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2848,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 720,
        "content": "## Step 5: Send & Save Report\nThis step sends the weekly report to Slack and email. So everyone can see the update easily. It also saves important data like total tasks and progress into a sheet. This helps track history over time."
      },
      "typeVersion": 1
    },
    {
      "id": "9bea85d9-589b-4468-a668-e4ae68c76e5f",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2400,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 624,
        "content": "## Step 4: Combine Final Report\nThis step combines all information into one final report. It adds the AI summary into the main message. It also prepares special alert messages if there are urgent or late tasks. Everything is now ready to send."
      },
      "typeVersion": 1
    },
    {
      "id": "26efcea7-d0b3-4e1a-b4f1-5407677c6d22",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 624,
        "content": "## Step 3: Generate Smart Summary\nThis step creates a short and clear summary of the weekly report. It explains overall progress in simple sentences. It also highlights important issues like late or urgent tasks. This helps people quickly understand the situation."
      },
      "typeVersion": 1
    },
    {
      "id": "54583244-0db4-44c1-bb21-836142fe354b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 624,
        "content": "## Step 2: Analyze Task Data\nThis step checks all the tasks and understands their condition. It finds how many tasks are completed, pending, late, or urgent. It also prepares a clean report format. If no data is found, it creates a simple message saying no activity."
      },
      "typeVersion": 1
    },
    {
      "id": "d4921d4e-5613-4e8b-b5a4-acbbdfeebbf0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 624,
        "content": "## Step 1: Start & Get Data\nThis step starts the workflow automatically every week. After starting, it collects all the task data from the tracker. It gathers details like task name, status, and other information. This data is then passed to the next step for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "b495ad8f-2955-43aa-a482-a67ec3941c9d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        352
      ],
      "parameters": {
        "width": 512,
        "height": 624,
        "content": "## Airtable Weekly Digest\nThis workflow automatically creates a weekly report of tasks. Every Friday morning, it collects all task details, checks progress, and prepares a summary. It also highlights important or late tasks. Finally, it sends the report to Slack, email, and saves it for record.\n\n### How it works\n\n\t\u2022\tThe process starts automatically every Friday at 9 AM.\n\t\u2022\tIt collects all task data from the tracker.\n\t\u2022\tIt checks which tasks are done, pending, late, or important.\n\t\u2022\tIt creates a simple summary of the whole week.\n\t\u2022\tIt adds a smart short summary using AI.\n\t\u2022\tIt sends the report to Slack and email.\n\t\u2022\tIt saves the report data for future use.\n\n### Setup Steps\n\n\t1.\tSet a fixed time (Friday 9 AM) to run the workflow.\n\t2.\tConnect your task tracker (Airtable).\n\t3.\tAdd steps to analyze and summarize data.\n\t4.\tConnect Slack and Gmail to send reports.\n\t5.\tConnect Google Sheets to save weekly data."
      },
      "typeVersion": 1
    },
    {
      "id": "4e8c181a-c843-465b-8d9b-897d3d7fd2ce",
      "name": "Gmail: Send Urgent Alert",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3184,
        1472
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=<div style=\"font-family: Arial, sans-serif; max-width: 560px; margin: 0 auto;\">\n  <div style=\"background: #d93025; color: white; padding: 24px; border-radius: 10px 10px 0 0; text-align: center;\">\n    <h2 style=\"margin: 0; font-size: 20px;\">\u26a0\ufe0f Action Required</h2>\n    <p style=\"margin: 6px 0 0; opacity: 0.9; font-size: 14px;\">{{ $json.weekRange }}</p>\n  </div>\n  <div style=\"background: white; padding: 24px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;\">\n    <table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-spacing: 8px; border-collapse: separate; margin-bottom: 20px;\">\n      <tr>\n        <td style=\"text-align: center; padding: 16px; background: #fce8e6; border-radius: 8px;\">\n          <div style=\"font-size: 28px; font-weight: bold; color: #d93025;\">{{ $json.overdueCount }}</div>\n          <div style=\"font-size: 12px; color: #555; margin-top: 4px;\">Overdue Tasks</div>\n        </td>\n        <td width=\"4%\"></td>\n        <td style=\"text-align: center; padding: 16px; background: #fff3e0; border-radius: 8px;\">\n          <div style=\"font-size: 28px; font-weight: bold; color: #e37400;\">{{ $json.urgentCount }}</div>\n          <div style=\"font-size: 12px; color: #555; margin-top: 4px;\">Urgent Tasks</div>\n        </td>\n      </tr>\n    </table>\n    <p style=\"color: #333; font-size: 14px; line-height: 1.6;\">Please check Slack <strong>#general</strong> for the full weekly digest and take action on overdue or high-priority items before end of day.</p>\n    <p style=\"color: #bbb; font-size: 11px; text-align: center; margin-top: 20px;\">\ud83e\udd16 n8n automated alert \u2022 {{ $json.runTime }}</p>\n  </div>\n</div>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\u26a0\ufe0f Action Needed: {{ $json.overdueCount }} overdue + {{ $json.urgentCount }} urgent tasks \u2014 {{ $json.weekRange }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "8bb493bb-3376-49eb-b147-50daf57249f2",
      "name": "Slack: DM Urgent Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        3184,
        1232
      ],
      "parameters": {
        "text": "={{ $json.urgentSlackMessage }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c816a843-a389-481c-b7b6-9d79ece58c0e",
      "name": "IF: Has Urgent or Overdue?",
      "type": "n8n-nodes-base.if",
      "position": [
        2960,
        1376
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-urgent-overdue",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.overdueCount + $json.urgentCount }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "fddfcc5d-00d2-49e1-9348-cd2c9eb299d7",
      "name": "Google Sheets: Log Weekly Stats",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2960,
        912
      ],
      "parameters": {
        "columns": {
          "value": {
            "Done": "={{ $json.doneCount }}",
            "Total": "={{ $json.total }}",
            "Status": "={{ $json.weekStatus }}",
            "Urgent": "={{ $json.urgentCount }}",
            "Overdue": "={{ $json.overdueCount }}",
            "Timestamp": "={{ $json.runTime }}",
            "AI Summary": "={{ $json.aiSummary }}",
            "Week Range": "={{ $json.weekRange }}",
            "Completion %": "={{ $json.completionRate }}"
          },
          "schema": [
            {
              "id": "Week Range",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Week Range",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Done",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Done",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Completion %",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Completion %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Overdue",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Overdue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Urgent",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Urgent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "AI Summary",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "AI Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "1a47eb1f-773c-4b27-92d2-b3d27da9203b",
      "name": "Gmail: Send Digest Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2960,
        528
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=<div style=\"font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #f9fafb; padding: 20px;\">\n  <div style=\"background: linear-gradient(135deg, #1a73e8 0%, #1557b0 100%); color: white; padding: 32px 24px; border-radius: 12px 12px 0 0; text-align: center;\">\n    <h1 style=\"margin: 0; font-size: 24px; font-weight: 700;\">\ud83d\udcca Weekly Airtable Digest</h1>\n    <p style=\"margin: 8px 0 0; opacity: 0.9;\">{{ $json.weekRange }}</p>\n  </div>\n  <div style=\"background: white; padding: 32px 24px; border-radius: 0 0 12px 12px; border: 1px solid #e5e7eb;\">\n    <div style=\"background: #f0f7ff; border-left: 4px solid #1a73e8; border-radius: 8px; padding: 16px 20px; margin-bottom: 28px;\">\n      <p style=\"font-size: 13px; font-weight: 700; color: #1e40af; margin: 0 0 6px;\">\ud83e\udd16 AI Summary</p>\n      <p style=\"font-size: 14px; color: #1e3a5f; margin: 0; line-height: 1.6;\">{{ $json.aiSummary }}</p>\n    </div>\n    <table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin-bottom: 28px;\">\n      <tr>\n        <td width=\"48%\" style=\"background: #f0f7ff; border-radius: 10px; padding: 20px; text-align: center;\">\n          <div style=\"font-size: 11px; color: #1e40af; font-weight: 700;\">Total Tasks</div>\n          <div style=\"font-size: 28px; font-weight: 800; color: #1a73e8;\">{{ $json.total }}</div>\n        </td>\n        <td width=\"4%\"></td>\n        <td width=\"48%\" style=\"background: #f0fdf4; border-radius: 10px; padding: 20px; text-align: center;\">\n          <div style=\"font-size: 11px; color: #166534; font-weight: 700;\">Completed</div>\n          <div style=\"font-size: 28px; font-weight: 800; color: #16a34a;\">{{ $json.doneCount }}</div>\n        </td>\n      </tr>\n      <tr><td height=\"12\"></td></tr>\n      <tr>\n        <td width=\"48%\" style=\"background: #fffbeb; border-radius: 10px; padding: 20px; text-align: center;\">\n          <div style=\"font-size: 11px; color: #92400e; font-weight: 700;\">Completion Rate</div>\n          <div style=\"font-size: 28px; font-weight: 800; color: #d97706;\">{{ $json.completionRate }}%</div>\n        </td>\n        <td width=\"4%\"></td>\n        <td width=\"48%\" style=\"background: #fef2f2; border-radius: 10px; padding: 20px; text-align: center;\">\n          <div style=\"font-size: 11px; color: #991b1b; font-weight: 700;\">Overdue</div>\n          <div style=\"font-size: 28px; font-weight: 800; color: #dc2626;\">{{ $json.overdueCount }}</div>\n        </td>\n      </tr>\n    </table>\n    <div>\n      <h3 style=\"font-size: 16px; color: #374151; margin-bottom: 16px;\">\ud83d\udccb Tasks This Week</h3>\n      <table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-size: 14px; border-collapse: collapse;\">\n        <thead>\n          <tr style=\"background: #f8fafc;\">\n            <th style=\"padding: 12px; text-align: left; color: #374151;\">Task Name</th>\n            <th style=\"padding: 12px; text-align: center; color: #374151;\">Status</th>\n            <th style=\"padding: 12px; text-align: right; color: #374151;\">Assignee</th>\n          </tr>\n        </thead>\n        <tbody>\n        {{\n          ($json.topRecords || []).map(rec => {\n            const s = (rec.status || '').toLowerCase();\n            let c = '#d97706';\n            if (s.includes('done') || s.includes('complete')) c = '#16a34a';\n            if (s.includes('overdue')) c = '#dc2626';\n            return `<tr style=\"border-bottom:1px solid #f1f5f9\"><td style=\"padding:12px\">${rec.name}</td><td style=\"padding:12px;text-align:center;font-weight:600;color:${c}\">${rec.status||'\u2014'}</td><td style=\"padding:12px;text-align:right;color:#6b7280\">${rec.assignee||'\u2014'}</td></tr>`;\n          }).join('')\n        }}\n        </tbody>\n      </table>\n    </div>\n    <div style=\"margin-top: 32px; text-align: center; font-size: 12px; color: #9ca3af;\">\n      \ud83e\udd16 n8n Automation \u2022 Report generated at {{ $json.runTime }}\n    </div>\n  </div>\n</div>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\ud83d\udcca Weekly Airtable Digest | {{ $json.weekRange }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "d9a2506b-5653-405f-8f36-45be1eec149c",
      "name": "Slack: Post Weekly Digest",
      "type": "n8n-nodes-base.slack",
      "position": [
        2960,
        720
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "otherOptions": {
          "mrkdwn": true,
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "393d6a59-dce5-420d-84b2-e197a3a48e14",
      "name": "Code: Merge AI Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        720
      ],
      "parameters": {
        "jsCode": "// ================================================================\n// MERGE AI SUMMARY \u2014 v2 POLISHED\n// Reads from the LangChain OpenAI node and injects summary into slackMessage\n// ================================================================\n\nconst codeOutput = $('Code: Analyze & Format').first().json;\n\n// \u2500\u2500 Extract AI summary from all known output paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet aiSummary = 'Summary unavailable.';\ntry {\n  const openAiJson = $('OpenAI: Generate AI Summary').first().json;\n  aiSummary =\n    openAiJson?.message?.content?.trim() ||\n    openAiJson?.choices?.[0]?.message?.content?.trim() ||\n    openAiJson?.content?.[0]?.text?.trim() ||\n    openAiJson?.text?.trim() ||\n    'Summary unavailable.';\n} catch(e) {\n  aiSummary = 'AI summary could not be generated.';\n}\n\n// \u2500\u2500 Inject AI summary into slackMessage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst finalSlackMessage = (codeOutput.slackMessage || '').replace('{{AI_SUMMARY}}', aiSummary);\n\n// \u2500\u2500 Urgent alert Slack message (polished) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst urgentSlackMessage = [\n  `\ud83d\udea8 *Action Required \u2014 Urgent/Overdue Tasks Detected*`,\n  ``,\n  `This week\\'s Airtable digest has flagged items that need immediate attention:`,\n  ``,\n  `\ud83d\udd34 *Overdue Tasks:* ${codeOutput.overdueCount}`,\n  `\u26a1 *Urgent / High-Priority Tasks:* ${codeOutput.urgentCount}`,\n  ``,\n  `Please review the full digest in *#general* and resolve these items before end of day.`,\n  ``,\n  `_Week: ${codeOutput.weekRange} \u2022 ${codeOutput.runTime}_`\n].join('\\n');\n\n// \u2500\u2500 No-activity Slack message \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst noActivitySlackMessage = [\n  `\ud83d\udced *Weekly Airtable Digest | ${codeOutput.weekRange}*`,\n  ``,\n  `No records were found in the tracker this week. All quiet! \u2705`,\n  ``,\n  `_\ud83e\udd16 Auto-posted by n8n every Friday \u2022 ${codeOutput.runTime}_`\n].join('\\n');\n\nreturn [{ json: {\n  ...codeOutput,\n  aiSummary,\n  slackMessage: finalSlackMessage,\n  urgentSlackMessage,\n  noActivitySlackMessage\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4404b6a4-eeb5-43b8-b1ab-29bd54c035b3",
      "name": "OpenAI: Generate AI Summary",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2032,
        720
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {
          "maxTokens": 300,
          "temperature": 0.3
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are a concise, senior project manager assistant. Your job is to write a sharp 2\u20133 sentence plain-English summary of a weekly Airtable project tracker report.\n\nRules:\n- State the completion rate and total tasks in the first sentence.\n- If there are overdue or urgent tasks, name them specifically and flag the risk.\n- End with a single clear action recommendation (e.g. who should be followed up with, or what area needs focus).\n- Tone: direct, professional, no fluff.\n- Format: plain sentences only \u2014 no markdown, no bullet points, no lists."
            },
            {
              "content": "={{ $json.aiSummaryInput }}"
            }
          ]
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "f1389764-fe3b-4c2b-9463-2b819500ec36",
      "name": "Code: Analyze & Format",
      "type": "n8n-nodes-base.code",
      "position": [
        1664,
        720
      ],
      "parameters": {
        "jsCode": "// ================================================================\n// WEEKLY DIGEST PROCESSOR \u2014 v6 POLISHED\n// ================================================================\n\nconst records = $input.all();\nconst now = new Date();\n\nconst META_KEYS = new Set(['id', 'createdTime', 'fields', 'tableId', 'baseId']);\nconst getFields = (record) => {\n  const j = record.json || {};\n  if (j.fields && typeof j.fields === 'object' && Object.keys(j.fields).length > 0) return j.fields;\n  const flat = {};\n  for (const [k, v] of Object.entries(j)) { if (!META_KEYS.has(k)) flat[k] = v; }\n  return flat;\n};\n\n// Week range (Mon\u2013Sun of the current week)\nconst dow = now.getDay();\nconst diffToMon = dow === 0 ? -6 : 1 - dow;\nconst monday = new Date(now);\nmonday.setDate(now.getDate() + diffToMon);\nmonday.setHours(0, 0, 0, 0);\nconst sunday = new Date(monday);\nsunday.setDate(monday.getDate() + 6);\nconst fmt  = d => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\nconst fmtShort = d => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });\nconst weekRange = `${fmt(monday)} \u2013 ${fmt(sunday)}`;\nconst total   = records.length;\nconst runTime = now.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });\n\n// \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (total === 0) {\n  const slackMessage = [\n    `\ud83d\udced *Weekly Airtable Digest | ${weekRange}*`,\n    `No records were found this week. All quiet! \u2705`,\n    ``,\n    `_\ud83e\udd16 Auto-posted by n8n every Friday \u2022 ${runTime}_`\n  ].join('\\n');\n\n  return [{ json: {\n    hasRecords: false, total: 0, doneCount: 0, completionRate: 0,\n    overdueCount: 0, urgentCount: 0, detectedFields: [],\n    weekRange, runTime,\n    slackColor: '#5f6368',\n    slackMessage,\n    aiSummaryInput: 'No records were found in Airtable this week. The project tracker is empty for this period.',\n    topRecords: []\n  }}];\n}\n\n// \u2500\u2500 Field detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sampleFields = getFields(records[0]);\nconst fieldNames   = Object.keys(sampleFields);\nconst find = (candidates) => candidates.find(f => fieldNames.includes(f)) || null;\n\nconst nameField     = find(['Name','Title','Subject','Task','Item','Project','Record Name','Label']) || fieldNames[0];\nconst statusField   = find(['Status','Stage','State','Type','Category','Phase']);\nconst assigneeField = find(['Assignee','Owner','Assigned To','Responsible','Team Member']);\nconst dueField      = find(['Due Date','Due','Deadline','Due By','End Date','Target Date']);\nconst priorityField = find(['Priority','Urgency','Importance','Severity']);\n\nconst resolveVal = (raw) => {\n  if (raw == null || raw === '') return null;\n  if (Array.isArray(raw)) {\n    if (!raw.length) return null;\n    return raw.map(i => typeof i === 'object' ? (i.name || i.email || i.id || '') : String(i)).filter(Boolean).join(', ');\n  }\n  if (typeof raw === 'object') return raw.name || raw.email || raw.id || null;\n  return String(raw);\n};\n\nconst isDone = statusField\n  ? (r) => { const s = resolveVal(getFields(r)[statusField]) || ''; return /done|complete|closed|finished/i.test(s); }\n  : () => false;\n\nconst doneCount       = statusField ? records.filter(isDone).length : 0;\nconst completionRate  = total > 0 ? Math.round((doneCount / total) * 100) : 0;\nconst barFilled       = Math.round(completionRate / 10);\nconst bar             = '\u2588'.repeat(barFilled) + '\u2591'.repeat(10 - barFilled);\n\n// \u2500\u2500 Aggregations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst statusCounts   = {};\nconst assigneeCounts = {};\n\nif (statusField) records.forEach(r => {\n  const k = resolveVal(getFields(r)[statusField]) || 'No Status';\n  statusCounts[k] = (statusCounts[k] || 0) + 1;\n});\n\nif (assigneeField) records.forEach(r => {\n  const raw = getFields(r)[assigneeField];\n  const items = Array.isArray(raw) ? raw : [raw];\n  items.forEach(n => {\n    const k = n == null ? 'Unassigned'\n      : typeof n === 'object' ? (n.name || n.email || 'Unknown')\n      : String(n || 'Unassigned');\n    assigneeCounts[k] = (assigneeCounts[k] || 0) + 1;\n  });\n});\n\n// \u2500\u2500 Overdue detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst overdueItems = [];\nif (dueField && statusField) {\n  records.forEach(r => {\n    const f = getFields(r);\n    const dueRaw = f[dueField]; if (!dueRaw) return;\n    const dueDate = new Date(dueRaw); if (isNaN(dueDate.getTime())) return;\n    if (!isDone(r) && dueDate < now) {\n      const name     = resolveVal(f[nameField]) || r.json.id || '?';\n      const days     = Math.floor((now - dueDate) / 86400000);\n      const assignee = assigneeField ? resolveVal(f[assigneeField]) : null;\n      overdueItems.push({ name, days, assignee });\n    }\n  });\n}\n\n// \u2500\u2500 Urgent detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst urgentItems = [];\nif (priorityField) records.forEach(r => {\n  const f    = getFields(r);\n  const pVal = resolveVal(f[priorityField]) || '';\n  if (/high|urgent|critical|p0|p1/i.test(pVal)) {\n    const name     = resolveVal(f[nameField]) || r.json.id || '?';\n    const assignee = assigneeField ? resolveVal(f[assigneeField]) : null;\n    urgentItems.push({ name, priority: pVal, assignee });\n  }\n});\n\n// \u2500\u2500 Top records \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst TOP_N      = 10;\nconst topRecords = records.slice(0, TOP_N).map((r, i) => {\n  const f = getFields(r);\n  return {\n    name:     resolveVal(f[nameField])     || r.json.id || `Record ${i+1}`,\n    status:   statusField   ? resolveVal(f[statusField])   : null,\n    assignee: assigneeField ? resolveVal(f[assigneeField]) : null,\n    due:      dueField      ? resolveVal(f[dueField])      : null,\n    priority: priorityField ? resolveVal(f[priorityField]) : null\n  };\n});\n\n// \u2500\u2500 Build full Slack message (plain mrkdwn, with {{AI_SUMMARY}} placeholder) \u2500\u2500\nconst recordLines = topRecords.map(rec => {\n  const statusPart   = rec.status   ? ` (${rec.status})`   : '';\n  const assigneePart = rec.assignee ? ` \u2192 ${rec.assignee}` : '';\n  let duePart = '';\n  if (rec.due) {\n    const d = new Date(rec.due);\n    if (!isNaN(d.getTime())) duePart = ` \u2014 ${fmtShort(d)}`;\n    else duePart = ` \u2014 ${rec.due}`;\n  }\n  return `\u2022 ${rec.name}${statusPart}${assigneePart}${duePart}`;\n}).join('\\n');\n\nlet statusBreakdownText = '';\nif (statusField && Object.keys(statusCounts).length > 0) {\n  const rows = Object.entries(statusCounts).sort((a,b) => b[1]-a[1]).map(([k,v]) => `  \u2022 ${k}: ${v}`).join('\\n');\n  statusBreakdownText = `\\n\\n*Breakdown by Status:*\\n${rows}`;\n}\n\nlet assigneeBreakdownText = '';\nif (assigneeField && Object.keys(assigneeCounts).length > 0) {\n  const rows = Object.entries(assigneeCounts).sort((a,b) => b[1]-a[1]).slice(0,5).map(([k,v]) => `  \u2022 ${k}: ${v}`).join('\\n');\n  assigneeBreakdownText = `\\n\\n*Top Contributors:*\\n${rows}`;\n}\n\nlet overdueSection = '';\nif (overdueItems.length > 0) {\n  const lines = overdueItems.map(i => { const ap = i.assignee ? ` \u2192 ${i.assignee}` : ''; return `  \u2022 *${i.name}* \u2014 ${i.days}d overdue${ap}`; }).join('\\n');\n  overdueSection = `\\n\\n*\ud83d\udd34 Overdue Tasks (${overdueItems.length}):*\\n${lines}`;\n}\n\nlet urgentSection = '';\nif (urgentItems.length > 0) {\n  const lines = urgentItems.map(i => { const ap = i.assignee ? ` \u2192 ${i.assignee}` : ''; return `  \u2022 *${i.name}* _(${i.priority})_${ap}`; }).join('\\n');\n  urgentSection = `\\n\\n*\u26a1 Urgent / High Priority (${urgentItems.length}):*\\n${lines}`;\n}\n\nconst moreNote = total > TOP_N ? `\\n_...and ${total - TOP_N} more \u2014 check Airtable for the full list._` : '';\n\nconst slackMessage = [\n  `\ud83d\udcca *Weekly Airtable Digest | ${weekRange}*`,\n  `Total: ${total}   Done: ${doneCount}   Completion: \\`${bar}\\` ${completionRate}%`,\n  `Detected fields: ${fieldNames.join(', ')}`,\n  ``,\n  `*\ud83e\udd16 AI Summary*`,\n  `{{AI_SUMMARY}}`,\n  overdueSection ? overdueSection.trim() : null,\n  urgentSection  ? urgentSection.trim()  : null,\n  ``,\n  `*Records (showing ${Math.min(total, TOP_N)} of ${total}):*`,\n  recordLines,\n  moreNote || null,\n  statusBreakdownText  ? statusBreakdownText.trim()  : null,\n  assigneeBreakdownText ? assigneeBreakdownText.trim() : null,\n  ``,\n  `_\ud83e\udd16 Auto-posted by n8n every Friday \u2022 ${runTime}_`\n].filter(l => l !== null).join('\\n');\n\n// \u2500\u2500 Colour / status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet color      = '#36a64f';\nlet weekStatus = 'Normal Week';\nif (overdueItems.length > 0)  { color = '#d93025'; weekStatus = 'Overdue Tasks'; }\nelse if (urgentItems.length > 0) { color = '#f4b400'; weekStatus = 'High-Priority Tasks'; }\n\n// \u2500\u2500 AI summary input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst statusLines = Object.entries(statusCounts).map(([k,v]) => `${k}: ${v}`).join(', ');\nconst aiSummaryInput = [\n  `Weekly Airtable project tracker digest for ${weekRange}:`,\n  `- Total records: ${total}`,\n  `- Completed: ${doneCount} (${completionRate}%)`,\n  `- Overdue: ${overdueItems.length}${overdueItems.length > 0 ? ' \u2014 ' + overdueItems.map(i => i.name).join(', ') : ''}`,\n  `- Urgent/high-priority: ${urgentItems.length}${urgentItems.length > 0 ? ' \u2014 ' + urgentItems.map(i => i.name).join(', ') : ''}`,\n  `- Status breakdown: ${statusLines || 'N/A'}`,\n  `- Top assignees: ${Object.entries(assigneeCounts).sort((a,b)=>b[1]-a[1]).slice(0,3).map(([k,v])=>`${k} (${v})`).join(', ') || 'N/A'}`,\n  `- Sample tasks: ${topRecords.slice(0,5).map(r => r.name).join(', ')}`\n].join('\\n');\n\nreturn [{ json: {\n  hasRecords: true,\n  total, doneCount, completionRate,\n  overdueCount: overdueItems.length,\n  urgentCount:  urgentItems.length,\n  detectedFields: fieldNames,\n  weekRange, runTime,\n  slackColor: color,\n  weekStatus,\n  slackMessage,\n  aiSummaryInput,\n  topRecords,\n  statusCounts,\n  assigneeCounts\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "686c3ef3-6228-4294-ab74-50b0a3295b55",
      "name": "Airtable: Fetch This Week",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1312,
        720
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {},
        "operation": "search"
      },
      "typeVersion": 2
    },
    {
      "id": "7874cd93-1873-4a1d-a891-3b21a1edcbf5",
      "name": "Schedule: Every Friday 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1120,
        720
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                5
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.1
    }
  ],
  "connections": {
    "Code: Analyze & Format": {
      "main": [
        [
          {
            "node": "OpenAI: Generate AI Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Merge AI Summary": {
      "main": [
        [
          {
            "node": "IF: Has Urgent or Overdue?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Google Sheets: Log Weekly Stats",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail: Send Digest Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack: Post Weekly Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable: Fetch This Week": {
      "main": [
        [
          {
            "node": "Code: Analyze & Format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Has Urgent or Overdue?": {
      "main": [
        [
          {
            "node": "Gmail: Send Urgent Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack: DM Urgent Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: Every Friday 9AM": {
      "main": [
        [
          {
            "node": "Airtable: Fetch This Week",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: Generate AI Summary": {
      "main": [
        [
          {
            "node": "Code: Merge AI Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automatically creates a weekly report of tasks. Every Friday morning, it collects all task details from Airtable, checks progress, and prepares a summary. It also highlights important or late tasks and sends the report to Slack, email, and saves it for record.…

Source: https://n8n.io/workflows/15872/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

This n8n workflow automates the end-to-end client onboarding process: capturing client details, validating emails, assigning tiers, generating welcome packs, creating tasks, notifying teams, archiving

Google Sheets, Gmail, Airtable +5
Data & Sheets

Lead Generating Web Scraper & CRM Automation. Uses httpRequest, airtable, googleSheets, gmail. Scheduled trigger; 38 nodes.

HTTP Request, Airtable, Google Sheets +4
Data & Sheets

Automatically triage Product UAT feedback using AI, route it to the right tools and teams, and close the feedback loop with testers, all in one workflow.

Jira, Slack, Notion +3
Data & Sheets

Splitout Code. Uses httpRequest, stickyNote, manualTrigger, splitOut. Event-driven trigger; 33 nodes.

HTTP Request, OpenAI, Noco Db +3
Data & Sheets

This workflow automates the process of gathering LinkedIn advice articles, extracting their content, and generating unique contributions for each article using an AI model. The contributions are then

HTTP Request, OpenAI, Noco Db +3