{
  "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
          }
        ]
      ]
    }
  }
}