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 →
{
"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.
anthropicApigmailOAuth2googleDocsOAuth2ApigoogleDriveOAuth2ApigoogleSheetsOAuth2ApigoogleSlidesOAuth2ApislackApi
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.
Categories Content Creation AI Automation Publishing Social Media
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
Triage Japanese side gigs with Gmail, Gemini, Sheets, Drive, and Calendar. Uses gmail, googleDrive, googleDocs, googleCalendar. Event-driven trigger; 29 nodes.