AutomationFlowsFinance › Invoice Reader (ep04)

Invoice Reader (ep04)

Invoice Reader (ep04). Event-driven trigger; 4 nodes.

Event trigger★★★★☆ complexity4 nodes
Finance Trigger: Event Nodes: 4 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "name": "Invoice Reader (ep04)",
  "nodes": [
    {
      "parameters": {},
      "id": "b4000000-0000-4000-8000-000000000001",
      "name": "Run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Load the invoice images and the purchase-order list. One item per invoice.\nconst fs = require('fs');\nconst path = require('path');\nconst base = $env.EP04_DIR;\nconst pos = fs.readFileSync(path.join(base, 'data', 'purchase-orders.csv'), 'utf8')\n  .trim().split('\\n').slice(1).map((l) => {\n    const [po_number, vendor, amount] = l.split(',');\n    return { po_number, vendor, amount: Number(amount) };\n  });\nconst dir = path.join(base, 'data', 'invoices');\nreturn fs.readdirSync(dir).filter((f) => f.endsWith('.png')).sort().map((f) => ({\n  json: {\n    file: f,\n    image: 'data:image/png;base64,' + fs.readFileSync(path.join(dir, f)).toString('base64'),\n    pos,\n  },\n}));"
      },
      "id": "b4000000-0000-4000-8000-000000000002",
      "name": "Load invoices + POs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// A vision model reads every invoice image into strict JSON.\n// If a field is not on the document, it must say null \u2014 never guess.\nconst https = require('https');\n\nconst read = (payload) => new Promise((resolve, reject) => {\n  const body = JSON.stringify(payload);\n  const req = https.request({\n    hostname: 'fal.run', path: '/fal-ai/any-llm/vision', method: 'POST',\n    headers: {\n      'Authorization': 'Key ' + $env.FAL_KEY,\n      'Content-Type': 'application/json',\n      'Content-Length': Buffer.byteLength(body),\n    },\n  }, (res) => {\n    let data = '';\n    res.on('data', (c) => (data += c));\n    res.on('end', () => resolve(JSON.parse(data)));\n  });\n  req.on('error', reject);\n  req.write(body);\n  req.end();\n});\n\nconst out = [];\nfor (const item of $input.all()) {\n  const t = item.json;\n  const res = await read({\n    model: 'google/gemini-flash-1.5',\n    system_prompt:\n      'You extract structured data from invoice images. Reply with ONLY JSON: ' +\n      '{\"vendor\": str, \"invoice_number\": str, \"invoice_date\": \"YYYY-MM-DD\", ' +\n      '\"total\": number, \"currency\": \"USD\"|..., \"po_number\": str|null}. ' +\n      'If a field is not on the document, use null. Never guess a PO number.',\n    prompt: 'Extract the fields from this invoice.',\n    image_url: t.image,\n  });\n  let fields;\n  try { fields = JSON.parse((res.output ?? '').replace(/```json|```/g, '').trim()); }\n  catch { fields = { vendor: null, invoice_number: null, total: null, po_number: null, unreadable: true }; }\n  out.push({ json: { file: t.file, pos: t.pos, ...fields } });\n}\nreturn out;"
      },
      "id": "b4000000-0000-4000-8000-000000000003",
      "name": "Vision: read every invoice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// The gate. An invoice only gets approved if it matches a real PO,\n// to the cent, and hasn't been paid before. Everything else \u2192 a human.\nconst fs = require('fs');\nconst path = require('path');\nconst dir = path.join($env.EP04_DIR, 'out');\nfs.mkdirSync(dir, { recursive: true });\n\nconst seen = new Set();\nconst approved = [];\nconst flagged = [];\nfor (const { json: r } of $input.all()) {\n  const id = r.vendor + '|' + r.invoice_number;\n  const po = (r.pos ?? []).find((p) => p.po_number === r.po_number);\n  const flag =\n    r.unreadable ? 'UNREADABLE \u2014 human review' :\n    seen.has(id) ? 'DUPLICATE \u2014 already processed ' + r.invoice_number :\n    !r.po_number ? 'NO PO \u2014 nobody ordered this' :\n    !po ? 'UNKNOWN PO \u2014 ' + r.po_number + ' is not in the system' :\n    Math.abs(r.total - po.amount) > 0.01\n      ? 'AMOUNT MISMATCH \u2014 invoice $' + r.total + ' vs PO $' + po.amount :\n    null;\n  seen.add(id);\n  const { pos, image, ...rec } = r;\n  (flag ? flagged : approved).push(flag ? { ...rec, flag } : rec);\n}\nfs.writeFileSync(path.join(dir, 'approved.json'), JSON.stringify(approved, null, 2));\nfs.writeFileSync(path.join(dir, 'flagged.json'), JSON.stringify(flagged, null, 2));\nconst summary = {\n  invoices: approved.length + flagged.length,\n  approved: approved.length,\n  flagged: flagged.length,\n  approved_value: '$' + approved.reduce((n, a) => n + a.total, 0).toFixed(2),\n};\nfs.writeFileSync(path.join(dir, 'summary.json'), JSON.stringify(summary, null, 2));\nreturn [{ json: summary }];"
      },
      "id": "b4000000-0000-4000-8000-000000000004",
      "name": "Reconcile + receipts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        300
      ]
    }
  ],
  "connections": {
    "Run": {
      "main": [
        [
          {
            "node": "Load invoices + POs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load invoices + POs": {
      "main": [
        [
          {
            "node": "Vision: read every invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vision: read every invoice": {
      "main": [
        [
          {
            "node": "Reconcile + receipts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Invoice Reader (ep04). Event-driven trigger; 4 nodes.

Source: https://github.com/Ships-Itself/builds/blob/main/ep04-invoice-agent/workflow.json — original creator credit. Request a take-down →

More Finance workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Finance

This is the ultimate sales-to-cash automation. When a deal in Airtable is marked "Approved for Invoicing," this workflow intelligently syncs customer data across QuickBooks and Stripe (creating them i

Airtable Trigger, Stripe, QuickBooks +2
Finance

This workflow triggers on successful Stripe payment events, generates and finalizes invoices, then formats and sends invoice details via Gmail, stores invoice PDFs in Google Drive, and notifies an adm

Stripe, Gmail, HTTP Request +3
Finance

How It Works Trigger: Watches for new emails in Gmail with PDF/image attachments. OCR: Sends the attachment to OCR.space API (https://ocr.space/OCRAPI) to extract invoice text. Parsing: Extracts key f

Gmail Trigger, Google Sheets, Slack +3
Finance

Automated Stripe Payment to QuickBooks Sales Receipt

HTTP Request, Stripe, Stripe Trigger +1
Finance

Invoice to Ledger. Uses telegramTrigger, telegram, googleSheets, httpRequest. Event-driven trigger; 21 nodes.

Telegram Trigger, Telegram, Google Sheets +1