DEV Community

Alberto Landa
Alberto Landa

Posted on

I built a 100% local invoice reader with Ollama + n8n — the real trick was teaching it NOT to guess

I'm not a professional developer — I run a small business and I'm self-taught (HTML/CSS/JS, a bit of Python). Every month I had the same chore: a pile of PDF invoices and me copying supplier, date, concept and total into a spreadsheet by hand. Boring, and full of typos.

I automated it with n8n, but with two hard rules:

  • Invoices never leave my computer. They're supplier and business data — I didn't want to push them to any cloud API.
  • No monthly subscription per model call.

So I ran it on Ollama locally (qwen2.5-coder:7b) instead of a paid API. It works on a normal laptop — a Ryzen 7, 16 GB RAM, no pro GPU — at ~12 s per invoice and €0 cost per run.

The flow is 7 nodes: a trigger (Gmail label "Invoices" in production) → a Code node with 3 sample invoices embedded so anyone can test it with zero setup → an HTTP Request to Ollama asking for JSON (supplier, date, concept, total) → a classifier → a summary → a Telegram ping → a slot to dump into Google Sheets.

The part that actually mattered

At first the model invented data when an invoice was blurry or badly scanned — a disaster for accounting. The fix was to ask the prompt for an extra confidence field (0 to 1) and add one simple rule:

if confidence >= 0.8 AND supplier AND total  ->  OK
else                                          ->  REVIEW
Enter fullscreen mode Exit fullscreen mode

Now dubious invoices don't slip through: they're flagged and I check them in 10 seconds. I'd rather that than a silent €300 error. The prompt uses temperature: 0 and format: json so it doesn't ramble.

A few gotchas that cost me time

  • If n8n runs in Docker, the Ollama node must point to http://host.docker.internal:11434, not localhost — that's the classic "connection refused".
  • The first invoice is much slower (~1 min on my laptop) because Ollama loads the model into memory the first time. From the second one it's ~8–10 s. First one is the toll, then it flies.
  • If everything comes back "REVIEW", your confidence threshold is too high — drop it to 0.6.
  • If the PDF is a scan with no text layer, the model gets nothing — you need an OCR step first.

Two questions for anyone who's done this

  • How do you handle the "REVIEW" bucket — a second pass with another model, or straight to human review like me?
  • For production: Gmail Trigger, a watched folder, or a webhook?

Happy to share the full workflow JSON if it's useful. Any critique welcome — I'm more bar-counter than code. 🙂

Top comments (0)