AutomationFlowsAI & RAG › Scheduled Report Digest

Scheduled Report Digest

Scheduled-Report-Digest. Uses googleSheets, anthropic, googleDrive, googleSlides. Scheduled trigger; 12 nodes.

Cron / scheduled trigger★★★★☆ complexityAI-powered12 nodesGoogle SheetsAnthropicGoogle DriveGoogle SlidesGoogle DocsGmailSlack
AI & RAG Trigger: Cron / scheduled Nodes: 12 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Gmail → Google Docs 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
{
  "nodes": [
    {
      "id": "3f9a1c04-5b62-4e18-9a71-1c0d7e2b8a41",
      "name": "Every Monday morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        320
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "id": "7d2e6b19-8c40-4f2a-b135-6ea9c04d3b72",
      "name": "Pull last week's numbers",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        220,
        320
      ],
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1Br1ghtLaneOpsLog2026aBcDeFgHiJkLmNoPqRs"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Weekly Metrics"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "1a8c5f37-2d94-4b60-8e13-9f7a2c60d581",
      "name": "Do the math",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        320
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// ===== EDITABLE RULES BLOCK =====\n// Everything the report cares about lives here. Change these, not the code below.\nconst RULES = {\n  metrics: [\n    { key: 'revenue',          label: 'Revenue',             agg: 'sum', format: 'currency', better: 'up' },\n    { key: 'orders',           label: 'Orders',              agg: 'sum', format: 'number',   better: 'up' },\n    { key: 'new_customers',    label: 'New customers',       agg: 'sum', format: 'number',   better: 'up' },\n    { key: 'avg_order_value',  label: 'Average order value', agg: 'avg', format: 'currency', better: 'up' },\n    { key: 'refund_rate',      label: 'Refund rate',         agg: 'avg', format: 'percent',  better: 'down' },\n    { key: 'late_shipments',   label: 'Late shipments',      agg: 'sum', format: 'number',   better: 'down' },\n  ],\n  // A metric gets flagged when it drops more than drop_pct, or sits above max_value.\n  thresholds: {\n    revenue:        { drop_pct: 10 },\n    orders:         { drop_pct: 15 },\n    new_customers:  { drop_pct: 20 },\n    refund_rate:    { max_value: 4 },\n    late_shipments: { max_value: 25 },\n  },\n  daysPerWeek: 7,\n  topMoverCount: 3,\n  dateColumn: 'date',\n  currency: { symbol: '$', locale: 'en-US', decimals: 0 },\n  percentDecimals: 1,\n};\n// ===== END EDITABLE RULES BLOCK =====\n\nconst rows = $input.all().map((item) => item.json);\n\nfunction toNumber(value) {\n  if (value === null || value === undefined || value === '') return null;\n  const cleaned = String(value).replace(/[^0-9.\\-]/g, '');\n  if (cleaned === '' || cleaned === '-' || cleaned === '.') return null;\n  const parsed = Number(cleaned);\n  return Number.isFinite(parsed) ? parsed : null;\n}\n\nconst sorted = rows\n  .filter((row) => row[RULES.dateColumn])\n  .sort((a, b) => new Date(a[RULES.dateColumn]) - new Date(b[RULES.dateColumn]));\n\nconst span = RULES.daysPerWeek;\nconst thisWeekRows = sorted.slice(-span);\nconst lastWeekRows = sorted.slice(-span * 2, -span);\n\nfunction rollUp(days, metric) {\n  const values = days.map((day) => toNumber(day[metric.key])).filter((v) => v !== null);\n  if (values.length === 0) return null;\n  const total = values.reduce((sum, v) => sum + v, 0);\n  return metric.agg === 'avg' ? total / values.length : total;\n}\n\nfunction show(value, format) {\n  if (value === null) return 'missing';\n  if (format === 'currency') {\n    return RULES.currency.symbol + value.toLocaleString(RULES.currency.locale, {\n      minimumFractionDigits: RULES.currency.decimals,\n      maximumFractionDigits: RULES.currency.decimals,\n    });\n  }\n  if (format === 'percent') return value.toFixed(RULES.percentDecimals) + '%';\n  return Math.round(value).toLocaleString(RULES.currency.locale);\n}\n\nfunction showChange(pct) {\n  if (pct === null) return 'no prior week';\n  return (pct > 0 ? '+' : '') + pct.toFixed(RULES.percentDecimals) + '%';\n}\n\nconst summary = [];\nconst display = {};\nconst missing = [];\n\nfor (const metric of RULES.metrics) {\n  const current = rollUp(thisWeekRows, metric);\n  const prior = rollUp(lastWeekRows, metric);\n  let changePct = null;\n  if (current !== null && prior !== null && prior !== 0) {\n    changePct = ((current - prior) / Math.abs(prior)) * 100;\n  }\n  const line = {\n    key: metric.key,\n    label: metric.label,\n    thisWeek: current,\n    lastWeek: prior,\n    changeAbs: current !== null && prior !== null ? current - prior : null,\n    changePct: changePct === null ? null : Number(changePct.toFixed(RULES.percentDecimals)),\n    thisWeekDisplay: show(current, metric.format),\n    lastWeekDisplay: show(prior, metric.format),\n    changeDisplay: showChange(changePct),\n    goodNews: changePct === null ? null : (metric.better === 'up' ? changePct >= 0 : changePct <= 0),\n  };\n  summary.push(line);\n  display[metric.key] = line.thisWeekDisplay;\n  display[metric.key + '_change'] = line.changeDisplay;\n  if (current === null) missing.push(metric.label);\n}\n\nconst ranked = summary.filter((line) => line.changePct !== null);\nconst moversUp = ranked\n  .slice()\n  .sort((a, b) => b.changePct - a.changePct)\n  .filter((line) => line.changePct > 0)\n  .slice(0, RULES.topMoverCount);\nconst moversDown = ranked\n  .slice()\n  .sort((a, b) => a.changePct - b.changePct)\n  .filter((line) => line.changePct < 0)\n  .slice(0, RULES.topMoverCount);\n\nconst flags = [];\nfor (const metric of RULES.metrics) {\n  const rule = RULES.thresholds[metric.key];\n  const line = summary.find((s) => s.key === metric.key);\n  if (!rule || !line) continue;\n  if (rule.drop_pct !== undefined && line.changePct !== null && line.changePct <= -rule.drop_pct) {\n    flags.push(line.label + ' is down ' + line.changeDisplay + ' on last week, now ' + line.thisWeekDisplay);\n  }\n  if (rule.max_value !== undefined && line.thisWeek !== null && line.thisWeek > rule.max_value) {\n    flags.push(line.label + ' is at ' + line.thisWeekDisplay + ', over the ' + show(rule.max_value, metric.format) + ' line');\n  }\n}\n\nconst revenueLine = summary.find((s) => s.key === 'revenue');\nconst weekRevenueValues = thisWeekRows.map((day) => toNumber(day.revenue)).filter((v) => v !== null);\nconst dailyAverage = weekRevenueValues.length\n  ? weekRevenueValues.reduce((sum, v) => sum + v, 0) / weekRevenueValues.length\n  : null;\n\nconst weekEnding = thisWeekRows.length\n  ? String(thisWeekRows[thisWeekRows.length - 1][RULES.dateColumn]).slice(0, 10)\n  : '';\nconst weekEndingDisplay = weekEnding\n  ? new Date(weekEnding + 'T12:00:00').toLocaleDateString(RULES.currency.locale, {\n      month: 'long', day: 'numeric', year: 'numeric',\n    })\n  : 'unknown';\n\nconst topMover = moversUp.length\n  ? moversUp[0].label + ' ' + moversUp[0].changeDisplay\n  : (moversDown.length ? moversDown[0].label + ' ' + moversDown[0].changeDisplay : 'nothing moved much');\n\nconst numberLines = summary.map((line) =>\n  line.label + ': ' + line.thisWeekDisplay + ' this week, ' + line.lastWeekDisplay + ' last week, ' + line.changeDisplay\n);\n\nconst numbersForWriter = [\n  'Week ending ' + weekEndingDisplay,\n  'Days counted: ' + thisWeekRows.length,\n  '',\n  'Metrics',\n  ...numberLines,\n  '',\n  'Week revenue total: ' + show(revenueLine ? revenueLine.thisWeek : null, 'currency'),\n  'Average revenue per day: ' + show(dailyAverage, 'currency'),\n  '',\n  'Biggest gains: ' + (moversUp.length ? moversUp.map((m) => m.label + ' ' + m.changeDisplay).join(', ') : 'none'),\n  'Biggest drops: ' + (moversDown.length ? moversDown.map((m) => m.label + ' ' + m.changeDisplay).join(', ') : 'none'),\n  '',\n  'Flagged: ' + (flags.length ? flags.join(' | ') : 'nothing past a threshold'),\n  'Missing from the sheet: ' + (missing.length ? missing.join(', ') : 'nothing'),\n].join('\\n');\n\nreturn [{\n  json: {\n    week_ending: weekEnding,\n    week_ending_display: weekEndingDisplay,\n    days_counted: thisWeekRows.length,\n    week_revenue_total: revenueLine ? revenueLine.thisWeek : null,\n    week_revenue_total_display: show(revenueLine ? revenueLine.thisWeek : null, 'currency'),\n    daily_revenue_average: dailyAverage,\n    daily_revenue_average_display: show(dailyAverage, 'currency'),\n    metrics: summary,\n    display,\n    movers_up: moversUp,\n    movers_down: moversDown,\n    top_mover: topMover,\n    flags,\n    flags_text: flags.length ? flags.map((f) => '- ' + f).join('\\n') : '- nothing past a threshold',\n    missing,\n    numbers_for_writer: numbersForWriter,\n  },\n}];\n"
      }
    },
    {
      "id": "9c4b0e26-7a13-45df-8b92-3d61f8a04c17",
      "name": "Write the summary",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        660,
        320
      ],
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "claude-sonnet-5"
        },
        "messages": {
          "values": [
            {
              "role": "user",
              "content": "=Here are last week's finished numbers for Brightlane Supply. Write the summary from these and nothing else.\n\n{{ $json.numbers_for_writer }}"
            }
          ]
        },
        "simplify": false,
        "options": {
          "system": "You write the Monday numbers summary for Brightlane Supply, a wholesale supply company.\n\nRules about numbers, these matter most:\n- Use only the figures in the message below. They are already final.\n- Do not calculate anything. No adding, no averaging, no percentages of your own, no rounding.\n- Copy each figure exactly as written, dollar signs and percent signs included.\n- If a figure you want to mention is not in the list, say it is missing and move on. Never fill the gap with a guess.\n\nHow to write:\n- Plain register, like a smart coworker typing quickly in chat.\n- No corporate words. Skip seamless, robust, leverage, delve, landscape, reaching out.\n- No emojis. No em dashes. No lists of three. Do not end sentences with an -ing clause.\n- Short sentences. Name the thing and move on.\n\nReply with JSON only, nothing before or after it. Keys:\n- headline: one line under 90 characters, the single thing leadership should know\n- what_moved: one paragraph, 3 to 5 sentences, on what went up and down and by how much\n- watch: one line on what to keep an eye on this week",
          "maxTokens": 1024
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "5e73a9d1-6f28-4c05-9d41-b820c7e3f964",
      "name": "Tidy up what Claude wrote",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        320
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Claude replies with JSON. Pull the three pieces out so the deck, the doc,\n// the email and Slack can each grab the bit they need.\nconst raw = $json.content?.[0]?.text ?? $json.text ?? $json.content ?? '';\nconst text = typeof raw === 'string' ? raw : JSON.stringify(raw);\nconst between = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1);\n\nlet written = {};\ntry {\n  written = JSON.parse(between);\n} catch (error) {\n  written = { headline: text.split('\\n')[0], what_moved: text, watch: '' };\n}\n\nconst numbers = $('Do the math').first().json;\n\nreturn [{\n  json: {\n    headline: (written.headline || '').trim(),\n    what_moved: (written.what_moved || '').trim(),\n    watch: (written.watch || '').trim(),\n    week_ending_display: numbers.week_ending_display,\n  },\n}];\n"
      }
    },
    {
      "id": "2b64d8f5-0a37-49e1-8c26-7f13a95d0b48",
      "name": "Start this week's deck from the template",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1100,
        320
      ],
      "parameters": {
        "resource": "file",
        "operation": "copy",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "1BrightlaneWeeklyDeckTemplate2026aBcD"
        },
        "name": "=Brightlane weekly deck, week ending {{ $('Do the math').item.json.week_ending_display }}",
        "sameFolder": false,
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "1BrightlaneReportsFolder2026xYzAbCd"
        },
        "options": {}
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "8a15c3e7-4b90-4d62-a731-05e9b6c28f13",
      "name": "Drop the numbers into the deck",
      "type": "n8n-nodes-base.googleSlides",
      "typeVersion": 2,
      "position": [
        1320,
        320
      ],
      "parameters": {
        "resource": "presentation",
        "operation": "replaceText",
        "presentationId": "={{ $json.id }}",
        "textUi": {
          "textValues": [
            {
              "text": "[[week_ending]]",
              "replaceText": "={{ $('Do the math').item.json.week_ending_display }}"
            },
            {
              "text": "[[revenue]]",
              "replaceText": "={{ $('Do the math').item.json.display.revenue }}"
            },
            {
              "text": "[[orders]]",
              "replaceText": "={{ $('Do the math').item.json.display.orders }}"
            },
            {
              "text": "[[top_mover]]",
              "replaceText": "={{ $('Do the math').item.json.top_mover }}"
            },
            {
              "text": "[[headline]]",
              "replaceText": "={{ $('Tidy up what Claude wrote').item.json.headline }}"
            }
          ]
        },
        "options": {}
      },
      "credentials": {
        "googleSlidesOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "6f30b2a8-9e14-4c73-85d0-2a7c1f4e90b6",
      "name": "Write up the brief in Docs",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        1540,
        320
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "document",
        "operation": "create",
        "driveId": "myDrive",
        "folderId": "1BrightlaneReportsFolder2026xYzAbCd",
        "title": "=Brightlane weekly brief, week ending {{ $('Do the math').item.json.week_ending_display }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "4d81e5c2-3f76-40a9-9b18-6c02d8a7e315",
      "name": "Add the write-up to the brief",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        1760,
        320
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "document",
        "operation": "update",
        "documentURL": "={{ $json.documentId }}",
        "simple": true,
        "actionsUi": {
          "actionFields": [
            {
              "object": "text",
              "action": "insert",
              "insertSegment": "body",
              "locationChoice": "endOfSegmentLocation",
              "text": "=Brightlane Supply weekly brief\nWeek ending {{ $('Do the math').item.json.week_ending_display }}\n\n{{ $('Tidy up what Claude wrote').item.json.headline }}\n\nWhat moved\n{{ $('Tidy up what Claude wrote').item.json.what_moved }}\n\nWatch this week\n{{ $('Tidy up what Claude wrote').item.json.watch }}\n\nThe numbers\n{{ $('Do the math').item.json.numbers_for_writer }}\n\nFlagged\n{{ $('Do the math').item.json.flags_text }}\n"
            }
          ]
        },
        "updateFields": {}
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "0c26f7b4-1a58-4e93-8d07-b591e3a6c284",
      "name": "Send the pack to leadership",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        320
      ],
      "parameters": {
        "sendTo": "leadership@brightlane-supply.example.com",
        "subject": "=Brightlane weekly numbers, week ending {{ $('Do the math').item.json.week_ending_display }}",
        "emailType": "text",
        "message": "=Morning. Last week's pack is ready.\n\n{{ $('Tidy up what Claude wrote').item.json.headline }}\n\n{{ $('Tidy up what Claude wrote').item.json.what_moved }}\n\nWatch this week: {{ $('Tidy up what Claude wrote').item.json.watch }}\n\nDeck: https://docs.google.com/presentation/d/{{ $(\"Start this week's deck from the template\").item.json.id }}/edit\nBrief: https://docs.google.com/document/d/{{ $('Write up the brief in Docs').item.json.documentId }}/edit\n\nFlagged:\n{{ $('Do the math').item.json.flags_text }}\n\nEvery number in the pack came out of the ops sheet. Ping me if a line looks off and I will check the source rows.",
        "options": {
          "senderName": "Brightlane Ops",
          "replyTo": "ops@brightlane-supply.example.com",
          "appendAttribution": false
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "e5b93a70-2c41-4867-9f5a-83d012c74be9",
      "name": "Post the highlights in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2200,
        320
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#leadership"
        },
        "text": "=Weekly numbers are out, week ending {{ $('Do the math').item.json.week_ending_display }}\n\n{{ $('Tidy up what Claude wrote').item.json.headline }}\n\nFlagged:\n{{ $('Do the math').item.json.flags_text }}\n\nDeck and brief are in your inbox.",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "bb47d1e9-58a2-4f36-9c70-4e21b806d5f7",
      "name": "How the pack gets built",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        620,
        0
      ],
      "parameters": {
        "content": "## The math, then the write-up\n\n- The rules block at the top of Do the math holds the metric list, the thresholds and the formatting, so changing what gets flagged is a one line edit.\n- Out the other end: a filled deck, a written brief in Docs, an email to leadership, and the flagged metrics in Slack.",
        "width": 620,
        "height": 240,
        "color": 7
      }
    }
  ],
  "connections": {
    "Every Monday morning": {
      "main": [
        [
          {
            "node": "Pull last week's numbers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pull last week's numbers": {
      "main": [
        [
          {
            "node": "Do the math",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Do the math": {
      "main": [
        [
          {
            "node": "Write the summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the summary": {
      "main": [
        [
          {
            "node": "Tidy up what Claude wrote",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tidy up what Claude wrote": {
      "main": [
        [
          {
            "node": "Start this week's deck from the template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start this week's deck from the template": {
      "main": [
        [
          {
            "node": "Drop the numbers into the deck",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Drop the numbers into the deck": {
      "main": [
        [
          {
            "node": "Write up the brief in Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write up the brief in Docs": {
      "main": [
        [
          {
            "node": "Add the write-up to the brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add the write-up to the brief": {
      "main": [
        [
          {
            "node": "Send the pack to leadership",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the pack to leadership": {
      "main": [
        [
          {
            "node": "Post the highlights in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Scheduled-Report-Digest. Uses googleSheets, anthropic, googleDrive, googleSlides. Scheduled trigger; 12 nodes.

Source: https://github.com/mcruz1799/automation-examples/blob/main/claude-code/02-automation-file-factory/skills/n8n/section-templates/scheduled-report-digest.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Imagine a dedicated financial expert tirelessly working behind the scenes, sifting through every transaction, every investment move, and every accounting entry. That's exactly what this automated syst

HTTP Request, Google Sheets, OpenAI +3
AI & RAG

WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.

Memory Mongo Db Chat, Agent, HTTP Request +14
AI & RAG

Categories Content Creation AI Automation Publishing Social Media

Google Docs, HTTP Request, Slack +7
AI & RAG

This workflow monitors Gmail for Japanese side-gig inquiries, flags risky wording, and uses Google Gemini to extract details and score each request. It logs every opportunity to Google Sheets, then dr

Gmail, Google Drive, Google Docs +4
AI & RAG

Triage Japanese side gigs with Gmail, Gemini, Sheets, Drive, and Calendar. Uses gmail, googleDrive, googleDocs, googleCalendar. Event-driven trigger; 29 nodes.

Gmail, Google Drive, Google Docs +4