This workflow corresponds to n8n.io template #15694 β we link there as the canonical source.
This workflow follows the Gmail β Gmail 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": "OpKu0lBYH8fjHJZw",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Invoice OCR Pipeline Automation",
"tags": [],
"nodes": [
{
"id": "38181a69-951e-45f4-8cd0-ea1965203ffa",
"name": "\ud83d\udce7 Gmail \u2014 Watch for Invoice Emails",
"type": "n8n-nodes-base.gmailTrigger",
"notes": "Polls Gmail every minute for unread emails with 'invoice' in subject. Captures all attachments as binary fields.",
"position": [
-2240,
896
],
"parameters": {
"simple": false,
"filters": {
"sender": "user@example.com"
},
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "9bf735e6-8d44-4b76-91ee-0445280fd7e5",
"name": "\u2753 Has PDF Attachment?",
"type": "n8n-nodes-base.if",
"notes": "Only continue if email has PDF/image attachments. Emails without attachments are silently skipped.",
"position": [
-1824,
896
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-001",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.has_attachment }}",
"rightValue": "true"
}
]
}
},
"typeVersion": 2
},
{
"id": "02e15b78-e0e2-4c0f-ae91-f5801ba600a3",
"name": "\ud83d\udcce Gmail \u2014 Download Invoice Attachment",
"type": "n8n-nodes-base.gmail",
"notes": "Downloads the full attachment binary so Mistral OCR can process it.",
"position": [
-1584,
832
],
"parameters": {
"simple": false,
"options": {
"downloadAttachments": true
},
"messageId": "={{ $json.id }}",
"operation": "get"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "a8420d4d-1554-40b1-8d08-0c7d64c49cff",
"name": "\ud83e\udd16 GPT-4o \u2014 Extract Structured Invoice Fields",
"type": "@n8n/n8n-nodes-langchain.openAi",
"notes": "Sends OCR text to GPT-4o with a strict JSON schema prompt. Temperature=0 for deterministic extraction. Returns structured invoice data.",
"position": [
-400,
832
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {
"temperature": 0
},
"messages": {
"values": [
{
"content": "=Validate this invoice and return JSON in exactly this structure:\n\nINPUT INVOICE:\n{{ JSON.stringify($json) }}\n\nRETURN THIS EXACT JSON STRUCTURE:\n{\n \"invoice_number\": \"value\",\n \"invoice_date\": \"value\",\n \"due_date\": \"value\",\n \"vendor_name\": \"value\",\n \"vendor_gstin\": \"value\",\n \"bill_to\": \"value\",\n \"subtotal\": number,\n \"tax_amount\": number,\n \"total_amount\": number,\n \"currency\": \"value\",\n \"line_items_count\": number,\n \"line_items_total\": number,\n \"payment_terms\": \"value or null\",\n \"po_number\": \"value or null\",\n \"status\": \"APPROVED\" or \"NEEDS_REVIEW\",\n \"flags\": [\"array of issue strings, empty if none\"],\n \"summary\": \"one sentence plain English summary of this invoice\"\n}\n\nRules for status:\n- APPROVED if all validations pass\n- NEEDS_REVIEW if any flags exist"
},
{
"role": "system",
"content": "You are an invoice validation and summarization assistant for a finance team.\n\nYou receive structured invoice JSON data and must:\n1. Validate all fields for completeness and correctness\n2. Flag any issues or anomalies\n3. Return a clean validation report in JSON\n\nValidation rules:\n- invoice_number must be present\n- invoice_date and due_date must be valid dates\n- due_date must be after invoice_date\n- total_amount must equal subtotal + tax_amount\n- vendor_gstin must match Indian GST format: 2 digits + 5 letters + 4 digits + 1 letter + 1 alphanumeric + Z + 1 alphanumeric\n- line_items amounts must add up to subtotal\n- flag null fields as warnings\n\nAlways return ONLY valid JSON. No markdown, no explanation."
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "948dd4e4-7083-4382-9b7c-2afab788e521",
"name": "\u2705 Parse & Validate Invoice Data",
"type": "n8n-nodes-base.code",
"notes": "Parses GPT-4o JSON output. Validates: missing fields, high-value threshold (>1L INR), date sanity, math consistency, GSTIN format. Tags status as AUTO_APPROVED or NEEDS_REVIEW.",
"position": [
32,
832
],
"parameters": {
"jsCode": "// Parse GPT-4o JSON output safely\nconst raw = $input.item.json.text || $input.item.json.message?.content || '';\n\nlet parsed;\ntry {\n // Strip any accidental markdown fences\n const cleaned = raw.replace(/```json/g, '').replace(/```/g, '').trim();\n parsed = JSON.parse(cleaned);\n} catch (e) {\n return [{ json: { parse_error: true, raw_text: raw, error: e.message } }];\n}\n\n// --- VALIDATION LOGIC ---\nconst flags = [];\nconst now = new Date();\n\n// 1. Missing critical fields\nif (!parsed.invoice_number) flags.push('MISSING_INVOICE_NUMBER');\nif (!parsed.vendor?.name) flags.push('MISSING_VENDOR_NAME');\nif (!parsed.total_amount || parsed.total_amount === 0) flags.push('MISSING_OR_ZERO_TOTAL');\nif (!parsed.invoice_date) flags.push('MISSING_INVOICE_DATE');\n\n// 2. Amount threshold check (flag if > 1,00,000 INR for extra approval)\nif (parsed.total_amount > 100000) flags.push('HIGH_VALUE_INVOICE');\n\n// 3. Date sanity check\nif (parsed.invoice_date) {\n const invDate = new Date(parsed.invoice_date);\n const monthsAgo = (now - invDate) / (1000 * 60 * 60 * 24 * 30);\n if (monthsAgo > 6) flags.push('STALE_INVOICE_OVER_6_MONTHS');\n if (invDate > now) flags.push('FUTURE_DATED_INVOICE');\n}\n\n// 4. Due date check\nif (parsed.due_date) {\n const due = new Date(parsed.due_date);\n if (due < now) flags.push('OVERDUE_INVOICE');\n}\n\n// 5. Math validation \u2014 subtotal + tax should ~= total\nif (parsed.subtotal && parsed.tax_amount && parsed.total_amount) {\n const expected = parsed.subtotal + parsed.tax_amount - (parsed.discount || 0);\n const diff = Math.abs(expected - parsed.total_amount);\n if (diff > 1) flags.push(`AMOUNT_MISMATCH_DIFF_${diff.toFixed(2)}`);\n}\n\n// 6. GSTIN format check (Indian GST: 15 alphanumeric)\nif (parsed.vendor?.gstin) {\n const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/;\n if (!gstinRegex.test(parsed.vendor.gstin)) flags.push('INVALID_GSTIN_FORMAT');\n}\n\n// Determine approval route\nconst needsHumanReview = flags.length > 0;\nconst status = needsHumanReview ? 'NEEDS_REVIEW' : 'AUTO_APPROVED';\n\nreturn [{\n json: {\n ...parsed,\n _meta: {\n status,\n flags,\n processed_at: new Date().toISOString(),\n source_email_subject: $('Gmail \u2014 Watch Invoice Emails').item.json.subject || '',\n source_email_from: $('Gmail \u2014 Watch Invoice Emails').item.json.from || ''\n }\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "10db344b-eaf4-4094-8277-daf37d42c470",
"name": "\u2753 Auto-Approved or Needs Review?",
"type": "n8n-nodes-base.if",
"notes": "Routes clean invoices to auto-processing. Flagged invoices go to Slack approval thread.",
"position": [
432,
832
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-002",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json._meta.status }}",
"rightValue": "AUTO_APPROVED"
}
]
}
},
"typeVersion": 2
},
{
"id": "8f2571a8-2570-408a-add8-a11785103d08",
"name": "\ud83d\udcca Google Sheets \u2014 Log Approved Invoice",
"type": "n8n-nodes-base.googleSheets",
"notes": "Appends invoice to master Google Sheet tracker. All 16 fields including status and flags. Used as the single source of truth for finance team.",
"position": [
672,
736
],
"parameters": {
"columns": {
"value": {},
"schema": [],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1463078804,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0/edit#gid=1463078804",
"cachedResultName": "Sheet4"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0/edit?usp=drivesdk",
"cachedResultName": "IT Allocation"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "1aecc27c-2a5a-4bec-adfe-b9807395f52c",
"name": "\ud83d\udd14 Slack \u2014 Notify: Invoice Auto-Approved",
"type": "n8n-nodes-base.slack",
"notes": "Posts clean approval summary to #finance-invoices. No action needed from finance team.",
"position": [
912,
688
],
"parameters": {
"text": "=\u2705 *Invoice Auto-Approved*\n\n*Vendor:* {{ $json.vendor?.name }}\n*Invoice #:* {{ $json.invoice_number }}\n*Date:* {{ $json.invoice_date }}\n*Due:* {{ $json.due_date || 'Not specified' }}\n*Amount:* {{ $json.currency || 'INR' }} {{ $json.total_amount?.toLocaleString('en-IN') }}\n*GSTIN:* {{ $json.vendor?.gstin || 'N/A' }}\n\n_Auto-logged to Google Sheets \u2713_",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0B0FH02CH2",
"cachedResultName": "all-anuj"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "2eeb896c-e40a-4dd8-bdff-29d008a2c44e",
"name": "\ud83d\udea9 Slack \u2014 Flag Invoice for Human Review",
"type": "n8n-nodes-base.slack",
"notes": "Posts flagged invoice to #finance-approvals with all flags listed. Human must react with \u2705 or \u274c emoji to approve/reject.",
"position": [
640,
1088
],
"parameters": {
"text": "=\u26a0\ufe0f *Invoice Needs Human Review*\n\n*From:* {{ $json._meta.source_email_from }}\n*Vendor:* {{ $json.vendor?.name || 'UNKNOWN' }}\n*Invoice #:* {{ $json.invoice_number || 'NOT FOUND' }}\n*Amount:* {{ $json.currency || 'INR' }} {{ $json.total_amount || 0 }}\n*Date:* {{ $json.invoice_date || 'NOT FOUND' }}\n\n*\ud83d\udea9 Flags Raised:*\n{{ $json._meta.flags?.map(f => '\u2022 ' + f).join('\\n') }}\n\n*Action Required:*\n\ud83d\udc4d React with \u2705 to approve and log to Sheets\n\ud83d\udc4e React with \u274c to reject and notify sender\n\n_Processed at {{ $json._meta.processed_at }}_",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0B0FH02CH2",
"cachedResultName": "all-anuj"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "ebc0aa11-3956-4a70-8fad-a67eb4978d73",
"name": "\ud83d\udccb Google Sheets \u2014 Log Flagged Invoice",
"type": "n8n-nodes-base.googleSheets",
"notes": "Also logs flagged invoices to Sheets with NEEDS_REVIEW status so nothing is lost.",
"position": [
912,
976
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "invoice_number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "invoice_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "invoice_date",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "invoice_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "due_date",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "due_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "vendor_name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "vendor_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "vendor_gstin",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "vendor_gstin",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "bill_to",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "bill_to",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "subtotal",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "subtotal",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "tax_amount",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "tax_amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "total_amount",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "total_amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "currency",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "line_items_count",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "line_items_count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "line_items_total",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "line_items_total",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "payment_terms",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "payment_terms",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "po_number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "po_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "flags",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "summary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "_meta",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "_meta",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1463078804,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0/edit#gid=1463078804",
"cachedResultName": "Sheet4"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fiSPjKE-ELzgTJdNyfE1t8z5_Z7JeE6bBUUTcCjkBH0/edit?usp=drivesdk",
"cachedResultName": "IT Allocation"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "2a25b1ec-279f-455b-8b75-598c1b21e849",
"name": "\ud83d\udea8 Slack \u2014 Alert: No Attachment Found",
"type": "n8n-nodes-base.slack",
"notes": "Notifies finance team when an invoice email arrives with no attachment so nothing slips through.",
"position": [
-1584,
1056
],
"parameters": {
"text": "=\u2139\ufe0f Invoice email received from {{ $json.from }} but *no PDF attachment found*.\n\nSubject: {{ $json.subject }}\n\nPlease forward with attachment attached.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0B0FH02CH2",
"cachedResultName": "all-anuj"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "4bab0786-8e32-4edf-bea8-4951d69de20e",
"name": "\ud83d\udd27 Prepare Email Metadata",
"type": "n8n-nodes-base.code",
"position": [
-2016,
896
],
"parameters": {
"jsCode": "// Code node (JavaScript) \u2014 paste into \"Run Once for Each Item\"\nconst headers = $input.item.json.headers || {};\n\n// Signal 1: Outlook \"has attachment\" header\nconst outlookFlag = (headers[\"x-ms-has-attach\"] || \"\")\n .toLowerCase().includes(\"yes\");\n\n// Signal 2: MIME multipart/mixed (universal)\nconst mimeFlag = (headers[\"content-type\"] || \"\")\n .toLowerCase().includes(\"multipart/mixed\");\n\n// Signal 3: sizeEstimate > 50KB suggests binary attachment\nconst sizeFlag = ($input.item.json.sizeEstimate || 0) > 50000;\n\nconst hasAttachment = outlookFlag || mimeFlag;\n\nreturn [{\n json: {\n ...$input.item.json, // pass all original fields through\n has_attachment: hasAttachment,\n attachment_signals: {\n outlook_header: outlookFlag,\n mime_multipart: mimeFlag,\n large_size: sizeFlag\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "399bf4f1-3aa9-4964-913e-ed1e927fafce",
"name": "\ud83c\udf10 Mistral OCR \u2014 Parse Invoice Image",
"type": "n8n-nodes-base.httpRequest",
"position": [
-944,
832
],
"parameters": {
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=YOUR_TOKEN_HERE",
"method": "POST",
"options": {},
"jsonBody": "={\n \"contents\": [{\n \"parts\": [\n {\n \"inline_data\": {\n \"mime_type\": \"application/pdf\",\n \"data\": \"{{ $json.data }}\"\n }\n },\n {\n \"text\": \"Extract all invoice fields and return ONLY valid JSON: invoice_number, invoice_date, due_date, vendor_name, vendor_gstin, bill_to, subtotal, tax_amount, total_amount, currency, line_items, payment_terms, po_number. Use null for missing fields. No markdown, no explanation.\"\n }\n ]\n }]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "x-goog-api-key",
"value": "AIzaYOUR_GOOGLE_API_KEY_HERE"
}
]
}
},
"typeVersion": 4.4
},
{
"id": "edf56afe-4c96-4424-b30d-1ee18100e2c2",
"name": "\ud83d\udcc4 Extract Raw File Content",
"type": "n8n-nodes-base.extractFromFile",
"position": [
-1392,
832
],
"parameters": {
"options": {},
"operation": "binaryToPropery",
"binaryPropertyName": "attachment_0"
},
"typeVersion": 1.1
},
{
"id": "434dbf05-9b9e-4584-a0e5-98f5bc344e5f",
"name": "\ud83d\udd0d Extract Text from PDF",
"type": "n8n-nodes-base.extractFromFile",
"position": [
-1168,
832
],
"parameters": {
"options": {},
"operation": "binaryToPropery",
"binaryPropertyName": "attachment_1"
},
"typeVersion": 1.1
},
{
"id": "2da5361f-df34-489b-9141-bdd62c1b821f",
"name": "\ud83e\uddf9 Clean & Normalise OCR Output",
"type": "n8n-nodes-base.code",
"position": [
-672,
832
],
"parameters": {
"jsCode": "// Extract the text from Gemini's response structure\nconst raw = $input.item.json.candidates[0].content.parts[0].text;\n\n// Parse the JSON string\nconst invoice = JSON.parse(raw);\n\nreturn [{ json: invoice }];"
},
"typeVersion": 2
},
{
"id": "39838335-cb59-426d-94de-15da23320845",
"name": "Sticky Note \u2014 Phase 1: Email Intake & Attachment Check",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2336,
640
],
"parameters": {
"color": 3,
"width": 500,
"height": 217,
"content": "## \ud83d\udce5 Phase 1: Email Intake & Attachment Check\nWatches Gmail for incoming invoice emails.\nPrepares email metadata, then checks whether a PDF attachment is present:\n- \u2705 **Has attachment** \u2192 download and proceed to OCR pipeline\n- \u274c **No attachment** \u2192 immediately alert the team via Slack and stop"
},
"typeVersion": 1
},
{
"id": "5bc90691-6209-418a-a9d3-127de33fbf6d",
"name": "Sticky Note \u2014 Phase 2: OCR Extraction Pipeline",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1456,
544
],
"parameters": {
"color": 5,
"width": 510,
"height": 232,
"content": "## \ud83d\udd0d Phase 2: OCR Extraction Pipeline\nProcesses the downloaded invoice PDF through a multi-stage OCR pipeline:\n1. **Extract raw file content** from the attachment\n2. **Extract text layer** from the PDF\n3. **Mistral OCR** parses the invoice as an image for scanned/non-text PDFs\n4. **Clean & normalise** the raw OCR output for consistent formatting before AI processing"
},
"typeVersion": 1
},
{
"id": "2a9277e2-4001-492f-aa95-338ef4958dbf",
"name": "Sticky Note \u2014 Phase 3: GPT-4o Structured Field Extraction",
"type": "n8n-nodes-base.stickyNote",
"position": [
-672,
544
],
"parameters": {
"color": 4,
"width": 530,
"height": 263,
"content": "## \ud83e\udd16 Phase 3: GPT-4o \u2014 Structured Field Extraction\n**GPT-4o** reads the cleaned OCR text and extracts structured invoice fields:\n- Vendor name, invoice number, date, due date\n- Line items, quantities, unit prices\n- Subtotal, tax, total amount, currency\n\nOutput is then **parsed and validated** by a code node to enforce data types, flag missing fields, and apply business rules (e.g. amount thresholds for auto-approval)."
},
"typeVersion": 1
},
{
"id": "8be3c0cd-ba09-46c4-8d85-e0358b1a22a0",
"name": "Sticky Note \u2014 Phase 4: Auto-Approval Routing",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
528
],
"parameters": {
"color": 6,
"width": 490,
"height": 207,
"content": "## \u2705 Phase 4: Auto-Approval or Human Review Routing\nRoutes each invoice based on validation outcome and business rules:\n- \u2705 **Auto-Approved** \u2192 log to Google Sheets + Slack notification to finance team\n- \ud83d\udea9 **Needs Review** \u2192 log as flagged in Google Sheets + Slack alert to reviewer with reason"
},
"typeVersion": 1
},
{
"id": "e24bcce1-fb5f-4bb3-9046-8af4a2f7038a",
"name": "Sticky Note \u2014 Phase 5: Logging & Notifications",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
592
],
"parameters": {
"color": 7,
"width": 460,
"height": 265,
"content": "## \ud83d\udcca Phase 5: Logging & Team Notifications\n**Auto-Approved path:**\n- Google Sheets logs the invoice with status **Approved**\n- Slack notifies the finance team with invoice summary\n\n**Flagged path:**\n- Google Sheets logs the invoice with status **Flagged for Review**\n- Slack alerts the reviewer with the specific flag reason for manual action"
},
"typeVersion": 1
},
{
"id": "fe4ece6f-8d2b-41e5-bb25-e3c7de32be48",
"name": "Sticky Note \u2014 Error Path: No Attachment",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1360,
1104
],
"parameters": {
"color": 2,
"width": 420,
"height": 151,
"content": "## \ud83d\udea8 Error Path: No Attachment Found\nIf the incoming email has no PDF attachment, the workflow exits early and sends a **Slack alert** with the sender and subject so the team can follow up directly."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "64f87a32-931f-41a7-aa45-b56796f55162",
"connections": {
"\u2753 Has PDF Attachment?": {
"main": [
[
{
"node": "\ud83d\udcce Gmail \u2014 Download Invoice Attachment",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\udea8 Slack \u2014 Alert: No Attachment Found",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd0d Extract Text from PDF": {
"main": [
[
{
"node": "\ud83c\udf10 Mistral OCR \u2014 Parse Invoice Image",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Prepare Email Metadata": {
"main": [
[
{
"node": "\u2753 Has PDF Attachment?",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcc4 Extract Raw File Content": {
"main": [
[
{
"node": "\ud83d\udd0d Extract Text from PDF",
"type": "main",
"index": 0
}
]
]
},
"\u2705 Parse & Validate Invoice Data": {
"main": [
[
{
"node": "\u2753 Auto-Approved or Needs Review?",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\uddf9 Clean & Normalise OCR Output": {
"main": [
[
{
"node": "\ud83e\udd16 GPT-4o \u2014 Extract Structured Invoice Fields",
"type": "main",
"index": 0
}
]
]
},
"\u2753 Auto-Approved or Needs Review?": {
"main": [
[
{
"node": "\ud83d\udcca Google Sheets \u2014 Log Approved Invoice",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\udea9 Slack \u2014 Flag Invoice for Human Review",
"type": "main",
"index": 0
},
{
"node": "\ud83d\udccb Google Sheets \u2014 Log Flagged Invoice",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce7 Gmail \u2014 Watch for Invoice Emails": {
"main": [
[
{
"node": "\ud83d\udd27 Prepare Email Metadata",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udf10 Mistral OCR \u2014 Parse Invoice Image": {
"main": [
[
{
"node": "\ud83e\uddf9 Clean & Normalise OCR Output",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcce Gmail \u2014 Download Invoice Attachment": {
"main": [
[
{
"node": "\ud83d\udcc4 Extract Raw File Content",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Google Sheets \u2014 Log Approved Invoice": {
"main": [
[
{
"node": "\ud83d\udd14 Slack \u2014 Notify: Invoice Auto-Approved",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udd16 GPT-4o \u2014 Extract Structured Invoice Fields": {
"main": [
[
{
"node": "\u2705 Parse & Validate Invoice Data",
"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.
gmailOAuth2googleSheetsOAuth2ApiopenAiApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automate your entire invoice processing pipeline with AI-powered OCR, validation, and approval workflows ππ€. This n8n automation monitors incoming Gmail invoices, extracts structured data using OCR and GPT-4o, validates invoice accuracy, and automatically routes clean invoicesβ¦
Source: https://n8n.io/workflows/15694/ β 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.
Overview
Small teams, solo operators, and security-conscious individuals who receive email attachments from external senders. Useful for freelancers, agencies, HR teams, and anyone handling CVs, invoices, or d
Complete AI-powered sales system Automates lead capture, qualification, and follow-up from multiple channels. AI INTELLIGENCE:
LeadInboxTriageBot_GT. Uses gmailTrigger, openAi, googleSheets, gmail. Event-driven trigger; 36 nodes.
This n8n workflow β HRMate β streamlines your entire recruitment process by automatically parsing incoming job applications, evaluating candidate fit using AI, and sending personalized acceptance or r