This workflow follows the Emailsend → 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 →
{
"name": "einvoice-daily-reconcile \u2014 Schedule \u2192 query yesterday \u2192 diff \u2192 finance summary",
"nodes": [
{
"parameters": {
"content": "## einvoice-daily-reconcile (scheduler) \u2014 v0.28.0 hardened\n\n**v0.28.0 fixes (from V&V findings)**\n- workflow settings.timezone = `Asia/Taipei` so 02:00 fires in Taipei time AND `new Date()` in Code nodes uses Taipei-local intent. The yesterday calculation is now done with a Taipei locale string instead of UTC ISO slice that was off-by-one at 02:00 Taipei.\n- HTTP node uses `fullResponse: true` so `statusCode` and `body` exist.\n- The README-promised \"finance summary email\" \u2014 actually shipped as the `Email finance` node at the end of the diff branch.\n\n**Trigger**: Schedule 02:00 (Asia/Taipei).\n\n**Flow**\n1. Calculate yesterday in Asia/Taipei (handles DST n/a + tz drift correctly).\n2. Read Audit Sheet rows; filter to op=issue + appliedAt prefixed with yesterday + status=issued.\n3. Call svc /v1/query per candidate.\n4. Diff against expected `ISSUED` state.\n5. Mismatches \u2192 Mismatches tab + Slack alert.\n6. Always send finance daily summary email with green/mismatch counts.\n\n**Before activating**: replace `__REPLACE_ME__EINVOICE_SVC_URL__`, `__REPLACE_ME__GOOGLE_SHEET_ID__`, `__REPLACE_ME__SLACK_CHANNEL__`, `__REPLACE_ME__FINANCE_EMAIL__`.\n\n---\n\n## \u4e2d\u6587 \u2014 einvoice-daily-reconcile\uff08\u6838\u5fc3 / \u6392\u7a0b\uff09\n\n**v0.28.0 \u4fee\u6b63\u7d00\u9304**\uff1a\n- workflow settings.timezone = `Asia/Taipei`\uff0c\u51cc\u6668 02:00 \u5728\u53f0\u5317\u6642\u9593 fire\uff1bCode \u7bc0\u9ede\u7684\u65e5\u671f\u904b\u7b97\u6539\u7528 `Intl.DateTimeFormat` + \u53f0\u5317 locale\uff0c\u907f\u514d UTC slice \u5728\u53f0\u5317 02:00 \u7b97\u5230\u300c\u524d\u5929\u300d\u7684\u9677\u9631\u3002\n- HTTP \u7bc0\u9ede `fullResponse: true`\uff0c`statusCode` \u771f\u7684\u5b58\u5728\u3002\n- README \u627f\u8afe\u7684\u300c\u8ca1\u52d9\u65e5\u5831 email\u300d\u5be6\u969b\u505a\u51fa\u4f86 \u2014 \u5728 diff \u5206\u652f\u672b\u7aef `Email finance` \u7bc0\u9ede\u3002\n\n**v0.32.0 \u52a0 twin-node**\uff1a`[TEST] Read Audit` Code \u7bc0\u9ede\u70ba sibling\uff0c\u9810\u8a2d disabled\u3002runtime smoke \u6642 toggle \u5169\u500b disabled flag \u5373\u53ef\u5728\u6c92\u6709 Google Sheets credential \u4e0b\u8dd1\u5b8c\u3002\n\n**\u89f8\u767c**\uff1aSchedule 02:00\uff08Asia/Taipei\uff09\u3002\n\n**\u6d41\u7a0b**\uff1a\n1. \u5728\u53f0\u5317\u6642\u5340\u7b97\u51fa\u6628\u5929\uff08\u8655\u7406 DST \u4e0d\u9069\u7528 + tz \u6f02\u79fb\uff09\u3002\n2. \u8b80 Audit Sheet \u5217\uff1bfilter \u51fa op=issue + appliedAt \u958b\u982d\u662f\u6628\u5929 + status=issued\u3002\n3. \u5c0d\u6bcf\u7b46\u547c svc /v1/query\u3002\n4. \u8ddf\u671f\u671b `ISSUED` \u72c0\u614b\u6bd4\u5c0d\u3002\n5. \u4e0d\u4e00\u81f4 \u2192 \u5beb Mismatches tab + Slack \u8b66\u793a\u3002\n6. \u4e00\u5f8b\u5bc4\u8ca1\u52d9\u65e5\u5831 email\uff08\u542b\u7da0 / \u4e0d\u4e00\u81f4\u7b46\u6578\uff09\u3002\n\n**\u4e0a\u7dda\u524d\u9700\u66ff\u63db**\uff1a`__REPLACE_ME__EINVOICE_SVC_URL__`\u3001`__REPLACE_ME__GOOGLE_SHEET_ID__`\u3001`__REPLACE_ME__SLACK_CHANNEL__`\u3001`__REPLACE_ME__FINANCE_EMAIL__`",
"height": 900,
"width": 580
},
"id": "note-1",
"name": "Pattern note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-740,
-380
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"triggerAtHour": 2
}
]
}
},
"id": "sched-1",
"name": "Daily 02:00 Taipei",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
-120,
-40
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Compute yesterday in Asia/Taipei robustly (Intl handles tz, no UTC slice trap).\nconst now = new Date();\nconst taipei = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit' });\nconst todayParts = taipei.formatToParts(now).reduce((acc,p) => { if (p.type !== 'literal') acc[p.type] = p.value; return acc; }, {});\nconst todayTaipei = `${todayParts.year}-${todayParts.month}-${todayParts.day}`;\n// One day back in Taipei = subtract 24h from the Taipei-formatted today.\nconst y = new Date(todayTaipei + 'T00:00:00+08:00');\ny.setUTCDate(y.getUTCDate() - 1);\nconst yParts = taipei.formatToParts(y).reduce((acc,p) => { if (p.type !== 'literal') acc[p.type] = p.value; return acc; }, {});\nconst yesterday = `${yParts.year}-${yParts.month}-${yParts.day}`;\nreturn [{ json: { correlationId: `recon-${yesterday}-${Math.floor(Math.random()*1e6)}`, yesterday, todayTaipei } }];"
},
"id": "stamp-1",
"name": "Stamp yesterday (Taipei)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
120,
-40
]
},
{
"parameters": {
"operation": "read",
"documentId": "__REPLACE_ME__GOOGLE_SHEET_ID__",
"sheetName": "Audit"
},
"id": "read-1",
"name": "Read Audit",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
360,
-40
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// [TEST] Twin-node test injection per code2n8n-pipeline SKILL \u00a71.7.\n// Toggle THIS node disabled=false AND the sibling 'Read Audit' disabled=true\n// to run runtime smoke without a Google Sheets credential.\n// Schema MUST match what 'Read Audit' would return (a list of items shaped\n// like a previously-written 'Audit' tab row).\nreturn [\n { json: { correlationId: 'fake-yest-001', op: 'issue', provider: 'amego', orderId: 'order-yest-001', invoiceNumber: 'AA00000001', status: 'issued', result: JSON.stringify({ invoiceNumber: 'AA00000001' }), appliedAt: '2026-06-18T03:15:00.000Z' } },\n { json: { correlationId: 'fake-yest-002', op: 'issue', provider: 'amego', orderId: 'order-yest-002', invoiceNumber: 'AA00000002', status: 'issued', result: JSON.stringify({ invoiceNumber: 'AA00000002' }), appliedAt: '2026-06-18T09:42:00.000Z' } },\n { json: { correlationId: 'fake-yest-003', op: 'issue', provider: 'amego', orderId: 'order-yest-003', invoiceNumber: 'AA00000999', status: 'issued', result: JSON.stringify({ invoiceNumber: 'AA00000999' }), appliedAt: '2026-06-18T14:20:00.000Z' } }\n];"
},
"id": "read-test-1",
"name": "[TEST] Read Audit",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
360,
80
],
"disabled": true
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const yesterday = $node['Stamp yesterday (Taipei)'].json.yesterday;\nconst correlationId = $node['Stamp yesterday (Taipei)'].json.correlationId;\nconst rows = $items().map(i => i.json);\nconst candidates = rows.filter(r => r.op === 'issue' && (r.appliedAt || '').startsWith(yesterday) && r.status === 'issued');\nif (candidates.length === 0) return [{ json: { correlationId, yesterday, empty: true, total: 0, green: 0, mismatchRows: [] } }];\nreturn candidates.map(json => ({ json: { ...json, correlationId, yesterday } }));"
},
"id": "filter-1",
"name": "Filter to yesterday",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
600,
-40
]
},
{
"parameters": {
"url": "=__REPLACE_ME__EINVOICE_SVC_URL__/v1/query",
"method": "POST",
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ { provider: $json.provider, input: { invoiceNumber: (JSON.parse($json.result || '{}').invoiceNumber) || null, orderId: $json.orderId } } }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"specifyBody": "json"
},
"id": "query-1",
"name": "Query provider",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
840,
-40
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Collapse all queries into one summary item.\nconst items = $items().map(i => i.json);\nconst correlationId = $node['Stamp yesterday (Taipei)'].json.correlationId;\nconst yesterday = $node['Stamp yesterday (Taipei)'].json.yesterday;\nconst mismatchRows = [];\nlet green = 0;\nfor (const it of items) {\n const statusCode = Number(it.statusCode ?? 0);\n const body = it.body ?? it;\n const expected = 'ISSUED';\n const actual = body?.result?.status ?? (statusCode >= 200 && statusCode < 300 ? 'UNKNOWN' : 'NETWORK_OR_ERROR');\n if (statusCode >= 200 && statusCode < 300 && actual === expected) green++;\n else mismatchRows.push({ orderId: it.orderId, provider: it.provider, invoiceNumber: body?.result?.invoiceNumber ?? null, expected, actual });\n}\nreturn [{ json: { correlationId, yesterday, total: items.length, green, mismatch: mismatchRows.length, mismatchRows } }];"
},
"id": "diff-1",
"name": "Summarize diffs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1080,
-40
]
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.mismatch }}",
"operation": "larger",
"value2": 0
}
]
}
},
"id": "if-1",
"name": "Any mismatch?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1320,
-40
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Fan out mismatches into one item each for Sheet append.\nconst d = $json;\nreturn (d.mismatchRows || []).map(m => ({ json: { correlationId: d.correlationId, yesterday: d.yesterday, ...m, detectedAt: new Date().toISOString() } }));"
},
"id": "fanout-1",
"name": "Fan out mismatches",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
-120
]
},
{
"parameters": {
"operation": "append",
"documentId": "__REPLACE_ME__GOOGLE_SHEET_ID__",
"sheetName": "Mismatches",
"columnsUi": {
"columnValues": [
{
"column": "correlationId",
"value": "={{ $json.correlationId }}"
},
{
"column": "orderId",
"value": "={{ $json.orderId }}"
},
{
"column": "provider",
"value": "={{ $json.provider }}"
},
{
"column": "invoiceNumber",
"value": "={{ $json.invoiceNumber }}"
},
{
"column": "expected",
"value": "={{ $json.expected }}"
},
{
"column": "actual",
"value": "={{ $json.actual }}"
},
{
"column": "detectedAt",
"value": "={{ $json.detectedAt }}"
}
]
}
},
"id": "write-mis-1",
"name": "Append mismatches",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1800,
-120
]
},
{
"parameters": {
"channel": "__REPLACE_ME__SLACK_CHANNEL__",
"text": "=\ud83d\udcc9 einvoice reconcile \u00b7 {{ $node['Summarize diffs'].json.yesterday }}\\n{{ $node['Summarize diffs'].json.mismatch }} mismatch / {{ $node['Summarize diffs'].json.total }} total \u00b7 correlation `{{ $node['Summarize diffs'].json.correlationId }}`"
},
"id": "slack-mis-1",
"name": "Slack mismatch alert",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
2040,
-120
]
},
{
"parameters": {
"fromEmail": "noreply@your-company.tw",
"toEmail": "__REPLACE_ME__FINANCE_EMAIL__",
"subject": "=einvoice \u65e5\u5c0d\u5e33 {{ $node['Summarize diffs'].json.yesterday }} \u00b7 {{ $node['Summarize diffs'].json.mismatch }} \u4e0d\u4e00\u81f4 / {{ $node['Summarize diffs'].json.total }} \u7b46",
"text": "=correlationId: {{ $node['Summarize diffs'].json.correlationId }}\\n\u65e5\u671f: {{ $node['Summarize diffs'].json.yesterday }}\\n\u7e3d\u8a08: {{ $node['Summarize diffs'].json.total }} \u7b46\\n\u901a\u904e: {{ $node['Summarize diffs'].json.green }} \u7b46\\n\u4e0d\u4e00\u81f4: {{ $node['Summarize diffs'].json.mismatch }} \u7b46\\n\\n\u8a73\u898b Google Sheet Mismatches tab\u3002",
"options": {}
},
"id": "email-1",
"name": "Email finance",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
2280,
-40
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "return [{ json: { ok: true, summary: $node['Summarize diffs'].json } }];"
},
"id": "noop-1",
"name": "All green \u2014 proceed",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
80
]
}
],
"connections": {
"Daily 02:00 Taipei": {
"main": [
[
{
"node": "Stamp yesterday (Taipei)",
"type": "main",
"index": 0
}
]
]
},
"Stamp yesterday (Taipei)": {
"main": [
[
{
"node": "Read Audit",
"type": "main",
"index": 0
}
]
]
},
"Read Audit": {
"main": [
[
{
"node": "Filter to yesterday",
"type": "main",
"index": 0
}
]
]
},
"[TEST] Read Audit": {
"main": [
[
{
"node": "Filter to yesterday",
"type": "main",
"index": 0
}
]
]
},
"Filter to yesterday": {
"main": [
[
{
"node": "Query provider",
"type": "main",
"index": 0
}
]
]
},
"Query provider": {
"main": [
[
{
"node": "Summarize diffs",
"type": "main",
"index": 0
}
]
]
},
"Summarize diffs": {
"main": [
[
{
"node": "Any mismatch?",
"type": "main",
"index": 0
}
]
]
},
"Any mismatch?": {
"main": [
[
{
"node": "Fan out mismatches",
"type": "main",
"index": 0
}
],
[
{
"node": "All green \u2014 proceed",
"type": "main",
"index": 0
}
]
]
},
"Fan out mismatches": {
"main": [
[
{
"node": "Append mismatches",
"type": "main",
"index": 0
}
]
]
},
"Append mismatches": {
"main": [
[
{
"node": "Slack mismatch alert",
"type": "main",
"index": 0
}
]
]
},
"Slack mismatch alert": {
"main": [
[
{
"node": "Email finance",
"type": "main",
"index": 0
}
]
]
},
"All green \u2014 proceed": {
"main": [
[
{
"node": "Email finance",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"timezone": "Asia/Taipei"
},
"active": false,
"tags": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
einvoice-daily-reconcile — Schedule → query yesterday → diff → finance summary. Uses googleSheets, httpRequest, slack, emailSend. Scheduled trigger; 14 nodes.
Source: https://github.com/MorrisLu-Taipei/TigerAI-A2A-Code2n8n-Skill-Pack/blob/5417647cddf7868cc10e32eacc7fdfdacd68ea4b/examples/einvoice-n8n/workflows/einvoice-daily-reconcile.workflow.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.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Simplify financial oversight with this automated n8n workflow. Triggered daily, it fetches cash flow and expense data from a Google Sheet, analyzes inflows and outflows, validates records, and generat
This workflow automatically monitors competitor affiliate programs twice daily using Bright Data's web scraping API to extract commission rates, cookie durations, average order values, and payout term
Automate your payroll process with this efficient workflow. Triggered monthly on the 28th, it fetches employee data from a Google Sheet, uses AI to calculate net salaries with tax and deductions, stru
debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.