Skip to content
OneLessTool

Replacement workflow · reviewed Oct 13, 2026

Extract invoice data with AI into Google Sheets, with validation and human approval

For low invoice volumes: use Gemini in Google Workspace or ChatGPT file upload with a strict extraction prompt (no-code), or an n8n flow from Gmail to an LLM to Sheets (automated). Either way, totals and duplicates are checked by rules, and a human approves every invoice before payment.

moderateSetup Path A: 1–2 hours. Path…Per run Path A: ~2–3 minutes per…+$5/monthEditorialIllustrative example
Uses
Google WorkspaceGmail (incoming invoices), Drive (PDF archive), Sheets (invoice log, validation formulas, approval column).
Google Gemini (optional)Path A: Gemini in Sheets/Drive side panel on eligible Workspace plans, or the Gemini app, to read invoice PDFs.
ChatGPT (optional)Path A alternative: upload PDFs and run the extraction prompt.
Claude (optional)Path A alternative: upload PDFs and run the extraction prompt.
n8n (optional)Path B: Gmail trigger → PDF text → Information Extractor (LLM, JSON schema) → validation → Sheets.
Time
Setup: Path A: 1–2 hours. Path B: 1 day including the parallel-run test.
Per run: Path A: ~2–3 minutes per batch of 5–10 PDFs plus review. Path B: automatic; ~1 minute human review per invoice.
Extra cost
Path A (no-code, using an AI assistant or Gemini plan you already pay for) adds $0. Path B adds LLM API usage on top of an n8n instance you already run. $5/month is a conservative ILLUSTRATIVE allowance for ~100 invoices — we did not verify current per-token API prices today; check your provider's pricing page and the token count of a typical invoice.

Is it for you?

Today

Invoices arrive by email. Someone opens each PDF and types vendor, number, dates and amounts into a spreadsheet or accounting system — or pays for an AP-capture tool or a parser add-on chained into an automation platform to do it.

With this workflow

Path A: once or twice a week, drag 5–10 invoice PDFs into ChatGPT, Claude or Gemini with a saved extraction prompt; paste the JSON/TSV into the Invoice log where formulas flag line-sum, total and duplicate problems. Path B: an n8n workflow watches a Gmail label, extracts PDF text, asks an LLM for schema-valid JSON, validates totals and duplicates in a Code node, appends to the same sheet as 'Awaiting approval', and emails the approver. In both paths nothing gets paid until a person has compared the row with the PDF and marked it Approved.

Good fit if…

  • Small businesses receiving tens to roughly a hundred supplier invoices a month
  • Bookkeepers who key invoices into a spreadsheet or accounting tool by hand today
  • Teams already on Google Workspace with Gmail as the invoice inbox
  • Anyone using a general-purpose automation tool plus a parser add-on just for invoices

Not for you if…

  • Hundreds or thousands of invoices a month, multi-entity AP, or three-way PO matching — dedicated AP automation is built for this
  • Businesses that need audit-ready approval workflows, segregation of duties or e-invoicing compliance (e.g. mandated structured e-invoice formats) — a sheet doesn't provide these
  • Organisations whose data policies don't allow financial documents to be sent to an external LLM provider
  • Anyone hoping to remove human review — extraction errors must be caught before payment

