Documents

Capture invoice PDFs into a table

Read supplier invoices (text or scanned PDFs), extract the invoice number, dates and total, and insert them into an Invoices data table.

Suppliers email Northwind their invoices as PDFs — most generated by their accounting system, a few scanned. This workflow reads each invoice, picks out the invoice number, the dates and the total, and adds a row to an Invoices data table. Invoices it cannot read are sent to a person.

Steps: start form → Read Document (OCR for scans) → Extract from Document → Branch by Condition → Data (insert), or a User Task.

The invoice from Exotic Liquids reads:

text
Invoice INV-2026-0042

Date: 15 October 2026
Bill to: Exotic Liquids, accounts@exotic.example

Product Qty Amount
Chai 10 $180.00
Chang 5 $95.00

Total due: NZ$275.00 by 2026-11-14

1. Data table and start form

Create Invoices with the columns invoice_number (text), supplier_email (text), invoice_date (date), due_date (date), total (number), currency (text) and pages (number). Give the workflow a start form with an Attachments field invoice (PDF only, at most 1 file).

2. Read the invoice

Read Document
{
  "type": "doc_read",
  "label": "Read invoice",
  "config": {
    "source": "{{form.invoice}}",
    "ocr": "auto",
    "ocr_languages": "eng",
    "output_var": "invoice"
  }
}

The canvas shows OCR auto → invoice. With OCR: Pages without text, a PDF from an accounting system is read from its text layer (OCR never starts), and a scan is recognised page by page. For a text PDF the run shows:

json
{ "variable": "invoice", "file": "INV-2026-0042.pdf", "format": "pdf", "pages": 2, "words": 33, "characters": 219, "rows": 2, "tables": 1, "truncated": false, "text_preview": "Invoice INV-2026-0042 Date: 15 October 2026 Bill to: Exotic Liquids, accounts@exotic.example Product Qty Amount Chai 10 $180.00 Chang 5 $95.00 Total due: NZ$275.00 by 2026-11-14 Page two: terms and conditions apply." }

For a scan, each page in {{invoice.structure.pages}} has has_text_layer: false and an ocr entry with its confidence. OCR needs a provider on the instance; see Scanned PDFs and OCR.

3. Extract the values

Extract from Document takes text as well as files, so give it the text Read Document already has — the PDF is not read, or recognised, a second time:

Extract from Document
{
  "type": "doc_extract",
  "label": "Extract invoice fields",
  "config": {
    "source": "{{invoice_text}}",
    "extract_tables": false,
    "patterns": [
      { "name": "invoice_number", "pattern": "Invoice\\s+(INV-[\\d-]+)" },
      { "name": "total_text", "pattern": "Total due:\\s*(\\S+)" }
    ],
    "finders": ["emails", "dates", "amounts"],
    "date_order": "DMY",
    "output_var": "invoice_data"
  }
}

The canvas shows 2 patterns · emails, dates, amounts. The result:

{{invoice_data}}
{
  "file": "text.txt",
  "rows": [],
  "values": { "invoice_number": "INV-2026-0042", "total_text": "NZ$275.00" },
  "found": {
    "emails": ["accounts@exotic.example"],
    "dates": [{ "text": "15 October 2026", "date": "2026-10-15" }, { "text": "2026-11-14", "date": "2026-11-14" }],
    "amounts": [
      { "text": "$180.00", "value": 180, "currency": "USD" },
      { "text": "$95.00", "value": 95, "currency": "USD" },
      { "text": "NZ$275.00", "value": 275, "currency": "NZD" }
    ]
  }
}
  • {{invoice_data_invoice_number}} is INV-2026-0042.
  • The amounts finder turns NZ$275.00 into a number and a currency. The total is the last amount on this layout: {{=LAST(PLUCK(invoice_data.found.amounts, "value"))}} is 275 and {{=LAST(PLUCK(invoice_data.found.amounts, "currency"))}} is NZD. The line items before it are read as USD because a plain $ is US dollars — another reason to take the total from the "Total due" line.
  • The dates finder gives ISO dates in the order they appear: the invoice date first, the due date last. Date order: Day first reads 03/04/2026 as 3 April.

Patterns match within one line and go through the ReDoS guard. Adjust them to each supplier's wording (Invoice No\.?\s*(\S+), Amount due\s*(\S+)), or add one Extract step per supplier behind a condition on {{invoice_data.found.emails.0}}.

4. Check, then insert

Add Branch by Condition: when {{invoice_data_invoice_number}} is empty, the invoice could not be read — send it to a User Task for manual entry. You can also route scans with low OCR confidence there: {{=MIN(PLUCK(invoice.structure.pages, "ocr.confidence"))}} less than 70.

On the Else path, insert the row:

Data (insert)
{
  "type": "update_data_table",
  "label": "Save invoice",
  "config": {
    "table_id": "<invoices-table-id>",
    "table_name": "Invoices",
    "operation": "insert",
    "column_values": {
      "invoice_number": "{{invoice_data_invoice_number}}",
      "supplier_email": "{{invoice_data.found.emails.0}}",
      "invoice_date": "{{=FIRST(PLUCK(invoice_data.found.dates, \"date\"))}}",
      "due_date": "{{=LAST(PLUCK(invoice_data.found.dates, \"date\"))}}",
      "total": "{{=LAST(PLUCK(invoice_data.found.amounts, \"value\"))}}",
      "currency": "{{=LAST(PLUCK(invoice_data.found.amounts, \"currency\"))}}",
      "pages": "{{invoice.metadata.pages}}"
    }
  }
}

The run ends with a new Invoices row: INV-2026-0042, accounts@exotic.example, 2026-10-15, 2026-11-14, 275, NZD, 2.

Line items too

To store the lines as well, add a second Extract step on the file itself ("source": "{{form.invoice}}", "to_table": true) into an Invoice lines table: the table Product / Qty / Amount is found from the positions of the text on the page. That works for text PDFs only — tables are not detected on OCR'd pages — and, being best effort, for simple grids. See Extract from Document.

Messy layouts: use an agent

When suppliers' layouts vary too much for patterns, replace step 3 with Run Agent: put {{invoice.markdown}} in the prompt ("Return the invoice number, invoice date, due date, total and currency of this invoice") and give the agent declared outputs invoice_number, invoice_date, due_date, total and currency. They become workflow variables you map in the Data step the same way. Keep the condition in step 4: check that the values are there before you insert them.