This workflow corresponds to n8n.io template #16389 — we link there as the canonical source.
This workflow follows the Agent → Error Trigger 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": "EegiaHUeHSdOLYuc",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Vision AI Receipt & Document Verification",
"tags": [],
"nodes": [
{
"id": "915c1efe-6a38-45d5-bfb3-2e477329adf2",
"name": "Document Upload Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1440,
128
],
"parameters": {
"path": "receipt-verify",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "7423276b-e20c-44eb-b20c-39cfaa2600dc",
"name": "Normalize Input",
"type": "n8n-nodes-base.code",
"position": [
-1232,
128
],
"parameters": {
"jsCode": "const b = ($input.first().json.body) || {};\nreturn [{\n json: {\n documentId: b.document_id || ('doc_' + Date.now()),\n guestId: b.guest_id || 'unknown',\n bookingId: b.booking_id || '',\n imageUrl: b.image_url || b.imageUrl || '',\n docType: b.doc_type || 'receipt',\n expectedAmount: (b.expected_amount != null) ? Number(b.expected_amount) : null,\n expectedCurrency: String(b.currency || 'EUR').toUpperCase(),\n receivedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "3a855dc5-d14b-4fa6-b442-f0d863925abb",
"name": "Parse Extraction",
"type": "n8n-nodes-base.code",
"position": [
-752,
64
],
"parameters": {
"jsCode": "const res = $input.first().json;\nlet content = '{}';\ntry { content = res.choices[0].message.content; } catch (e) {}\nlet parsed = {};\ntry { parsed = JSON.parse(content); } catch (e) { parsed = { confidence: 0 }; }\nconst prev = $node['Normalize Input'].json;\nreturn [{\n json: {\n documentId: prev.documentId,\n guestId: prev.guestId,\n bookingId: prev.bookingId,\n expectedAmount: prev.expectedAmount,\n expectedCurrency: prev.expectedCurrency,\n vendor: parsed.vendor ?? null,\n date: parsed.document_date ?? null,\n total: (parsed.total != null) ? Number(parsed.total) : null,\n currency: parsed.currency ? String(parsed.currency).toUpperCase() : null,\n lineItems: parsed.line_items ?? [],\n confidence: (parsed.confidence != null) ? Number(parsed.confidence) : 0\n }\n}];"
},
"typeVersion": 2
},
{
"id": "2c2be2b9-d0be-4428-885b-e7d4c2435b68",
"name": "Validate Against Booking",
"type": "n8n-nodes-base.code",
"position": [
-512,
64
],
"parameters": {
"jsCode": "const d = $input.first().json;\nconst reasons = [];\nconst tolerance = 0.02;\nif (d.confidence < 0.6) reasons.push('Low extraction confidence');\nif (d.total == null) reasons.push('Total not found');\nif (d.currency && d.expectedCurrency && d.currency !== d.expectedCurrency) {\n reasons.push('Currency mismatch: got ' + d.currency + ' expected ' + d.expectedCurrency);\n}\nif (d.total != null && d.expectedAmount != null) {\n const diff = Math.abs(d.total - d.expectedAmount);\n const allowed = Math.abs(d.expectedAmount) * tolerance;\n if (diff > allowed) reasons.push('Amount mismatch: got ' + d.total + ' expected ' + d.expectedAmount);\n}\nlet verdict = 'APPROVED';\nif (reasons.length > 0) {\n verdict = (d.confidence < 0.6 || d.total == null) ? 'REJECTED' : 'REVIEW';\n}\nreturn [{\n json: Object.assign({}, d, {\n verdict: verdict,\n reasons: reasons.length ? reasons : ['All checks passed'],\n processedAt: new Date().toISOString()\n })\n}];"
},
"typeVersion": 2
},
{
"id": "4e447abe-f3e9-428a-bc86-b7c81a0c5cf2",
"name": "Auto Approve?",
"type": "n8n-nodes-base.if",
"position": [
-272,
64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-approved",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.verdict }}",
"rightValue": "APPROVED"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f6b47b44-f290-4dc2-a2c0-c9353ba152c9",
"name": "Flag For Review",
"type": "n8n-nodes-base.slack",
"position": [
-48,
-16
],
"parameters": {
"text": "=:mag: *Document needs review* ({{ $json.verdict }})\nDocument: {{ $json.documentId }} Booking: {{ $json.bookingId }}\nVendor: {{ $json.vendor }} Total: {{ $json.total }} {{ $json.currency }}\nExpected: {{ $json.expectedAmount }} {{ $json.expectedCurrency }}\nReasons: {{ $json.reasons.join('; ') }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0AK5VBUE1Z",
"cachedResultName": "pricing-alerts"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "8cac6fc5-8c39-40ca-8fb7-02d3901a1d46",
"name": "Build Error Verdict",
"type": "n8n-nodes-base.code",
"position": [
-736,
320
],
"parameters": {
"jsCode": "const prev = $node['Normalize Input'].json;\nreturn [{\n json: {\n documentId: prev.documentId,\n guestId: prev.guestId,\n bookingId: prev.bookingId,\n expectedAmount: prev.expectedAmount,\n expectedCurrency: prev.expectedCurrency,\n vendor: null,\n date: null,\n total: null,\n currency: null,\n lineItems: [],\n confidence: 0,\n verdict: 'ERROR',\n reasons: ['Vision extraction failed after retries'],\n processedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "20255ea7-22ca-42a7-b303-76c541acd279",
"name": "Record Result",
"type": "n8n-nodes-base.googleSheets",
"position": [
208,
128
],
"parameters": {
"columns": {
"value": {
"VELORA APPAREL CO.": "={{ $json.documentId }}"
},
"schema": [
{
"id": "VELORA APPAREL CO.",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "VELORA APPAREL CO.",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"VELORA APPAREL CO."
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1EV68pHdgAY4B5vTAk2djVSGEfXWhaXpSSy2GNjdCjYU/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1EV68pHdgAY4B5vTAk2djVSGEfXWhaXpSSy2GNjdCjYU",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1EV68pHdgAY4B5vTAk2djVSGEfXWhaXpSSy2GNjdCjYU/edit?usp=drivesdk",
"cachedResultName": "Cashflow"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "b707f596-45ba-402f-a3fb-a00d8d0c7d32",
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
448,
128
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ ({ documentId: $json.documentId, verdict: $json.verdict, reasons: $json.reasons }) }}"
},
"typeVersion": 1.1
},
{
"id": "685f0daf-3760-45d5-bb3d-78ecbc5e9b84",
"name": "Error Trigger",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-992,
608
],
"parameters": {},
"typeVersion": 1
},
{
"id": "26943552-536a-4169-9297-5395920b550e",
"name": "Alert Engineering",
"type": "n8n-nodes-base.slack",
"position": [
-752,
608
],
"parameters": {
"text": "=:rotating_light: *Receipt verification workflow failed*\nWorkflow: {{ $json.workflow.name }}\nNode: {{ $json.execution.lastNodeExecuted }}\nError: {{ $json.execution.error.message }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0AKM4U1K25",
"cachedResultName": "hr-team"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "abc87bd5-80c4-4c54-9092-933dda799457",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2224,
-240
],
"parameters": {
"color": 7,
"width": 560,
"height": 1100,
"content": "## \ud83e\uddfe Vision AI Receipt & Document Verification\n\nReceives a receipt or invoice image, reads it with GPT 4o Vision, validates the extracted total and currency against the expected booking values, then auto approves clean documents and flags anything suspicious for human review. Every result is logged to Google Sheets.\n\n**Perfect for:** hospitality finance and operations teams that need to verify guest receipts, deposits and supplier invoices at scale.\n\n---\n\n## How it works\n\n1. **Document Upload Webhook** \u2014 Receives the document image URL plus the expected amount and currency.\n2. **Normalize Input** \u2014 Cleans the payload into a consistent shape.\n3. **GPT Vision Extract** \u2014 Calls the OpenAI vision API to read the document into structured JSON. Retries on failure.\n4. **Parse Extraction** \u2014 Safely parses the model output into typed fields.\n5. **Validate Against Booking** \u2014 Checks total, currency and confidence against the expected values and assigns a verdict.\n6. **Auto Approve?** \u2014 Branches on whether the verdict is APPROVED.\n7. **Flag For Review** \u2014 Posts non approved documents to the finance review channel *(review path)*.\n8. **Build Error Verdict** \u2014 If the vision call fails, builds an ERROR verdict so nothing is silently dropped *(error path)*.\n9. **Record Result** \u2014 Appends the full result to Google Sheets.\n10. **Respond** \u2014 Returns the verdict to the caller.\n11. **Error Trigger \u2192 Alert Engineering** \u2014 Catches any unhandled failure and alerts the team.\n\n---\n\n## Setup (~10 minutes)\n\n1. **OpenAI** \u2014 Connect your key in the *GPT Vision Extract* node (uses the OpenAI credential).\n2. **Slack** \u2014 Connect your account in *Flag For Review* and *Alert Engineering*.\n3. **Google Sheets** \u2014 Connect in *Record Result* and set your sheet id and a tab named Verifications.\n> Send a POST to *Document Upload Webhook* with image_url, expected_amount, currency, guest_id and booking_id. The image URL must be publicly reachable by the OpenAI API."
},
"typeVersion": 1
},
{
"id": "657608e8-b12b-4eef-ae4d-c8cccc9d7733",
"name": "Section Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1536,
-176
],
"parameters": {
"color": 5,
"width": 464,
"height": 480,
"content": "## 1\ufe0f\u20e3 Intake\n\nThe **Document Upload Webhook** receives the document image URL together with the expected amount and currency for the booking. **Normalize Input** flattens the request body into a predictable structure and assigns a document id so every downstream node works from the same fields."
},
"typeVersion": 1
},
{
"id": "6854ae3e-9d15-4c1b-ac8c-9aae0a4e32df",
"name": "Section Vision",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1056,
-192
],
"parameters": {
"color": 3,
"width": 496,
"height": 684,
"content": "## 2\ufe0f\u20e3 Vision Extraction\n\n**GPT Vision Extract** sends the image to the OpenAI vision model and forces a strict JSON response, retrying once on a transient failure. **Parse Extraction** safely parses that response into typed fields. If extraction fails entirely, the error output routes to **Build Error Verdict** so the document is still logged rather than lost."
},
"typeVersion": 1
},
{
"id": "e660d8cd-3b01-4bcd-b28b-7d1c80900961",
"name": "Section Validate",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
-192
],
"parameters": {
"color": 6,
"width": 640,
"height": 684,
"content": "## 3\ufe0f\u20e3 Validation & Routing\n\n**Validate Against Booking** compares the extracted total and currency against the expected values within a 2 percent tolerance and gates on extraction confidence, producing an APPROVED, REVIEW, REJECTED or ERROR verdict. **Auto Approve?** sends clean documents straight to logging and routes everything else to **Flag For Review** in Slack."
},
"typeVersion": 1
},
{
"id": "3a6e51b1-64da-4ee6-a343-e4a492d63675",
"name": "Section Logging",
"type": "n8n-nodes-base.stickyNote",
"position": [
128,
-192
],
"parameters": {
"color": 4,
"width": 540,
"height": 728,
"content": "## 4\ufe0f\u20e3 Logging & Error Handling\n\n**Record Result** appends the vendor, totals, verdict, reasons and confidence to Google Sheets for a full audit trail, and **Respond** returns the verdict to the caller. The **Error Trigger** fires on any unhandled failure and **Alert Engineering** posts the failing node and message to the alerts channel."
},
"typeVersion": 1
},
{
"id": "38960c74-2c2f-40f1-adf1-ca8c449b631a",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-1024,
64
],
"parameters": {
"text": "= 'You are a precise document extraction engine for hotel receipts and invoices. Return ONLY JSON with keys: vendor (string), document_date (YYYY-MM-DD string), total (number), currency (3 letter code), line_items (array of objects with description and amount), confidence (number between 0 and 1). If a field is unreadable use null.' }, { role: 'user', content: [ { type: 'text', text: 'Extract the structured data from this document.' }, { type: 'image_url', image_url: { url: $json.imageUrl } } ] } ] }) }}",
"options": {},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "432df3b2-6d26-4fbc-9380-9e32c587903d",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-1008,
272
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5-mini"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "74bf75f1-64c6-4006-b62f-77a116985f37",
"nodeGroups": [],
"connections": {
"AI Agent": {
"main": [
[
{
"node": "Parse Extraction",
"type": "main",
"index": 0
},
{
"node": "Build Error Verdict",
"type": "main",
"index": 0
}
]
]
},
"Auto Approve?": {
"main": [
[
{
"node": "Record Result",
"type": "main",
"index": 0
}
],
[
{
"node": "Flag For Review",
"type": "main",
"index": 0
},
{
"node": "Record Result",
"type": "main",
"index": 0
}
]
]
},
"Error Trigger": {
"main": [
[
{
"node": "Alert Engineering",
"type": "main",
"index": 0
}
]
]
},
"Record Result": {
"main": [
[
{
"node": "Respond",
"type": "main",
"index": 0
}
]
]
},
"Normalize Input": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Parse Extraction": {
"main": [
[
{
"node": "Validate Against Booking",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Build Error Verdict": {
"main": [
[
{
"node": "Record Result",
"type": "main",
"index": 0
}
]
]
},
"Document Upload Webhook": {
"main": [
[
{
"node": "Normalize Input",
"type": "main",
"index": 0
}
]
]
},
"Validate Against Booking": {
"main": [
[
{
"node": "Auto Approve?",
"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.
googleSheetsOAuth2ApiopenAiApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow receives receipt or invoice image URLs via a webhook, uses OpenAI Vision to extract structured totals and currency, validates them against expected booking values, logs the result to Google Sheets, and posts non-approved cases to Slack for human review. Receives a…
Source: https://n8n.io/workflows/16389/ — 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 n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La
Enhance your support, onboarding, and internal knowledge workflows with an intelligent RAG-powered chatbot that responds using live data stored in Google Sheets. 🤖📚 Built for teams that rely on struct
Stop manually sending follow-ups. This workflow automates your entire cold email outreach with AI-powered personalization, smart scheduling, and automatic reply detection.
This workflow automates customer feedback processing by analyzing sentiment, identifying key issues, generating personalized responses, and escalating critical cases to support teams when required. De
This workflow automates financial reconciliation across multiple data sources such as bank statements, invoices, ERP systems, and CSV uploads.