This workflow corresponds to n8n.io template #15791 — we link there as the canonical source.
This workflow follows the Chainllm → Google Sheets 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 →
{
"id": "BiuZNdtI4xXOIGOe",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Weekly Business Metrics Report",
"tags": [],
"nodes": [
{
"id": "95d2d463-71fd-4102-9ab0-85b25ea82e18",
"name": "Every Monday at 08:00",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
8736,
1808
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "656803b3-9dbb-4eb7-9f68-fa19b2696421",
"name": "Compute Date Windows",
"type": "n8n-nodes-base.code",
"position": [
8960,
1808
],
"parameters": {
"jsCode": "// Timezone-aware date windows using Luxon (available in n8n >= 0.214).\n// Add REPORT_TIMEZONE to your n8n environment variables, e.g. 'Europe/Helsinki'.\n// Falls back to UTC if the env var is absent.\nconst { DateTime } = require('luxon');\nconst tz = $env.REPORT_TIMEZONE || 'UTC';\n\n// 'today' in the business timezone at the moment the workflow runs (Monday 08:00).\n// today.weekday === 1 (ISO Monday). startOf('week') gives this week's Monday,\n// minus({ weeks: 1 }) steps back to last week's Monday.\nconst today = DateTime.now().setZone(tz);\nconst lastMonday = today.startOf('week').minus({ weeks: 1 });\nconst lastSunday = lastMonday.endOf('week'); // Sun 23:59:59.999 in business TZ\n\nconst fmt = dt => dt.toISODate(); // YYYY-MM-DD in chosen TZ\nconst toUnix = dt => Math.floor(dt.toMillis() / 1000);\n\nreturn [{ json: {\n weekLabel: `${fmt(lastMonday)} to ${fmt(lastSunday)}`,\n timezone: tz,\n thisWeek: {\n start: lastMonday.toISO(),\n end: lastSunday.toISO(),\n startUnix: toUnix(lastMonday),\n endUnix: toUnix(lastSunday),\n startMs: lastMonday.toMillis(),\n endMs: lastSunday.toMillis()\n }\n} }];"
},
"typeVersion": 2
},
{
"id": "57e98e9a-9d2a-4865-a956-a7b6747230f6",
"name": "Stripe: This Week Revenue",
"type": "n8n-nodes-base.code",
"position": [
9200,
1808
],
"parameters": {
"jsCode": "const startUnix = $('Compute Date Windows').first().json.thisWeek.startUnix;\nconst endUnix = $('Compute Date Windows').first().json.thisWeek.endUnix;\nconst apiKey = $env.STRIPE_SECRET_KEY;\nconst PAGE_SIZE = 100;\nlet allCharges = [];\nlet startingAfter = null;\nlet hasMore = true;\nwhile (hasMore) {\n let url = `https://api.stripe.com/v1/charges?created[gte]=${startUnix}&created[lte]=${endUnix}&limit=${PAGE_SIZE}`;\n if (startingAfter) url += `&starting_after=${startingAfter}`;\n const resp = await $http.request({ method: 'GET', url, headers: { Authorization: `Bearer ${apiKey}` }, json: true });\n const page = resp.data || [];\n allCharges = allCharges.concat(page);\n hasMore = resp.has_more === true;\n if (hasMore && page.length > 0) { startingAfter = page[page.length - 1].id; } else { hasMore = false; }\n}\nreturn [{ json: { data: allCharges, has_more: false, object: 'list' } }];"
},
"typeVersion": 2
},
{
"id": "14648e6a-5cba-40e2-a233-288d77851486",
"name": "Notion: Pipeline This Week",
"type": "n8n-nodes-base.code",
"position": [
9424,
1808
],
"parameters": {
"jsCode": "const dbId = $env.NOTION_DEALS_DB_ID;\nconst apiKey = $env.NOTION_API_KEY;\nconst startIso = $('Compute Date Windows').first().json.thisWeek.start;\nconst endIso = $('Compute Date Windows').first().json.thisWeek.end;\nconst headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' };\nlet allPages = [];\nlet startCursor = undefined;\nlet hasMore = true;\nwhile (hasMore) {\n const body = { filter: { and: [{ property: 'Created', date: { on_or_after: startIso } }, { property: 'Created', date: { on_or_before: endIso } }] }, page_size: 100 };\n if (startCursor) body.start_cursor = startCursor;\n const resp = await $http.request({ method: 'POST', url: `https://api.notion.com/v1/databases/${dbId}/query`, headers, body: JSON.stringify(body), json: true });\n allPages = allPages.concat(resp.results || []);\n hasMore = resp.has_more === true;\n startCursor = resp.next_cursor || undefined;\n if (hasMore) await new Promise(r => setTimeout(r, 350));\n}\nconst results = allPages.map(page => { const p = page.properties || {}; return { id: page.id, properties: { amount: p['Deal Value']?.number ?? 0, dealstage: p['Stage']?.select?.name ?? 'unknown', hs_deal_stage_probability: p['Probability']?.number ?? 0, dealname: p['Name']?.title?.[0]?.plain_text ?? '' } }; });\nreturn [{ json: { results, has_more: false, object: 'list' } }];"
},
"typeVersion": 2
},
{
"id": "34f6c853-7829-4336-b531-83d2d425f3d9",
"name": "GSheets: Ops Metrics",
"type": "n8n-nodes-base.googleSheets",
"position": [
9648,
1808
],
"parameters": {
"options": {
"dataLocationOnSheet": {
"values": {
"range": "={{ $env.GSHEETS_LAST_ROW_RANGE }}",
"rangeDefinition": "specifyRangeA1"
}
}
},
"sheetName": {
"mode": "name",
"value": "={{ $env.GSHEETS_SHEET_NAME }}"
},
"documentId": {
"mode": "id",
"value": "={{ $env.GSHEETS_SPREADSHEET_ID }}"
}
},
"typeVersion": 4.5
},
{
"id": "0d635913-c3d8-45a1-93fb-1357e888983e",
"name": "Aggregate All Metrics",
"type": "n8n-nodes-base.code",
"position": [
10112,
1808
],
"parameters": {
"jsCode": "const dates = $('Compute Date Windows').first().json;\nconst stripeData = $('Stripe: This Week Revenue').first().json;\nconst notionData = $('Notion: Pipeline This Week').first().json;\nconst sheetsItems = $('GSheets: Ops Metrics').all();\nconst charges = stripeData.data || [];\nconst grossRevenue = charges.filter(c => c.status === 'succeeded' && !c.refunded).reduce((s, c) => s + c.amount, 0) / 100;\nconst refunds = charges.filter(c => c.refunded).reduce((s, c) => s + (c.amount_refunded || 0), 0) / 100;\nconst netRevenue = grossRevenue - refunds;\nconst newCharges = charges.filter(c => c.status === 'succeeded').length;\nconst deals = notionData.results || [];\nconst pipelineValue = deals.reduce((s, d) => s + (d.properties?.amount || 0), 0);\nconst weightedValue = deals.reduce((s, d) => { const amt = d.properties?.amount || 0; const prob = d.properties?.hs_deal_stage_probability || 0; return s + (amt * prob / 100); }, 0);\nconst dealsByStage = deals.reduce((acc, d) => { const stage = d.properties?.dealstage || 'unknown'; acc[stage] = (acc[stage] || 0) + 1; return acc; }, {});\nconst latestOps = sheetsItems.length > 0 ? sheetsItems[sheetsItems.length - 1].json : {};\nreturn [{ json: { weekLabel: dates.weekLabel, revenue: { gross: grossRevenue.toFixed(2), refunds: refunds.toFixed(2), net: netRevenue.toFixed(2), transactions: newCharges }, pipeline: { dealsCreated: deals.length, totalValue: pipelineValue.toFixed(2), weightedValue: weightedValue.toFixed(2), byStage: dealsByStage }, ops: { ticketsClosed: latestOps.tickets_closed || 'N/A', npsScore: latestOps.nps_score || 'N/A', uptimePct: latestOps.uptime_pct || 'N/A', supportHours: latestOps.support_hours || 'N/A' }, generatedAt: new Date().toISOString() } }];"
},
"typeVersion": 2
},
{
"id": "c7cf5900-f1db-4527-87c3-2cad1c069fea",
"name": "Claude: Write Executive Narrative",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
10288,
1808
],
"parameters": {
"text": "=You are a senior business analyst. Write a concise 3-paragraph executive narrative for a weekly business report. Be direct, data-driven, and highlight the most important trend. Use plain prose only \u2014 no bullet points, no headers.\n\nData for week {{ $('Aggregate All Metrics').first().json.weekLabel }}:\n\nREVENUE: Gross \u20ac{{ $('Aggregate All Metrics').first().json.revenue.gross }}, Net \u20ac{{ $('Aggregate All Metrics').first().json.revenue.net }}, Refunds \u20ac{{ $('Aggregate All Metrics').first().json.revenue.refunds }}, Transactions {{ $('Aggregate All Metrics').first().json.revenue.transactions }}\n\nPIPELINE: {{ $('Aggregate All Metrics').first().json.pipeline.dealsCreated }} new deals, total value \u20ac{{ $('Aggregate All Metrics').first().json.pipeline.totalValue }}, weighted \u20ac{{ $('Aggregate All Metrics').first().json.pipeline.weightedValue }}, stages: {{ JSON.stringify($('Aggregate All Metrics').first().json.pipeline.byStage) }}\n\nOPS: Tickets closed {{ $('Aggregate All Metrics').first().json.ops.ticketsClosed }}, NPS {{ $('Aggregate All Metrics').first().json.ops.npsScore }}, Uptime {{ $('Aggregate All Metrics').first().json.ops.uptimePct }}%\n\nParagraph 1: Revenue performance. Paragraph 2: Pipeline health. Paragraph 3: Operational highlights + one forward-looking observation.",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "cdb7c087-ea94-4874-b803-4b7bae0bb8b2",
"name": "Anthropic Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
10288,
1952
],
"parameters": {
"model": "claude-sonnet-4-20250514",
"options": {
"temperature": 0
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "b7c57bf4-351f-48e7-b87b-0d2310c95b41",
"name": "Build HTML Report",
"type": "n8n-nodes-base.code",
"position": [
10928,
1808
],
"parameters": {
"jsCode": "const metrics = $('Aggregate All Metrics').first().json;\nconst narrative = $input.first().json.text || 'Narrative unavailable.';\nconst stageRows = Object.entries(metrics.pipeline.byStage || {}).map(([s, c]) => `<tr><td>${s}</td><td>${c}</td></tr>`).join('');\nconst html = `<html><body>Report for ${metrics.weekLabel}</body></html>`;\nreturn [{ json: { ...metrics, narrative, html } }];"
},
"typeVersion": 2
},
{
"id": "afda275d-a83f-42ad-9817-849ca8f85491",
"name": "Send via SendGrid",
"type": "n8n-nodes-base.sendGrid",
"position": [
11152,
1808
],
"parameters": {
"subject": "=\ud83d\udcca Weekly Business Report \u2014 {{ $json.weekLabel }}",
"toEmail": "={{ $env.REPORT_EMAIL_TO }}",
"fromName": "ProFlow Weekly Report",
"resource": "mail",
"fromEmail": "={{ $env.REPORT_EMAIL_FROM }}",
"contentType": "html",
"additionalFields": {}
},
"typeVersion": 1
},
{
"id": "7d8918e8-83d3-45c2-b963-390d7868d86b",
"name": "Slack: Report Sent",
"type": "n8n-nodes-base.slack",
"position": [
11376,
1808
],
"parameters": {
"text": "=\ud83d\udcca Weekly report sent",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.SLACK_CHANNEL_ID }}"
},
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "0a9061cb-fd11-42e3-813e-33d1e04c7de0",
"name": "Workflow Error Trigger",
"type": "n8n-nodes-base.errorTrigger",
"position": [
8720,
2256
],
"parameters": {},
"typeVersion": 1
},
{
"id": "a4c7fe64-b130-4f7c-a850-27c1fb77bd2d",
"name": "Send Error Email",
"type": "n8n-nodes-base.sendGrid",
"position": [
8928,
2256
],
"parameters": {
"subject": "=\u274c Weekly Report Generation Failed [{{ $json.execution.id }}]",
"toEmail": "={{ $env.REPORT_ALERT_EMAIL || $env.REPORT_EMAIL_FROM }}",
"fromName": "ProFlow Automations",
"resource": "mail",
"fromEmail": "={{ $env.REPORT_EMAIL_FROM }}",
"contentType": "text",
"additionalFields": {}
},
"typeVersion": 1
},
{
"id": "5b7fdd57-c259-4561-91d5-f0124bb24710",
"name": "README",
"type": "n8n-nodes-base.stickyNote",
"position": [
8080,
1648
],
"parameters": {
"color": 2,
"width": 540,
"height": 748,
"content": "## Weekly Business Intelligence Report\n\n### Claude-Powered Executive Summary\n\n### How it works\n1. **Trigger:** Runs every Monday at 08:00. Computes the previous week's date window in your configured timezone.\n2. **Collect:** Fetches data from three sources in sequence \u2014 Stripe (paginated charge history), Notion (CRM pipeline), and Google Sheets (ops metrics).\n3. **Aggregate:** Normalises all data into a single structured item: net/gross revenue, refunds, deal counts, weighted pipeline value, and operational KPIs.\n4. **Write:** Claude Sonnet 4 writes a 3-paragraph executive narrative \u2014 revenue performance, pipeline health, and operational highlights with a forward-looking observation.\n5. **Deliver:** Composes a styled HTML email report and sends it via SendGrid. A Slack confirmation fires with a one-line summary.\n\n### Setup steps\n- [x] Anthropic Chat Model connected (Claude Sonnet 4)\n- [ ] Connect SendGrid credential to Send via SendGrid and Send Error Email nodes\n- [ ] Connect Google Sheets OAuth2 credential to GSheets: Ops Metrics\n- [ ] Connect Slack credential to Slack: Report Sent\n- [ ] Set `STRIPE_SECRET_KEY` env var\n- [ ] Set `NOTION_API_KEY` and `NOTION_DEALS_DB_ID` env vars\n- [ ] Set `GSHEETS_SPREADSHEET_ID`, `GSHEETS_SHEET_NAME`, `GSHEETS_LAST_ROW_RANGE` env vars\n- [ ] Set `REPORT_EMAIL_FROM` and `REPORT_EMAIL_TO` env vars\n- [ ] Set `REPORT_ALERT_EMAIL` env var (error alert recipient)\n- [ ] Set `SLACK_CHANNEL_ID` env var\n- [ ] Set `REPORT_TIMEZONE` env var (e.g. `Europe/Helsinki`, defaults to UTC)\n\n"
},
"typeVersion": 1
},
{
"id": "caa71043-6b77-4148-b1b2-374936696160",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
8656,
1648
],
"parameters": {
"color": 7,
"width": 460,
"height": 444,
"content": "## Trigger & Date Setup\nFires every Monday at 08:00. Computes last week's start and end as ISO timestamps, Unix epochs, and milliseconds \u2014 timezone-aware via Luxon."
},
"typeVersion": 1
},
{
"id": "56ae43c5-df59-4979-9da9-e64ee2e36953",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
9152,
1648
],
"parameters": {
"color": 7,
"width": 652,
"height": 444,
"content": "## Data Collection\nFetches data from three sources in sequence. Stripe uses cursor-based pagination to retrieve all charges in the window. Notion queries the Deals database with a date filter and rate-limit delay. Google Sheets reads the latest ops metrics row."
},
"typeVersion": 1
},
{
"id": "e43f6442-d399-4aa7-bb9e-cc78c35261e6",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
9840,
1648
],
"parameters": {
"color": 7,
"width": 992,
"height": 448,
"content": "## Aggregation & AI Narrative\nNormalises all three data sources into a single structured item. Claude Sonnet 4 then writes a 3-paragraph executive narrative \u2014 revenue performance, pipeline health, and operational highlights with a forward-looking observation. Temperature is set to 0 for consistent tone."
},
"typeVersion": 1
},
{
"id": "f9567f5d-55c6-429e-a9db-c69285f14d76",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
10864,
1648
],
"parameters": {
"color": 7,
"width": 748,
"height": 444,
"content": "## Report Build & Delivery\nAssembles a styled HTML email with KPI cards, pipeline stage breakdown, and the AI narrative. Sends via SendGrid, then fires a Slack confirmation with net revenue and deal count."
},
"typeVersion": 1
},
{
"id": "abba7fe3-bb3f-458b-8cb8-e6b4f6464c7f",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
8656,
2128
],
"parameters": {
"color": 7,
"width": 620,
"height": 272,
"content": "## Global Error Failsafe\nIndependent trigger that catches any workflow execution failure and sends an error email via SendGrid with the execution ID and failed node name."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "0299340d-9543-4f7b-ae60-15074c28e257",
"connections": {
"Build HTML Report": {
"main": [
[
{
"node": "Send via SendGrid",
"type": "main",
"index": 0
}
]
]
},
"Send via SendGrid": {
"main": [
[
{
"node": "Slack: Report Sent",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model": {
"ai_languageModel": [
[
{
"node": "Claude: Write Executive Narrative",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Compute Date Windows": {
"main": [
[
{
"node": "Stripe: This Week Revenue",
"type": "main",
"index": 0
}
]
]
},
"GSheets: Ops Metrics": {
"main": [
[
{
"node": "Aggregate All Metrics",
"type": "main",
"index": 0
}
]
]
},
"Aggregate All Metrics": {
"main": [
[
{
"node": "Claude: Write Executive Narrative",
"type": "main",
"index": 0
}
]
]
},
"Every Monday at 08:00": {
"main": [
[
{
"node": "Compute Date Windows",
"type": "main",
"index": 0
}
]
]
},
"Workflow Error Trigger": {
"main": [
[
{
"node": "Send Error Email",
"type": "main",
"index": 0
}
]
]
},
"Stripe: This Week Revenue": {
"main": [
[
{
"node": "Notion: Pipeline This Week",
"type": "main",
"index": 0
}
]
]
},
"Notion: Pipeline This Week": {
"main": [
[
{
"node": "GSheets: Ops Metrics",
"type": "main",
"index": 0
}
]
]
},
"Claude: Write Executive Narrative": {
"main": [
[
{
"node": "Build HTML Report",
"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.
anthropicApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Stop compiling your weekly business report by hand. Let automation pull the numbers, and let Claude write the narrative.
Source: https://n8n.io/workflows/15791/ — 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.
Lead Generating Web Scraper & CRM Automation. Uses httpRequest, airtable, googleSheets, gmail. Scheduled trigger; 38 nodes.
Splitout Code. Uses httpRequest, splitOut, lmChatAnthropic, spotify. Scheduled trigger; 37 nodes.
This n8n template gives ecommerce brands a fully automated review intelligence system — running every morning to scrape, analyze, and report on what customers are actually saying across every platform
This n8n template automatically monitors news sources daily, analyzes article sentiment using AI, and delivers structured intelligence reports to your team — all without any manual reading. It uses Mr
Revenue operations teams, SaaS growth managers, and sales directors who need automated weekly insights from their Stripe payment data. Perfect for small to medium businesses tracking subscription reve