In this categoryAutomation · 39
AutomationIntermediate

Extract Invoice Data with AI in n8n

A full invoice pipeline in n8n: read the PDF, pull fields and line items with an LLM against a strict schema, check the totals, and append clean rows to Google Sheets. Handles scanned invoices too.

15 minIntermediate

Invoices are the worst kind of manual data entry: same fields every time, different layout every vendor. This guide builds one n8n workflow that turns any incoming invoice PDF into a checked row of data. It reads the file, extracts the header fields and the line items with a language model against a strict schema, verifies that the line items add up to the total, and writes the result to Google Sheets or a database. If you only need plain text out of a PDF, start with the shorter build in the structured-data guide and come back here when you need invoice-specific fields and validation.

  • An n8n instance, cloud or self-hosted, version with the Advanced AI nodes
  • A trigger source for invoices: a Google Drive folder, a Gmail inbox, or an HTTP URL
  • An OpenAI or Anthropic API key for the chat model
  • A destination such as a Google Sheet or a Postgres table

The shape of the workflow

Five nodes carry an invoice from file to verified row. A trigger drops the PDF in, Extract From File turns it into text, the Information Extractor pulls the fields against a schema, a small Code or IF node checks the math, and a Google Sheets node appends the row. Scanned invoices need one extra branch, covered in step 4.

n8n - invoice workflow canvas
Gmail (attachment) -> Extract From File -> Information Extractor -> Code (check total) -> Google Sheets
binary: data op: Extract From PDF schema -> JSON totals match? Append row
^ OpenAI Chat Model
Trigger to verified row, with an OCR branch for scanned files.

Step 1: Get the invoice in as binary

The extractor needs the file bytes, not a link. Begin with a trigger, then a node that returns the PDF as binary: Gmail with Get Attachment for emailed invoices, Google Drive with Download File for a watched folder, or an HTTP Request node for a URL. Run it once so the invoice lands on a binary property, which n8n names data unless you change it.

Step 2: Pull the text with Extract From File

Add the Extract From File node and set Operation to Extract From PDF. Point Input Binary Field at your binary property (data by default). The node outputs the document text on a field named text. For a normal digital invoice this is all the reading you need. If the text field comes back empty, the PDF is a scan, and you handle that in step 4.

Step 3: Extract the invoice fields with a schema

Add the Information Extractor node from the Advanced AI section and connect an OpenAI Chat Model or Anthropic Chat Model underneath it as a sub-node with your API credential. In the Text field, map the extracted text with an expression such as {{ $json.text }}. For invoices, do not use From Attribute Descriptions. Choose Define using JSON Schema so you can mark optional fields and capture the line items as an array.

Information Extractor - Define using JSON Schema
{
  "type": "object",
  "properties": {
    "vendor": { "type": "string" },
    "invoice_number": { "type": "string" },
    "issue_date": { "type": "string", "description": "ISO date, YYYY-MM-DD" },
    "due_date": { "type": ["string", "null"] },
    "currency": { "type": "string", "description": "ISO 4217 code such as USD" },
    "subtotal": { "type": ["number", "null"] },
    "tax": { "type": ["number", "null"] },
    "total": { "type": "number" },
    "line_items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": { "type": "string" },
          "quantity": { "type": ["number", "null"] },
          "amount": { "type": "number" }
        },
        "required": ["description", "amount"]
      }
    }
  },
  "required": ["vendor", "invoice_number", "total", "currency", "line_items"]
}
Tell the model to return null, not a guess
A missing due date or tax line is normal, and a model asked for a number will often invent one. Add a line to the System Prompt: return null for any field that is not clearly present, and never estimate a value. Marking those fields as nullable in the schema above is what makes null a legal answer. Note that n8n does not support $ref references inside JSON Schema, so keep the schema flat and inline.

Step 4: Scanned invoices need OCR first

Extract From File reads the text layer of a PDF. A scanned or photographed invoice has no text layer, so the node returns an empty text field and the model gets nothing to work with. Add an IF node after Extract From File that checks whether text is empty. On the empty branch, send the file to a vision model instead, which reads the pixels the same way a person would. The vision approach is a separate build, covered end to end in the invoice-with-vision guide, and its output plugs into the same total-check and Sheets nodes below.

Step 5: Check the totals before you trust the row

The single most useful guard on an invoice pipeline is arithmetic the model cannot fake. Add a Code node that sums the line item amounts and compares that sum against the extracted subtotal or total, within a cent of rounding. When it does not match, flag the row for a human instead of writing it silently. This catches the failures that matter, a dropped line or a misread digit, without you reading every invoice.

Code node - verify the line items add up
const inv = $json.output;
const sum = (inv.line_items || []).reduce((t, li) => t + (li.amount || 0), 0);
const target = inv.subtotal ?? inv.total;
const ok = Math.abs(sum - target) < 0.01;

return [{ json: { ...inv, computed_line_total: Number(sum.toFixed(2)), totals_match: ok } }];

Step 6: Write the row, and skip duplicates

Add a Google Sheets node with Append or Update Row, or a Postgres Insert, and map each field: {{ $json.vendor }}, {{ $json.invoice_number }}, {{ $json.total }}, and the totals_match flag from the Code node. Use the invoice number as the matching column so the same invoice arriving twice updates one row instead of creating a second. Route rows where totals_match is false to a separate sheet or a Slack message so a person sees only the ones that need eyes.

SymptomLikely causeFix
text field is emptyScanned invoice, no text layerBranch to a vision model, see step 4
Wrong or invented tax amountField absent, model guessedMark nullable, tell the prompt to return null
totals_match is falseA line item was missed or misreadSend to human review, do not auto-write
Duplicate rowsAppend used instead of upsertMatch on invoice_number with Append or Update
n8n - execution log
Extract From File: text length 2.9 KB
Information Extractor: 5 fields + 3 line items
computed_line_total: 1240.00 total: 1240.00 totals_match: true
Google Sheets: upserted invoice INV-2041 (1 row)
$

Result: emailed and uploaded invoices become checked rows without you opening them, and the only ones that reach you are the ones where the math did not line up. Test with ten real invoices from different vendors before you activate the workflow, since layout variety is where extraction quietly fails.

Related guides

Watch related tutorials

Free weekly email

New guides in your inbox

Fresh step-by-step how-to guides as we publish them. One email a week, no more.

Tags
#extract invoice data from pdf#ai invoice extraction#n8n invoice#information extractor#invoice ocr