Step by step

  1. 1

    Set up the Invoice log and a Gmail label

    1. Create a Google Sheet Invoice log with the columns listed in the Validation rules artifact (A–P). Add the O2 (normalised key) and P2 (flags) formulas and fill down.
    2. Add data validation on K (Status): Awaiting approval, Approved, Rejected, Blocked – duplicate, Paid.
    3. Conditional formatting: =$P2<>"" → red fill.
    4. In Gmail, create a filter that applies the label AP/Inbox to mail from your suppliers or sent to your invoices@ address. Save PDFs to a Drive folder AP/2026 so the log can link to originals (column N).
  2. 2

    Path A (no-code): extract with your AI assistant

    Use whichever you already pay for:

    • ChatGPT or Claude: start a Project (so the instructions persist), paste the Invoice JSON schema and the Extract invoice to JSON prompt as project instructions, then upload 5–10 PDFs per message.
    • Gemini in Google Sheets (on eligible Workspace editions — Business Starter does not include Gemini in Sheets per Google's pricing page): open the side panel in the Invoice log, reference the Drive PDFs and ask for a table using the same field list. Check that it read the right files.

    Ask for the result as JSON and as TSV; paste the TSV into the log. The formulas then flag any line-sum, total or duplicate issues.

    Scanned or photographed invoices: the assistant may misread digits. Mark them HUMAN_CHECK and compare every number.

  3. 3

    Path B (automated): build the n8n workflow

    Nodes, in order:

    1. Gmail Trigger — poll every 15–60 min, filter by label AP/Inbox, unread only; enable downloading attachments.
    2. Filter — keep only items with a PDF attachment.
    3. Extract From File → Extract From PDF on the attachment's binary field. (Text-based PDFs only; n8n's docs don't mention OCR, so route scanned PDFs to a model that accepts images/PDFs directly, or to manual entry.)
    4. Information Extractor with Schema Type → Define using JSON Schema, paste the schema artifact, connect your chat model (OpenAI, Anthropic, Gemini). Put the extraction rules from the prompt in the system message.
    5. Google Sheets → Get rows from the Invoice log, then a Code node ("Run Once for All Items") that builds an existingKeys array — or simply rely on the sheet's DUPLICATE formula and keep this step simple.
    6. Code node Validate invoice (artifact) — adds flags, line_sum, norm_key, status.
    7. Google Drive → Upload the PDF to AP/2026; Google Sheets → Append row with the link and status Awaiting approval or Blocked – duplicate.
    8. Gmail → Send the approver a summary with the flags and the sheet link.
    9. Attach an Error Workflow so failures alert you instead of silently dropping invoices.
  4. 4

    Validate every invoice with rules, not vibes

    LLMs are good at reading invoices and bad at noticing their own mistakes. The Validation rules artifact catches the errors that cost money:

    • Line items sum to subtotal, and subtotal − discount + shipping + tax = total (±0.01).
    • Duplicate invoice number from the same vendor after normalising (INV-00123 = inv 123) → blocked.
    • Near-duplicates (same vendor and amount within 7 days, different number) → flagged; double-billing often looks like this.
    • Dates in the future or more than a year old → flagged.
    • Bank details present on a new vendor or changed from last time → verify by phone using a number you already have. Invoice fraud usually arrives as a legitimate-looking invoice with new bank details.

    Anything flagged gets a human check against the PDF; a clean row still needs approval.

  5. 5

    Human approval before any payment

    Set a simple rule and write it at the top of the sheet: nobody pays from an unapproved row.

    • The approver opens the PDF link (column N), compares vendor, number, total and due date, resolves flags with a note, then sets Status = Approved with their name and date.
    • Payment is made in your bank or accounting software by someone other than the approver if you have two people (basic segregation of duties).
    • After payment, set Status = Paid. Once a month, reconcile Paid rows against bank statements.

    Use Data → Protect sheets and ranges so only approvers can edit columns K–M.

  6. 6

    Parallel-run for one month, then decide

    For one month, keep your current process and run the new one alongside it. Track per invoice: fields wrong, flags raised, minutes spent. Move to the new process only if zero errors reached the 'Approved' state and the time saved is real. Re-check the extraction prompt whenever a new supplier's layout causes repeated HUMAN_CHECK flags — add a one-line hint per problem supplier to the prompt.

Prompts you can use now

Copy as-is, or fill in the blanks first. Highlighted parts are placeholders.

Extract invoice to JSON

Turn one or more invoice PDFs into schema-valid JSON without inventing values.

Fill in 3 blanks (optional)
You are an accounts-payable data-entry assistant. Extract data from the attached document(s) for {{company_name}}.

Return ONLY JSON that validates against this schema (one object per document; if several documents are attached, return a JSON array in the order given):
{{json_schema}}

Rules:
1. Copy values exactly as printed. Do not calculate, infer or 'fix' anything. If a value is not printed, use null (or 0 for tax_total only when the document clearly shows no tax).
2. Dates → YYYY-MM-DD. If the date format is ambiguous (e.g. 03/04/2026), assume {{date_convention}} and add the field to uncertain_fields.
3. Numbers → plain decimals without currency symbols or thousands separators.
4. invoice_number: keep prefixes, dashes and leading zeros exactly.
5. If it is not an invoice (statement, quote, receipt, marketing email), set document_type accordingly and still fill what you can.
6. Set bank_details_present to true if bank account / IBAN / routing details appear anywhere.
7. confidence = low if the scan is blurry, handwritten, cut off or totals don't appear.
No commentary outside the JSON.

Convert extracted JSON to sheet rows

Produce paste-ready TSV in the Invoice log's column order (Path A).

Fill in 3 blanks (optional)
Convert this extracted invoice JSON into TSV rows for my Invoice log. Columns, in order: Received ({{received_date}}), Vendor, Invoice no, Invoice date, Due date, Currency, Subtotal, Tax, Total, Line sum (sum of line_items.amount, rounded to 2 decimals), Status (always "Awaiting approval"), Approver (blank), Approved on (blank), File link ({{file_link_placeholder}}).

{{extracted_json}}

Output only one TSV code block with no header row. Do not change any extracted value; if a field is null, leave the cell empty. After the code block, list any invoice where Line sum ≠ Subtotal or Subtotal + Tax ≠ Total.

Second-look check on a flagged invoice

Re-read a flagged invoice field by field before a human decides.

Fill in 2 blanks (optional)
This invoice was flagged {{flags}} after extraction. Re-read the attached PDF carefully and compare it against the extracted JSON below, field by field.

{{extracted_json}}

Return a table: field | extracted value | value printed on the document (quote it exactly) | match (yes/no). Then say which flag is explained by an extraction error and which reflects a real problem on the invoice itself (e.g. the vendor's own arithmetic is wrong, or it's a duplicate). Do not recommend paying or not paying; just report what the document says.

Templates, scripts & configs

Invoice JSON schema

schema · json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ExtractedInvoice",
  "type": "object",
  "additionalProperties": false,
  "required": ["document_type", "vendor_name", "invoice_number", "invoice_date", "currency",
               "line_items", "subtotal", "tax_total", "total", "confidence", "uncertain_fields"],
  "properties": {
    "document_type":   { "enum": ["invoice", "credit_note", "receipt", "statement", "quote", "not_an_invoice"] },
    "vendor_name":     { "type": "string", "description": "Legal name as printed on the document" },
    "vendor_tax_id":   { "type": ["string", "null"], "description": "VAT/GST/EIN etc. exactly as printed, else null" },
    "vendor_address":  { "type": ["string", "null"] },
    "bill_to_name":    { "type": ["string", "null"] },
    "invoice_number":  { "type": "string", "description": "Exactly as printed, including prefixes and leading zeros" },
    "po_number":       { "type": ["string", "null"] },
    "invoice_date":    { "type": "string", "format": "date", "description": "YYYY-MM-DD" },
    "due_date":        { "type": ["string", "null"], "format": "date" },
    "payment_terms":   { "type": ["string", "null"], "description": "e.g. 'Net 30' as printed" },
    "currency":        { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO 4217, e.g. USD" },
    "line_items": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["description", "quantity", "unit_price", "amount"],
        "properties": {
          "description": { "type": "string" },
          "quantity":    { "type": ["number", "null"] },
          "unit_price":  { "type": ["number", "null"] },
          "amount":      { "type": "number", "description": "Line total before tax, as printed" },
          "tax_rate":    { "type": ["number", "null"], "description": "Percent, e.g. 20 for 20%" }
        }
      }
    },
    "subtotal":        { "type": "number", "description": "Before tax, after line-level discounts" },
    "discount_total":  { "type": ["number", "null"], "description": "Document-level discount as a positive number" },
    "shipping_total":  { "type": ["number", "null"] },
    "tax_total":       { "type": "number", "description": "0 if no tax shown" },
    "total":           { "type": "number", "description": "Amount due as printed" },
    "amount_paid":     { "type": ["number", "null"] },
    "bank_details_present": { "type": "boolean", "description": "true if the document shows bank/payment account details" },
    "confidence":      { "enum": ["high", "medium", "low"] },
    "uncertain_fields": { "type": "array", "items": { "type": "string" },
                          "description": "Names of fields that were hard to read, inferred or missing" }
  }
}

