This workflow corresponds to n8n.io template #16774 — we link there as the canonical source.
This workflow follows the Form → Form 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 →
{
"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
}
]
]
}
}
}
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.
easybitsExtractorApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow collects one or more invoice files via an n8n Form, extracts structured invoice fields with the easybits Extractor, appends the results to Google Sheets, and returns an on-screen completion summary that flags any invoices missing critical fields. Receives invoice…
Source: https://n8n.io/workflows/16774/ — 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.
Data Table Extraction Test w/ Reference Branch powered by easybits. Uses formTrigger, @easybits/n8n-nodes-extractor, googleSheets, form. Event-driven trigger; 41 nodes.
Upload your CV as a PDF and instantly get a structured "Master CV" Google Sheet – every experience, education entry, skill, and contact detail extracted into the right tab. This gives you a clean pers
Purchase Order Extractor powered by easybits. Uses formTrigger, @easybits/n8n-nodes-extractor, googleSheets, form. Event-driven trigger; 17 nodes.
Splitout Code. Uses splitOut, httpRequest, googleSheets, stickyNote. Event-driven trigger; 36 nodes.
This n8n workflow is designed for Customer Success Managers (CSM), marketers, sales teams, and data administrators who need to automate the process of uploading and processing CSV data in HubSpot. It