AI Agent

I Used Codex to Pull Structured Data Out of a Folder of PDF Invoices

· 5 min read · YayaAgent Team

The first version of this tool wasn't about extraction at all. It was a small web form: drag a batch of PDF invoices in, get back a clean, print-ready layout. It worked, mostly — except every so often, an invoice would come out with the text shrunk down to nearly unreadable size, squeezed to fit a page it wasn't designed for. Same code, same template, different PDF in, different result out. I never fully tracked down why before I moved on to something else.

Months later I came back to the same folder of invoices with a different goal: instead of reformatting them for print, I wanted the data out — vendor, invoice number, date, total — dropped into a spreadsheet I could actually use for bookkeeping. I built this second version with Codex, and about twenty minutes in, I hit a bug that made the original shrinking-layout mystery finally make sense.


Starting simple: "just read the PDFs"

The first prompt was about as minimal as it gets:

"Read every PDF in this folder and pull out the vendor name, invoice number, date, due date, and total into a CSV."

Codex reached for pdfplumber to extract text from each PDF and a handful of regex patterns to find labeled fields like "Invoice #" or "Total". Reasonable first pass. I ran it against three sample invoices and checked the output.

Two of the three fields looked right. The vendor name did not. Instead of Yaya Consulting, the CSV had Yaya Consulting INVOICE — my company name, glued directly to the word "INVOICE" with no space of substance between them.

Why the vendor name absorbed a random word

Here's the part that connected back to the old shrinking-layout bug. The invoice template puts the company info on the left side of the header and the word "INVOICE" plus the invoice number on the right side — a two-column layout, purely visual. But when a PDF gets read back out as text, there's no guarantee the extraction follows that visual arrangement. Depending on how the PDF was generated, "left column, then right column" and "everything on this horizontal line, left to right" can produce completely different text order. In my case, the two columns on the same row were getting flattened into a single line: company name immediately followed by "INVOICE".

That's the same root issue as the layout-shrinking bug from the original tool, just showing up differently. A PDF's visual layout and its internal text/structure are two separate things that don't always agree — one manifests as "the print layout doesn't scale right," the other as "the extracted text doesn't split where you'd expect." Once I described the symptom to Codex that way — "the company name and the word INVOICE are ending up on the same line even though they're visually in different columns" — the fix was a small one: strip a trailing "INVOICE" label off whatever line looks like a vendor name, rather than trusting line breaks to separate the two columns cleanly.

def guess_vendor(text: str) -> str:
    for line in text.splitlines():
        line = re.sub(r"\s*INVOICE\s*$", "", line.strip(), flags=re.I).strip()
        if line:
            return line
    return "UNKNOWN"

Reran it. Clean output across all three test invoices.

What the finished script actually does

  • Loops through every PDF in a folder
  • Pulls vendor, invoice number, invoice date, due date, and total using pattern matching that's deliberately loose — "Invoice #", "Invoice No.", and "# 1001" all match, because real-world invoices label the same field differently
  • Flags rows instead of guessing quietly. If a field can't be found, the row gets marked REVIEW (total not found) instead of just being left blank. This was a deliberate carry-over from the "don't trust silent extraction" habit — a blank cell in a spreadsheet looks the same whether the tool found nothing or found the wrong thing, and that ambiguity is worse than an obvious flag
  • Only works on text-based PDFs, not scanned photo receipts — I said as much to Codex up front, since OCR for scanned images is a genuinely different pipeline, not a variation on this one

Running it against three sample invoices produced a summary CSV in a few seconds, with every field checked against the source data:

source_file vendor invoice_id total confidence
invoice_1001.pdf Yaya Consulting 1001 1,520.00 ok
invoice_1002.pdf Yaya Consulting 1002 650.00 ok
invoice_1003.pdf Yaya Consulting 1003 1,200.00 ok

The full script is available to download below.


Going further: extracting every line item, not just the total

The version above only pulls the invoice-level total — enough for a quick bookkeeping summary, but not enough if you need the actual itemized breakdown (useful for time tracking, expense categorization, or auditing what you billed a client for). I didn't build that part myself, but this is exactly the kind of extension where being specific in the prompt matters more than being technical. Something close to this works:

"Extend this so it also pulls out every individual line item from the table on each invoice — not just the total. Each table has columns for Description, Qty, Unit Price, and Amount. Write these to a second CSV, one row per line item, with the invoice_id included on each row so I can still group them back together. Don't assume every invoice has the same number of line items — some might have one, some might have five or more."

The last sentence is the part people tend to leave out, and it's the part that actually matters. Without it, there's a decent chance the agent writes something that only reliably reads the first line item, or hardcodes an assumption about table size based on whatever sample data it was shown. Being explicit about the variable case up front — rather than fixing it after the agent guesses wrong — is a small habit that saves a full iteration cycle.


What I'd tell someone trying this

The bug is rarely where you think it is. I assumed the shrinking-layout problem in the original tool was a print/rendering issue. It took building the reverse tool — extraction instead of formatting — to realize both bugs came from the same source: PDFs don't store "what it looks like" and "what order the content is in" as the same thing.

Say what you see, not what you think is wrong. "The vendor field has an extra word in it" got fixed faster than any attempt on my part to diagnose why would have. If you're newer to this style of working with an agent, a beginner-friendly overview of how AI agents operate is a decent primer before you start describing bugs to one.

"Don't guess silently" is worth asking for explicitly. I had to specifically request that missing fields get flagged rather than left blank — it's not something Codex added on its own. For any tool that touches financial or record-keeping data, ask for this up front rather than after you've already trusted a wrong number.

Tools that read your local files need a bit of setup thinking. If this is your first project pointing an agent at files on your own machine, it's worth understanding how agents get access to local files before you start — it changes how you should scope folder permissions.

Pick the agent for the task, not out of habit. I used Codex here because it was already open. Whether that's the right call for a document-parsing tool versus something else is worth a real comparison rather than a guess — see Claude Code vs. Codex vs. Cursor if you're choosing for the first time.


Download

  • extract_invoices.py — the full extraction script
  • sample_invoices/ — three sample PDF invoices to test against
  • extraction_summary.csv — the output you'd get from running it

Source available on yayaagent-toolkit (link to be added once the repo folder is published).


Further Reading