Conventions: numbers are plain decimals (1234.5, not "1,234.50"); credit notes use positive numbers with
document_type "credit_note"; never compute a value that isn't printed — put the field in uncertain_fields instead.

Validation rules + Google Sheets formulas

checklist

VALIDATION RULES — apply to every extracted invoice before it can be approved

Tolerance: 0.01 per amount (0.02 if more than 10 line items, to allow per-line rounding).

R1  Line sum        | SUM(line_items.amount) = subtotal                                   → else "LINE_SUM_MISMATCH"
R2  Total           | subtotal − discount_total + shipping_total + tax_total = total       → else "TOTAL_MISMATCH"
R3  Tax sanity      | if line tax_rate present: tax_total ≈ Σ(amount × rate/100)          → else "TAX_CHECK" (warning)
R4  Duplicate       | same normalised vendor + same normalised invoice_number already in the log
                    | (normalise: upper-case, keep only letters/digits, drop zeros that lead a number)  → "DUPLICATE" (block)
R5  Near-duplicate  | same vendor + same total + invoice_date within 7 days, different number → "POSSIBLE_DUPLICATE"
R6  Dates           | invoice_date not in the future and not > 365 days old; due_date ≥ invoice_date → "DATE_CHECK"
R7  Required        | vendor_name, invoice_number, invoice_date, total present             → else "MISSING_FIELD"
R8  Doc type        | document_type = "invoice" (credit notes routed separately)           → else "NOT_INVOICE"
R9  Confidence      | confidence = "low" or uncertain_fields not empty                     → "HUMAN_CHECK"
R10 Bank details    | bank_details_present = true AND vendor is new, or bank details differ from the vendor master
                    | → "VERIFY_BANK_DETAILS_BY_PHONE" (classic invoice-fraud pattern: call a known number, never the one on the invoice)

