This workflow follows the Google Sheets → HTTP Request 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 →
{
"name": "18 \u00b7 Enterprise: Invoice Analytics \u2014 Daily Commentary & Smart Alert",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 6 * * *"
}
]
}
},
"id": "schedule-daily",
"name": "L\u1ecbch: Ch\u1ea1y H\u1eb1ng Ng\u00e0y",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
160,
300
],
"notes": "Ch\u1ea1y 6:00 s\u00e1ng h\u1eb1ng ng\u00e0y (gi\u1edd VN). \u0110\u1ed5i l\u1ecbch t\u1ea1i cron expression."
},
{
"parameters": {
"mode": "manual",
"includeOtherFields": true,
"assignments": {
"assignments": [
{
"id": "c1",
"name": "google_sheet_id",
"type": "string",
"value": "NHAP_GOOGLE_SHEET_ID"
},
{
"id": "c2",
"name": "fact_sheet_name",
"type": "string",
"value": "Invoice OCR"
},
{
"id": "c3",
"name": "commentary_sheet_name",
"type": "string",
"value": "Commentary"
},
{
"id": "c4",
"name": "anomalies_sheet_name",
"type": "string",
"value": "Anomalies"
},
{
"id": "c5",
"name": "gemini_model",
"type": "string",
"value": "gemini-2.5-flash"
},
{
"id": "c6",
"name": "telegram_chat_id",
"type": "string",
"value": "NHAP_TELEGRAM_CHAT_ID"
},
{
"id": "c7",
"name": "anomaly_pct_threshold",
"type": "number",
"value": 30
}
]
},
"options": {}
},
"id": "config-set-vars",
"name": "C\u1ea5u H\u00ecnh",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
380,
300
],
"notes": "C\u1ea5u h\u00ecnh t\u1eadp trung: Sheet ID, t\u00ean c\u00e1c tab (fact/commentary/anomalies), model Gemini, Telegram chat ID, ng\u01b0\u1ee1ng % b\u1ea5t th\u01b0\u1eddng."
},
{
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.google_sheet_id }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.fact_sheet_name }}",
"mode": "name"
},
"options": {}
},
"id": "read-fact",
"name": "Google Sheets: \u0110\u1ecdc D\u1eef Li\u1ec7u H\u00f3a \u0110\u01a1n",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
600,
300
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "\u0110\u1ecdc to\u00e0n b\u1ed9 tab Invoice OCR (fact table) \u0111\u1ec3 t\u00ednh KPI."
},
{
"parameters": {
"jsCode": "const rows = $input.all().map(r => r.json);\nconst cfg = $('C\u1ea5u H\u00ecnh').first().json;\n\nfunction num(v) {\n const n = Number(String(v ?? '').replace(/[^0-9.-]/g, ''));\n return Number.isFinite(n) ? n : 0;\n}\nfunction truthy(v) { return String(v).toLowerCase() === 'true'; }\n\nconst ym = new Date().toISOString().slice(0, 7);\nconst ymParts = ym.split('-').map(Number);\nconst prevYm = `${ymParts[1] === 1 ? ymParts[0] - 1 : ymParts[0]}-${String(ymParts[1] === 1 ? 12 : ymParts[1] - 1).padStart(2, '0')}`;\n\nfunction monthRows(m) { return rows.filter(r => String(r['Year Month'] || '') === m); }\n\nfunction agg(rs) {\n const spend = rs.reduce((s, r) => s + num(r['Total Amount VND']), 0);\n const count = rs.length;\n const needReview = rs.filter(r => String(r['Status']) === 'manual_review' || truthy(r['Needs Manual Review'])).length;\n const highValue = rs.filter(r => truthy(r['High Value'])).length;\n const failed = rs.filter(r => String(r['Status']) === 'ocr_failed').length;\n const byVendor = {};\n for (const r of rs) {\n const v = String(r['Vendor Canonical'] || r['Seller Name'] || '(kh\u00f4ng r\u00f5)');\n byVendor[v] = (byVendor[v] || 0) + num(r['Total Amount VND']);\n }\n const top = Object.entries(byVendor).sort((a, b) => b[1] - a[1])[0];\n return { spend, count, avg: count ? Math.round(spend / count) : 0, needReview, highValue, failed,\n topVendor: top ? top[0] : '(kh\u00f4ng r\u00f5)', topVendorSpend: top ? top[1] : 0 };\n}\n\nconst cur = agg(monthRows(ym));\nconst prv = agg(monthRows(prevYm));\nconst spendDeltaPct = prv.spend > 0 ? Math.round(((cur.spend - prv.spend) / prv.spend) * 100) : null;\nconst anomalyThreshold = Number(cfg.anomaly_pct_threshold) || 30;\nconst isAnomaly = spendDeltaPct !== null && Math.abs(spendDeltaPct) >= anomalyThreshold;\nconst model = String(cfg.gemini_model || 'gemini-2.5-flash');\n\nconst prompt = `B\u1ea1n l\u00e0 Senior Data Analyst c\u1ee7a ph\u00f2ng k\u1ebf to\u00e1n. Vi\u1ebft commentary 100-150 t\u1eeb ti\u1ebfng Vi\u1ec7t cho dashboard chi ti\u00eau h\u00f3a \u0111\u01a1n.\n\nDATA TH\u00c1NG ${ym}:\n- T\u1ed5ng chi (VND): ${cur.spend.toLocaleString('vi-VN')} (th\u00e1ng tr\u01b0\u1edbc ${prevYm}: ${prv.spend.toLocaleString('vi-VN')}${spendDeltaPct !== null ? `, thay \u0111\u1ed5i ${spendDeltaPct > 0 ? '+' : ''}${spendDeltaPct}%` : ''})\n- S\u1ed1 h\u00f3a \u0111\u01a1n: ${cur.count} | Gi\u00e1 tr\u1ecb trung b\u00ecnh: ${cur.avg.toLocaleString('vi-VN')} VND\n- Nh\u00e0 cung c\u1ea5p chi nhi\u1ec1u nh\u1ea5t: ${cur.topVendor} (${cur.topVendorSpend.toLocaleString('vi-VN')} VND)\n- H\u00f3a \u0111\u01a1n gi\u00e1 tr\u1ecb l\u1edbn: ${cur.highValue} | C\u1ea7n ki\u1ec3m tra l\u1ea1i: ${cur.needReview} | L\u1ed7i OCR: ${cur.failed}\n\nQUY T\u1eaeC VI\u1ebeT:\n- Tone professional, kh\u00f4ng alarmist, kh\u00f4ng b\u1ecba insight.\n- M\u1edf \u0111\u1ea7u b\u1eb1ng con s\u1ed1 quan tr\u1ecdng nh\u1ea5t + so s\u00e1nh th\u00e1ng tr\u01b0\u1edbc.\n- 1-2 nh\u1eadn \u0111\u1ecbnh \u0111\u00e1ng ch\u00fa \u00fd (vendor t\u1eadp trung chi, ch\u1ea5t l\u01b0\u1ee3ng OCR, xu h\u01b0\u1edbng chi ti\u00eau).\n- K\u1ebft: 1-2 h\u00e0nh \u0111\u1ed9ng c\u1ee5 th\u1ec3 cho k\u1ebf to\u00e1n/qu\u1ea3n l\u00fd.\n- N\u1ebfu m\u1ecdi th\u1ee9 b\u00ecnh th\u01b0\u1eddng th\u00ec n\u00f3i ng\u1eafn g\u1ecdn, \u0111\u1eebng b\u1ecba \"insight\".\n- KH\u00d4NG markdown, KH\u00d4NG emoji. Ch\u1ec9 tr\u1ea3 text thu\u1ea7n 100-150 t\u1eeb.`;\n\nreturn [{\n json: {\n metrics: {\n thang: ym, thang_truoc: prevYm,\n chi_thang_nay: cur.spend, chi_thang_truoc: prv.spend, delta_pct: spendDeltaPct,\n so_hoa_don: cur.count, gia_tri_tb: cur.avg,\n can_review: cur.needReview, hoa_don_lon: cur.highValue, loi_ocr: cur.failed,\n top_vendor: cur.topVendor, top_vendor_chi: cur.topVendorSpend\n },\n is_anomaly: isAnomaly,\n spend_delta_pct: spendDeltaPct,\n gemini_model: model,\n gemini_request: {\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n generationConfig: { temperature: 0.3, maxOutputTokens: 1024 }\n }\n }\n}];"
},
"id": "calc-kpi",
"name": "T\u00ednh KPI & B\u1ea5t Th\u01b0\u1eddng",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
820,
300
],
"notes": "T\u00ednh KPI th\u00e1ng n\u00e0y vs th\u00e1ng tr\u01b0\u1edbc, top vendor, ph\u00e1t hi\u1ec7n b\u1ea5t th\u01b0\u1eddng, t\u1ea1o prompt cho Gemini."
},
{
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/{{ $json.gemini_model }}:generateContent",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googlePalmApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.gemini_request }}",
"options": {
"timeout": 60000
}
},
"id": "gemini-commentary",
"name": "Gemini: Vi\u1ebft Nh\u1eadn \u0110\u1ecbnh",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1040,
300
],
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000,
"onError": "continueRegularOutput",
"notes": "G\u1ecdi Gemini sinh nh\u1eadn \u0111\u1ecbnh ti\u1ebfng Vi\u1ec7t. C\u00f3 retry; n\u1ebfu fail v\u1eabn ghi commentary fallback."
},
{
"parameters": {
"jsCode": "const resp = $input.first().json;\nconst kpi = $('T\u00ednh KPI & B\u1ea5t Th\u01b0\u1eddng').first().json;\nconst m = kpi.metrics;\n\nfunction esc(v) { return String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }\n\nlet commentary = (resp?.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();\nif (!commentary) {\n commentary = 'Kh\u00f4ng t\u1ea1o \u0111\u01b0\u1ee3c nh\u1eadn \u0111\u1ecbnh t\u1ef1 \u0111\u1ed9ng (Gemini kh\u00f4ng tr\u1ea3 n\u1ed9i dung). Vui l\u00f2ng xem s\u1ed1 li\u1ec7u tr\u1ef1c ti\u1ebfp tr\u00ean dashboard.';\n}\n\nconst today = new Date().toISOString().slice(0, 10);\nconst now = new Date().toISOString();\n\nconst commentaryRow = {\n 'Date': today,\n 'Period': m.thang,\n 'Commentary': commentary,\n 'Total Spend VND': m.chi_thang_nay,\n 'Invoice Count': m.so_hoa_don,\n 'Avg Value VND': m.gia_tri_tb,\n 'Delta vs Prev %': m.delta_pct,\n 'Needs Review': m.can_review,\n 'OCR Failed': m.loi_ocr,\n 'Top Vendor': m.top_vendor,\n 'Generated At': now\n};\n\nconst out = { commentary, commentary_row: commentaryRow, is_anomaly: Boolean(kpi.is_anomaly) };\n\nif (kpi.is_anomaly) {\n const delta = kpi.spend_delta_pct;\n const sign = delta > 0 ? 't\u0103ng' : 'gi\u1ea3m';\n const severity = Math.abs(delta) >= 50 ? 'HIGH' : 'MEDIUM';\n out.anomaly_row = {\n 'Date': today,\n 'Period': m.thang,\n 'Metric': 'T\u1ed5ng chi ti\u00eau th\u00e1ng',\n 'Value VND': m.chi_thang_nay,\n 'Baseline VND': m.chi_thang_truoc,\n 'Delta %': delta,\n 'Severity': severity,\n 'Note': `Chi ti\u00eau th\u00e1ng ${m.thang} ${sign} ${Math.abs(delta)}% so v\u1edbi ${m.thang_truoc}.`\n };\n out.telegram_message = `\ud83d\udea8 <b>C\u1ea2NH B\u00c1O CHI TI\u00caU B\u1ea4T TH\u01af\u1edcNG</b>\\n\\n<b>K\u1ef3:</b> ${esc(m.thang)}\\n<b>T\u1ed5ng chi:</b> ${Number(m.chi_thang_nay).toLocaleString('vi-VN')} VND (${sign} ${Math.abs(delta)}% vs ${esc(m.thang_truoc)})\\n<b>S\u1ed1 h\u00f3a \u0111\u01a1n:</b> ${m.so_hoa_don}\\n<b>M\u1ee9c \u0111\u1ed9:</b> ${severity}\\n\\n${esc(commentary)}`;\n}\n\nreturn [{ json: out }];"
},
"id": "prepare-write",
"name": "Chu\u1ea9n B\u1ecb Ghi Dashboard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1260,
300
],
"notes": "L\u1ea5y text commentary, d\u1ef1ng row Commentary + row Anomaly + message Telegram."
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.google_sheet_id }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.commentary_sheet_name }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Date": "={{ $json.commentary_row['Date'] }}",
"Period": "={{ $json.commentary_row['Period'] }}",
"Commentary": "={{ $json.commentary_row['Commentary'] }}",
"Total Spend VND": "={{ $json.commentary_row['Total Spend VND'] }}",
"Invoice Count": "={{ $json.commentary_row['Invoice Count'] }}",
"Avg Value VND": "={{ $json.commentary_row['Avg Value VND'] }}",
"Delta vs Prev %": "={{ $json.commentary_row['Delta vs Prev %'] }}",
"Needs Review": "={{ $json.commentary_row['Needs Review'] }}",
"OCR Failed": "={{ $json.commentary_row['OCR Failed'] }}",
"Top Vendor": "={{ $json.commentary_row['Top Vendor'] }}",
"Generated At": "={{ $json.commentary_row['Generated At'] }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "append-commentary",
"name": "Google Sheets: Ghi Commentary",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1480,
300
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "Append 1 d\u00f2ng nh\u1eadn \u0111\u1ecbnh v\u00e0o tab Commentary. Looker hi\u1ec3n th\u1ecb d\u00f2ng m\u1edbi nh\u1ea5t."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "anomaly",
"leftValue": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.is_anomaly }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-anomaly",
"name": "IF: C\u00f3 B\u1ea5t Th\u01b0\u1eddng?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1700,
300
],
"notes": "R\u1ebd nh\u00e1nh khi chi ti\u00eau th\u00e1ng v\u01b0\u1ee3t ng\u01b0\u1ee1ng % b\u1ea5t th\u01b0\u1eddng."
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.google_sheet_id }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "={{ $('C\u1ea5u H\u00ecnh').first().json.anomalies_sheet_name }}",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Date": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Date'] }}",
"Period": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Period'] }}",
"Metric": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Metric'] }}",
"Value VND": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Value VND'] }}",
"Baseline VND": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Baseline VND'] }}",
"Delta %": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Delta %'] }}",
"Severity": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Severity'] }}",
"Note": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.anomaly_row['Note'] }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "append-anomaly",
"name": "Google Sheets: Ghi Anomaly",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1920,
200
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "Ghi log b\u1ea5t th\u01b0\u1eddng v\u00e0o tab Anomalies."
},
{
"parameters": {
"chatId": "={{ $('C\u1ea5u H\u00ecnh').first().json.telegram_chat_id }}",
"text": "={{ $('Chu\u1ea9n B\u1ecb Ghi Dashboard').first().json.telegram_message }}",
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false
}
},
"id": "telegram-anomaly",
"name": "Telegram: C\u1ea3nh B\u00e1o B\u1ea5t Th\u01b0\u1eddng",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1920,
400
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"notes": "Smart alert: chi ti\u00eau b\u1ea5t th\u01b0\u1eddng k\u00e8m nh\u1eadn \u0111\u1ecbnh AI."
}
],
"connections": {
"L\u1ecbch: Ch\u1ea1y H\u1eb1ng Ng\u00e0y": {
"main": [
[
{
"node": "C\u1ea5u H\u00ecnh",
"type": "main",
"index": 0
}
]
]
},
"C\u1ea5u H\u00ecnh": {
"main": [
[
{
"node": "Google Sheets: \u0110\u1ecdc D\u1eef Li\u1ec7u H\u00f3a \u0110\u01a1n",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets: \u0110\u1ecdc D\u1eef Li\u1ec7u H\u00f3a \u0110\u01a1n": {
"main": [
[
{
"node": "T\u00ednh KPI & B\u1ea5t Th\u01b0\u1eddng",
"type": "main",
"index": 0
}
]
]
},
"T\u00ednh KPI & B\u1ea5t Th\u01b0\u1eddng": {
"main": [
[
{
"node": "Gemini: Vi\u1ebft Nh\u1eadn \u0110\u1ecbnh",
"type": "main",
"index": 0
}
]
]
},
"Gemini: Vi\u1ebft Nh\u1eadn \u0110\u1ecbnh": {
"main": [
[
{
"node": "Chu\u1ea9n B\u1ecb Ghi Dashboard",
"type": "main",
"index": 0
}
]
]
},
"Chu\u1ea9n B\u1ecb Ghi Dashboard": {
"main": [
[
{
"node": "Google Sheets: Ghi Commentary",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets: Ghi Commentary": {
"main": [
[
{
"node": "IF: C\u00f3 B\u1ea5t Th\u01b0\u1eddng?",
"type": "main",
"index": 0
}
]
]
},
"IF: C\u00f3 B\u1ea5t Th\u01b0\u1eddng?": {
"main": [
[
{
"node": "Google Sheets: Ghi Anomaly",
"type": "main",
"index": 0
},
{
"node": "Telegram: C\u1ea3nh B\u00e1o B\u1ea5t Th\u01b0\u1eddng",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"timezone": "Asia/Ho_Chi_Minh"
},
"staticData": null,
"tags": [
"enterprise",
"invoice",
"analytics",
"commentary",
"gemini",
"google-sheets",
"telegram"
],
"triggerCount": 1,
"versionId": "2.0.0",
"meta": {
"templateCredsSetupCompleted": false
},
"id": "wf-18-invoice-analytics-commentary",
"description": "Schedule daily -> \u0111\u1ecdc fact sheet -> t\u00ednh KPI th\u00e1ng + so s\u00e1nh th\u00e1ng tr\u01b0\u1edbc -> Gemini vi\u1ebft nh\u1eadn \u0111\u1ecbnh ti\u1ebfng Vi\u1ec7t -> ghi tab Commentary; n\u1ebfu chi ti\u00eau b\u1ea5t th\u01b0\u1eddng th\u00ec ghi tab Anomalies + Telegram smart alert. Feed cho dashboard Looker Studio."
}
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.
googlePalmApigoogleSheetsOAuth2ApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
18 · Enterprise: Invoice Analytics — Daily Commentary & Smart Alert. Uses googleSheets, httpRequest, telegram. Scheduled trigger; 10 nodes.
Source: https://github.com/congdinh2008/n8n-compose/blob/981f311fd5a6a76e50dbc259afccd1bdf346a833/workflows/enterprise/18-invoice-ocr-vision/workflow-v2-analytics-commentary.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.
⚠️ Heads up: this is satire. The "Hell Yeah!" workflow is a parody of "automate your whole life with AI agents" grindset content. The API endpoints are fictional and the function nodes are illustrativ
This workflow tracks a configurable crypto watchlist using the CoinGecko API, sends Telegram alerts when price, % change, or volume-spike conditions are met (with optional RSI filtering), optionally l
This workflow continuously monitors the Meta Ads Library for new creatives from a specific competitor pages, logs them into Google Sheets, and sends a concise Telegram notification with the number of
> n8n, Binance API, Google Sheets, Slack, Telegram, Jira & Email
This workflow is ideal for marketers, product managers, competitive intelligence teams, and anyone who needs to track changes on web pages — whether it's competitor pricing, job postings, policy updat