{
  "name": "Batch Invoice Extractor \u2013 powered by easybits",
  "nodes": [
    {
      "id": "75d9c034-4872-4746-9106-377710238e80",
      "name": "Split: Files to Items",
      "type": "n8n-nodes-base.code",
      "position": [
        400,
        0
      ],
      "parameters": {
        "jsCode": "const out = [];\nfor (const item of $input.all()) {\n  const bin = item.binary || {};\n  for (const key of Object.keys(bin)) {\n    out.push({\n      json: { source_file: bin[key].fileName || key },\n      binary: { data: bin[key] },\n      pairedItem: { item: 0 }\n    });\n  }\n}\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "1b573b79-d0fc-4845-abf3-ec15d64c79c9",
      "name": "Loop: One File at a Time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        672,
        0
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5ecd2580-8fc8-413b-b451-163ca70ae595",
      "name": "easybits: Extract Invoice Fields",
      "type": "@easybits/n8n-nodes-extractor.easybitsExtractor",
      "onError": "continueRegularOutput",
      "position": [
        944,
        0
      ],
      "parameters": {},
      "credentials": {
        "easybitsExtractorApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1f53db59-9ea5-4ed6-b7d4-155dc84d35b2",
      "name": "Merge: Reattach Filename",
      "type": "n8n-nodes-base.merge",
      "position": [
        1216,
        0
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "391f3d05-7f0c-490f-ab01-6b1e36351e84",
      "name": "Flag: Check Core Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        1488,
        0
      ],
      "parameters": {
        "jsCode": "function isMissing(v) {\n  if (v === null || v === undefined) return true;\n  if (typeof v === 'string') {\n    const t = v.trim().toLowerCase();\n    return t === '' || t === 'null';\n  }\n  return false;\n}\n\nconst critical = ['invoice_number', 'invoice_date', 'vendor_name', 'total_amount'];\n\nconst item = $input.first();\nconst data = item.json.data;\nconst filename = item.json.source_file ?? `File ${$runIndex + 1}`;\n\nlet status, missing = [];\nif (!data || typeof data !== 'object' || Object.keys(data).length === 0) {\n  status = '\u274c'; // extraction failed outright\n} else {\n  missing = critical.filter(k => isMissing(data[k]));\n  status = missing.length === 0 ? '\u2705' : '\u274c';\n}\n\n// Accumulate across loop iterations so Build Summary can list every file.\n// $runIndex === 0 is the first iteration of this run, so we reset there.\nconst store = $getWorkflowStaticData('global');\nif ($runIndex === 0) store.invoiceBatch = [];\nstore.invoiceBatch.push({ source_file: filename, status });\n\n// Keep `data` nested so 'Append row in sheet' can read $json.data.<field>\nreturn [{\n  json: { data, source_file: filename, status, missing_fields: missing.join(', ') },\n  pairedItem: { item: 0 }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c41a8f50-8826-4f0f-852c-4e3d31fd94f1",
      "name": "Append: Invoice Row",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1760,
        0
      ],
      "parameters": {
        "columns": {
          "value": {
            "currency": "={{ $json.data.currency }}",
            "vendor_name": "={{ $json.data.vendor_name }}",
            "invoice_date": "={{ $json.data.invoice_date }}",
            "total_amount": "={{ $json.data.total_amount }}",
            "customer_name": "={{ $json.data.customer_name }}",
            "vendor_vat_id": "={{ $json.data.vendor_vat_id }}",
            "billing_period": "={{ $json.data.billing_period }}",
            "invoice_number": "={{ $json.data.invoice_number }}",
            "payment_status": "={{ $json.data.payment_status }}",
            "customer_vat_id": "={{ $json.data.customer_vat_id }}"
          },
          "schema": [
            {
              "id": "invoice_number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "invoice_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "invoice_date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "invoice_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_amount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_amount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "vendor_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "vendor_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "vendor_vat_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "vendor_vat_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "customer_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_vat_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "customer_vat_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "payment_status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "payment_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "billing_period",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "billing_period",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "51c705f4-d7bf-43b3-9298-45de9b840c69",
      "name": "Build: Upload Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        -448
      ],
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst rows = store.invoiceBatch || [];\nconst total = rows.length;\nconst failed = rows.filter(r => r.status === '\u274c').length;\n\nconst header = failed === 0\n  ? `All ${total} invoice(s) processed successfully.`\n  : `${total - failed} of ${total} processed cleanly. ${failed} need a second look.`;\n\nconst list = rows.map(r => `${r.status} ${r.source_file}`).join('<br>');\n\nreturn { json: { summary: `${header}<br><br>${list}` } };"
      },
      "typeVersion": 2
    },
    {
      "id": "fcdeff6c-2025-4bea-8a4a-61be85617a61",
      "name": "Respond: Completion Summary",
      "type": "n8n-nodes-base.form",
      "position": [
        1216,
        -448
      ],
      "parameters": {
        "options": {},
        "operation": "completion",
        "completionTitle": "Upload Processed Successfully",
        "completionMessage": "={{ $json.summary }}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "316e00d2-9be0-4230-ae95-c8b2222137c0",
      "name": "Receive: Invoice Uploads",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        128,
        0
      ],
      "parameters": {
        "options": {},
        "formTitle": "Invoice Upload & Data Extraction",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Document Upload",
              "requiredField": true,
              "acceptFileTypes": "jpg, png, pdf"
            }
          ]
        },
        "formDescription": "Upload your PNG, JPG, or PDF files and we\u2019ll automatically extract and structure the information for you.\n\nSimply choose a file, submit it, and our system will process the content to generate usable data, saving you time on manual entry."
      },
      "typeVersion": 2.5
    },
    {
      "id": "b8c763a1-1d12-46f9-bf8b-c02bd1d7b4ec",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -668,
        -752
      ],
      "parameters": {
        "color": 5,
        "width": 700,
        "height": 1320,
        "content": "# \ud83d\udcc4 Batch Invoice Extractor\n### powered by the easybits Extractor\n\nUpload one or more invoices (JPG, PNG, or PDF) through a web form and get the key fields (invoice number, date, vendor, customer, total, currency, payment status, and more) extracted into a Google Sheet. When the batch finishes, the form shows a summary marking each file \u2705 or \u274c, so you instantly see which invoices need a second look.\n\n**Who it's for:** finance and ops teams who receive invoices in bulk and want structured data without manual entry.\n\n## \u2699\ufe0f How it works\n1. Upload one or many invoices through the form.\n2. Each file is split into its own item.\n3. The loop sends one file at a time to the Extractor (it returns one result per request).\n4. The filename is reattached and the core fields are checked.\n5. A row is appended to the Sheet for every invoice.\n6. A summary lists each file with \u2705 or \u274c on the completion screen.\n\n## \ud83d\udee0\ufe0f Set up steps\nTakes about 10 minutes.\n\n**1. Install the Extractor node**\nn8n Cloud: search \"easybits\" in the nodes panel (verified node) and add it.\nSelf-hosted: Settings \u2192 Community Nodes \u2192 Install \u2192 `@easybits/n8n-nodes-extractor`.\n\n**2. Create your pipeline**\nSign up at extractor.easybits.tech, create a pipeline, and add your invoice fields. Fastest way: upload one example invoice and use auto-mapping to detect and map every field, then tweak the field descriptions. Free tier covers 50 extractions per month. Copy the API key.\n\n**3. Connect it in n8n**\nOn the Extract node, add your easybits credential with the API key, and read the binary property named `data`.\n\n**4. Point the Sheet node at your own sheet**\nSelect your spreadsheet and tab, and set value input to RAW so amounts and dates store exactly as extracted.\n\nDocs: extractor.easybits.tech/documentation/integration"
      },
      "typeVersion": 1
    },
    {
      "id": "b1a11111-0000-4000-8000-000000000001",
      "name": "Section: Upload & Split",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 480,
        "content": "## 1. Upload & Split\nThe user uploads one or more invoices through a web form. Each file becomes its own item, renamed to `data` so the Extractor can read it, with the original filename kept for the summary."
      },
      "typeVersion": 1
    },
    {
      "id": "b1a11111-0000-4000-8000-000000000002",
      "name": "Section: Process Each Invoice",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        620,
        -240
      ],
      "parameters": {
        "color": 4,
        "width": 1290,
        "height": 480,
        "content": "## 2. Process Each Invoice (one at a time)\nThe loop feeds the Extractor a single file per pass, because it bundles everything it receives into one request and returns one result. For each file: extract the fields, reattach the filename, flag whether the core fields (number, date, vendor, total) are present, then append a row to the Sheet in RAW mode."
      },
      "typeVersion": 1
    },
    {
      "id": "b1a11111-0000-4000-8000-000000000003",
      "name": "Section: Summarize & Respond",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        900,
        -668
      ],
      "parameters": {
        "color": 6,
        "width": 470,
        "height": 360,
        "content": "## 3. Summarize & Respond\nOnce every file has looped through, the results become a summary that lists each file with a \u2705 or \u274c, shown to the user on the form's completion screen."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "connections": {
    "Append: Invoice Row": {
      "main": [
        [
          {
            "node": "Loop: One File at a Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build: Upload Summary": {
      "main": [
        [
          {
            "node": "Respond: Completion Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split: Files to Items": {
      "main": [
        [
          {
            "node": "Loop: One File at a Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag: Check Core Fields": {
      "main": [
        [
          {
            "node": "Append: Invoice Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop: One File at a Time": {
      "main": [
        [
          {
            "node": "Build: Upload Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "easybits: Extract Invoice Fields",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge: Reattach Filename",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge: Reattach Filename": {
      "main": [
        [
          {
            "node": "Flag: Check Core Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive: Invoice Uploads": {
      "main": [
        [
          {
            "node": "Split: Files to Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "easybits: Extract Invoice Fields": {
      "main": [
        [
          {
            "node": "Merge: Reattach Filename",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}