Google Sheets versions (Invoice log tab; columns: A Received, B Vendor, C Invoice no, D Invoice date,
E Due date, F Currency, G Subtotal, H Tax, I Total, J Line sum, K Status, L Approver, M Approved on, N File link,
O Norm key, P Flags):
  O2  =UPPER(REGEXREPLACE(B2,"[^A-Za-z0-9]",""))&"|"&REGEXREPLACE(UPPER(REGEXREPLACE(C2,"[^A-Za-z0-9]","")),"(^|[A-Z])0+([0-9])","$1$2")
  P2  =TEXTJOIN(", ",TRUE,
        IF(ABS(J2-G2)>0.01,"LINE_SUM_MISMATCH",""),
        IF(ABS(G2+H2-I2)>0.01,"TOTAL_MISMATCH",""),
        IF(COUNTIF($O$2:$O,O2)>1,"DUPLICATE",""),
        IF(D2>TODAY(),"DATE_CHECK",""))
  (Adjust P2's TOTAL check if you track discount/shipping columns.)
  Conditional formatting on A2:P: custom formula =$P2<>"" → red fill.

APPROVAL RULE: Status may only change to "Approved" when P is empty (or each flag is resolved with a note),
and the approver has opened the original PDF. Payment happens in your bank/accounting system, never from this sheet.

n8n Code node: validate invoice

script · javascript

// n8n Code node — "Validate invoice" (mode: Run Once for Each Item)
// Input: the Information Extractor's structured result (n8n puts it under `output`; we also accept a top-level object)
// plus `existingKeys` (array of normalised "VENDOR|INVOICENO" keys) merged in from a Google Sheets lookup step.
const inv = $json.output ?? $json;
const existingKeys = $json.existingKeys ?? [];
const flags = [];
const tol = (inv.line_items?.length ?? 0) > 10 ? 0.02 : 0.01;
const num = (v) => (typeof v === 'number' && isFinite(v) ? v : 0);
const close = (a, b) => Math.abs(a - b) <= tol + 1e-9;

for (const f of ['vendor_name', 'invoice_number', 'invoice_date', 'total']) {
  if (inv[f] === null || inv[f] === undefined || inv[f] === '') flags.push(`MISSING_FIELD:${f}`);
}
if (inv.document_type !== 'invoice') flags.push(`NOT_INVOICE:${inv.document_type}`);

const lineSum = (inv.line_items ?? []).reduce((s, l) => s + num(l.amount), 0);
if ((inv.line_items ?? []).length && !close(lineSum, num(inv.subtotal))) flags.push('LINE_SUM_MISMATCH');

const expectedTotal = num(inv.subtotal) - num(inv.discount_total) + num(inv.shipping_total) + num(inv.tax_total);
if (!close(expectedTotal, num(inv.total))) flags.push('TOTAL_MISMATCH');

const today = new Date(); today.setHours(0, 0, 0, 0);
const invDate = new Date(inv.invoice_date);
if (isNaN(invDate) || invDate > today || (today - invDate) / 86400000 > 365) flags.push('DATE_CHECK');
if (inv.due_date && new Date(inv.due_date) < invDate) flags.push('DATE_CHECK:due_before_invoice');

const norm = (s) => String(s ?? '').toUpperCase().replace(/[^A-Z0-9]/g, '');
const key = `${norm(inv.vendor_name)}|${norm(inv.invoice_number).replace(/(^|[A-Z])0+(?=\d)/g, '$1')}`; // INV-00123 -> INV123
if (existingKeys.includes(key)) flags.push('DUPLICATE');

if (inv.confidence === 'low' || (inv.uncertain_fields ?? []).length) flags.push('HUMAN_CHECK');
if (inv.bank_details_present) flags.push('VERIFY_BANK_DETAILS_BY_PHONE_IF_NEW_OR_CHANGED');

return {
  json: {
    ...inv,
    line_sum: Math.round(lineSum * 100) / 100,
    norm_key: key,
    flags: flags.join(', '),
    status: flags.includes('DUPLICATE') ? 'Blocked – duplicate' : 'Awaiting approval',
  },
};

What you give up

  • Extraction errors happen, especially on scans, photos, handwriting, multi-page tables and unusual layouts. Human review before payment is mandatory.
  • n8n's Extract From File node reads text from PDFs; its docs don't mention OCR, so scanned invoices need a vision-capable model or manual entry.
  • No three-way matching against purchase orders and goods receipts, no approval chains by amount, no vendor master management.
  • Doesn't post bills into accounting software by itself; that needs another integration step or manual entry.
  • Path A is still manual (upload, paste); it saves typing, not the whole process.
  • Gemini features in Sheets/Drive depend on your Google Workspace edition or Google AI plan.

Risks

  • highsensitive data

    Invoices contain bank details, amounts and personal data. OpenAI states it does not train on API, ChatGPT Business or Enterprise data by default; Anthropic states it does not train on commercial-product (Team, Enterprise, API) inputs by default. Consumer plans can differ — check your plan's data settings and your contracts before uploading supplier invoices.

  • highreliability

    A misread digit or a hallucinated total can lead to an over- or under-payment. Rule-based validation catches arithmetic and duplicates, not a wrong-but-consistent number — the human check against the PDF is the control.

  • mediumsecurity

    Automating the inbox can speed up invoice fraud (fake invoices, changed bank details) if people trust the pipeline. Keep the bank-details verification rule and never pay from a link or number on the invoice itself.

  • mediumcompliance

    Tax rules may require retaining original invoices and, in some countries, structured e-invoices. Keep the original PDFs in Drive and check local requirements; the extracted sheet is not a substitute for the original.

  • mediumfunctionality loss

    Compared with dedicated AP tools you lose supplier portals, automatic accounting sync, approval routing and audit trails.

Before you cancel

Run these in parallel with your current tool. Only cancel when every box is ticked.

0/7 checks passed

Saved in this browser only. Cancel the old subscription only after every check passes.

How the savings work

Savings = what you pay today for invoice capture (an AP-capture subscription or a parser add-on plus the automation tasks it consumes) − incremental cost of the new path (Path A: $0 with an AI plan you already have; Path B: LLM API usage). Time saved on data entry is real but not counted in dollars. Human review time remains in both cases.

Illustrative example

A small agency processing ~60 supplier invoices a month with a paid invoice-capture add-on, switching to Path B on an n8n instance it already runs.

Before$30.00/mo
After$5.00/mo
Saving$25.00/mo · $300/yr
  • ILLUSTRATIVE: $30/month for the current invoice-capture tool is an assumed figure, not a verified vendor price — use your own invoice.
  • ILLUSTRATIVE: $5/month is a conservative allowance for LLM API usage at ~60 invoices; actual cost depends on model and pages per invoice and was not verified against a price list today.
  • n8n and Google Workspace are already paid for; no new subscriptions.
  • Human review (~1 minute per invoice) continues in both setups.

Personalise this workflow

Tailors the steps and prompts to your tools, team and volume using only our verified catalog data.

Tools you already pay for

Sources & freshness

  1. n8n docs — Information Extractor node — accessed Oct 13, 2026 · Schema types: from attribute descriptions, generate from JSON example, define using JSON Schema.
  2. n8n docs — Extract From File — accessed Oct 13, 2026 · Supports Extract From PDF; input binary field default 'data'; OCR not mentioned.
  3. n8n docs — Gmail Trigger — accessed Oct 13, 2026 · Polling trigger with label, search and read-status filters.
  4. Gemini in Google Sheets — accessed Oct 13, 2026 · Side panel can create tables, summarise Drive files and insert results; requires an eligible Workspace edition or Google AI plan.
  5. Google Workspace pricing / plan comparison — accessed Oct 13, 2026 · Business Starter: Gemini in Docs/Sheets not supported; Standard and Plus include it (prices shown in local currency to our fetch, not used).
  6. OpenAI — Business data privacy — accessed Oct 13, 2026 · No training on ChatGPT Business/Enterprise/Edu or API data by default.
  7. Anthropic — Is my data used for model training? — accessed Oct 13, 2026 · Commercial products (Team, Enterprise, API): inputs/outputs not used for training by default.

Evidence: editorial — No dedicated AP-tool prices were verified; example is illustrative. OpenAI's business-data statement was confirmed via search results pointing to openai.com/business-data (direct fetch of OpenAI pages returned 403). The n8n Code node was written for this workflow and syntax-checked, not run against a live instance. · Machine-readable: JSON · Markdown