It takes a messy scanned PDF, sends it to PDF API Hub’s Analyze API, detects multiple tables, and returns clean structured data—ready for databases, spreadsheets, or downstream automations.
Scan → Extract → Structure → Automate
No manual data entry. Just one n8n workflow. n8n#n8nAutomation#OCR#WorkflowAutomation#PDF#DocumentAI#NoCode
Your Analyze node is returning the tables, but the workflow JSON you shared has no node after it and no connections — that is why nothing lands in Google Sheets yet.
Field mapping
Do not dump the whole Analyze JSON into one cell. Pin the Analyze output, find the array of table rows (usually under a tables / rows / cells key — use whatever your item actually shows), then add a Split Out (or a small Code node) so each row is one n8n item.
Make a Sheet with a header row you control, for example:
In Google Sheets → Append row, map each column by name. Keep OCR labels (Invoice No., Inv #) on the left of the mapping, Sheet headers on the right. If Analyze gives you two tables, map only the one whose header row matches that Sheet (line items vs totals are different tables).
Confidence
If the Analyze payload includes a score per cell or per table, add an IF before Sheets:
score ≥ your floor (start at 0.85 if the field is a required key like invoice number / total) → append
below floor, or a required mapped field is empty → do not append that row
If this node does not expose a score, treat “empty required cell”, “two tables claiming the same total”, or “row column count ≠ header count” as low confidence.
Into the Sheet
Analyze a PDF document (you already have this; point analyze_url at the live file, not only the sample S3 PDF)
Split Out / Code → one item per row
IF (confidence / missing required fields)
Google Sheets → Append row, mapped columns only
(optional) second Sheets tab exceptions for the rows you did not append
Test with one known PDF and check that header names in the Sheet match the mapped keys exactly (including spaces).
Where a human should look
A person should open the exception tab (or a Slack/email of that row) when any of these fire — not after every PDF:
required field empty after mapping
confidence below the floor
more than one table and you cannot tell which one is the line-item table
amount columns that do not add up to the mapped total
a row that would duplicate doc_id + row_index already in the Sheet
Do not skip that look and write straight to the ledger tab. Scanned PDFs will misread a digit; Sheets will not catch it for you.
If you paste one Analyze output JSON here (redact any real names/amounts), I can point at the exact keys to map.
Thanks — I checked pages → predicted_tables in the JSON you posted.
Do not Append the whole payload. This file is one page, two tables (table_index 1 and 2), 12 rows each. The row objects are already keyed by the detected headers.
Split Out
Split Out predicted_tables (2 items). The same array also sits on the item root, so you can split that field if pages is still nested.
Split Out rows on each table (12 + 12 → 24 row items).
If after that you still have 1 item, Split Out did not run on rows.
Field mapping (Google Sheets → Append row)
Freeze a header row you control, then map OCR keys onto it. Do not use the OCR header as the Sheet name.
page | table_index | Date | Open | High | Low | Close | Volume
Date ← Date
Open ← Open
High ← High
Low ← Low
Close ← Close / LastorClose / [-ast — table 1 used the garbled header, so mapping only Close / Last drops every row from table 1
Volume ← Volume
page / table_index from the parent table item (pass them through a small Set/Code node before Sheets)
Do not map full_text. Do not put both tables into one row.
Confidence / empty cell → human gate
pages[0].confidence is 91. That is the page, not the cell. Cell scores are under pages[0].tables[].cells[].confidence (table 1 Date cells are ~40–44).
IF before the ledger Append — hold the row (do not write it) when any of these fire:
required mapped field empty (Date / Open / High / Low / Close / Volume)
Open is 11 while High is 117.531 (table 2, 12/29/2016) — truncated cell
a price field uses a comma as the decimal (63,405, 63,43, 63,54, 63,025 on table 1)
Close header is Close / [-ast
Those rows go to an exceptions tab (same columns + reason). A person opens that tab; they do not re-read every PDF. Then Google Sheets → Append row on the ledger tab, mapped columns only.
Pin Analyze after Split Out rows and you should see 24 items before the IF.