{
  "name": "AI Invoice Parser — Gmail PDF to Google Sheets",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "has:attachment filename:pdf (invoice OR rechnung OR facture)",
          "readStatus": "unread"
        },
        "options": {
          "downloadAttachments": true
        }
      },
      "id": "b1a2c3d4-1111-4b2c-8d3e-0f1a2b3c4d5e",
      "name": "Gmail Trigger: Invoice Email",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [-220, 300],
      "credentials": {
        "gmailOAuth2": {
          "id": "YOUR_GMAIL_CREDENTIAL",
          "name": "YOUR_GMAIL_CREDENTIAL"
        }
      }
    },
    {
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "attachment_0",
        "options": {}
      },
      "id": "c2b3d4e5-2222-4c3d-9e4f-1a2b3c4d5e6f",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [20, 300]
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list",
          "cachedResultName": "gpt-4o-mini"
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are an accounts-payable data extraction engine. Extract invoice data from raw PDF text.\n\nRules:\n- Dates in ISO format YYYY-MM-DD.\n- total is a number with no currency symbols or thousands separators.\n- currency is the 3-letter ISO code (EUR, USD, GBP...).\n- If a field cannot be found, use null (or [] for line_items).\n- Set \"anomaly\" to true and explain in \"anomaly_reason\" if: total is missing or zero, total does not match the sum of line items (tolerance 0.05), invoice date is in the future, or the document does not look like an invoice. Otherwise anomaly=false and anomaly_reason=null.\n\nRespond ONLY with valid JSON, no markdown fences, exactly this shape:\n{\"vendor\": string|null, \"invoice_number\": string|null, \"invoice_date\": string|null, \"total\": number|null, \"currency\": string|null, \"line_items\": [{\"description\": string, \"quantity\": number, \"unit_price\": number, \"amount\": number}], \"anomaly\": boolean, \"anomaly_reason\": string|null}"
            },
            {
              "role": "user",
              "content": "=Extract the invoice data from this document text:\n\n{{ $json.text }}"
            }
          ]
        },
        "jsonOutput": true,
        "options": {
          "temperature": 0
        }
      },
      "id": "d3c4e5f6-3333-4d4e-8f5a-2b3c4d5e6f7a",
      "name": "OpenAI: Extract Invoice Fields",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [260, 300],
      "credentials": {
        "openAiApi": {
          "id": "YOUR_OPENAI_CREDENTIAL",
          "name": "YOUR_OPENAI_CREDENTIAL"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Flatten the LLM result and attach email metadata for traceability\nconst inv = $input.first().json.message?.content ?? $input.first().json;\nconst mail = $('Gmail Trigger: Invoice Email').first().json;\n\nreturn [{\n  json: {\n    vendor: inv.vendor,\n    invoice_number: inv.invoice_number,\n    invoice_date: inv.invoice_date,\n    total: inv.total,\n    currency: inv.currency,\n    line_items: JSON.stringify(inv.line_items ?? []),\n    anomaly: Boolean(inv.anomaly),\n    anomaly_reason: inv.anomaly_reason,\n    email_from: mail.from?.value?.[0]?.address ?? mail.From,\n    email_subject: mail.subject ?? mail.Subject,\n    processed_at: new Date().toISOString(),\n  },\n}];"
      },
      "id": "e4d5f6a7-4444-4e5f-9a6b-3c4d5e6f7a8b",
      "name": "Code: Normalize Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [500, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f5e6a7b8-5555-4f6a-8b7c-4d5e6f7a8b9c",
              "leftValue": "={{ $json.anomaly }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f5e6a7b8-5555-4f6a-8b7c-4d5e6f7a8b9d",
      "name": "IF: Anomaly Detected",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [740, 300]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#finance-review",
          "mode": "name"
        },
        "text": "=:warning: *Invoice needs manual review*\nVendor: {{ $json.vendor }} | Invoice #: {{ $json.invoice_number }} | Total: {{ $json.total }} {{ $json.currency }}\nProblem: {{ $json.anomaly_reason }}\nEmail: \"{{ $json.email_subject }}\" from {{ $json.email_from }}",
        "otherOptions": {}
      },
      "id": "a6f7b8c9-6666-4a7b-9c8d-5e6f7a8b9c0d",
      "name": "Slack: Flag Anomaly",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [1000, 180],
      "credentials": {
        "slackApi": {
          "id": "YOUR_SLACK_CREDENTIAL",
          "name": "YOUR_SLACK_CREDENTIAL"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Invoices",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Processed At": "={{ $json.processed_at }}",
            "Vendor": "={{ $json.vendor }}",
            "Invoice Number": "={{ $json.invoice_number }}",
            "Invoice Date": "={{ $json.invoice_date }}",
            "Total": "={{ $json.total }}",
            "Currency": "={{ $json.currency }}",
            "Line Items": "={{ $json.line_items }}",
            "Source Email": "={{ $json.email_from }}"
          }
        },
        "options": {}
      },
      "id": "b7a8c9d0-7777-4b8c-8d9e-6f7a8b9c0d1e",
      "name": "Google Sheets: Append Invoice",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1000, 420],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "YOUR_GOOGLE_SHEETS_CREDENTIAL",
          "name": "YOUR_GOOGLE_SHEETS_CREDENTIAL"
        }
      }
    }
  ],
  "connections": {
    "Gmail Trigger: Invoice Email": {
      "main": [
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "OpenAI: Extract Invoice Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: Extract Invoice Fields": {
      "main": [
        [
          {
            "node": "Code: Normalize Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Normalize Result": {
      "main": [
        [
          {
            "node": "IF: Anomaly Detected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Anomaly Detected": {
      "main": [
        [
          {
            "node": "Slack: Flag Anomaly",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets: Append Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
