cd /news/ai-agents/building-intelligent-document-parsin… · home › topics › ai-agents › article
[ARTICLE · art-140347] src=gist.github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Building Intelligent Document Parsing Systems: Balancing AI Capabilities with Deterministic Fail-Safes

Jawad Ul Hadi, a backend lead and architect, built a NestJS module that converts timesheets in six formats — PDF, DOCX, XLSX, XLS, PNG and JPG — into a fixed eight-column payroll export, using deterministic parsing as the primary path and an LLM only as a bounded, redacted, budget-capped fallback. The AI path redacts PII before any external call, runs at temperature 0 with forced tool use, and its output is re-parsed and re-scored by the same deterministic pipeline, winning only if it scores strictly higher. Hadi estimates the fallback fires on roughly 40% of documents, putting AI cost at about $22 per month for 10,000 documents, and notes an AI code review caught ten real bugs before merge.

by read6 min views2 publishedSep 27, 2026
  • Jawad Ul Hadi, Backend Lead / Architect — AI-First Systems Design

I built a NestJS module that turns timesheets in six formats (PDF, DOCX, XLSX, XLS, PNG, JPG) into one fixed eight-column payroll export. "AI-first" meant weighing AI at every design decision, and then using it only where it pays for itself. Deterministic parsing does the work. An LLM is a bounded, redacted, budget-capped fallback that can never fail an upload. AI was also part of how the module was built: planning, implementation, and an AI code review that caught ten real bugs before merge.

HR operations receive timesheets from client sites in whatever format each site uses: weekly grids with repeated In/Out/Hours blocks, individual timecards with dated punches, monthly day-numbered sheets, scans and phone photos. Payroll needs one thing: an eight-column workbook with one row per employee per worked day. #

Employee Number | First Name | Last Name | Full Name | Final Department | Employee Start | Final End | Final Hours
8092            | Jane       | Doe       | Jane Doe  | Afternoon Shift  | 8/24/2026 14:30| 8/25/2026 0:00 | 9.00

The rules are simple to state and easy to get wrong:

  • If the source has it, extract it. If not, leave it blank. Nothing is inferred and nothing is back-filled from the HR database.
  • AM/PM must survive. An overnight shift's end time moves to the next day.
  • Start and end are real Excel datetimes, not strings. Hours are decimals.

AI-first does not mean "send everything to an LLM". It means AI is considered first at every design decision, and then placed where it earns its cost, latency and privacy risk. I applied that in two places. #

flowchart TD
    U[Upload] --> V{Validate MIME + size}
    V --> R{Route by format}
    R -- XLSX / XLS / DOCX --> N[Native table extraction]
    R -- Digital PDF --> T[Text layer to grid]
    R -- PNG / JPG --> O[OCR to grid]
    N & T & O --> P[Deterministic parser<br/>weekly grid / timecard]
    P --> Q{Quality gate<br/>confident?}
    Q -- yes --> F[Flatten to 8 columns]
    Q -- no --> K{AI enabled?}
    K -- no --> F
    K -- yes --> AI[Redact PII, then LLM tool call,<br/>then re-parse and re-score]
    AI --> B{Better than deterministic?<br/>ties go to deterministic}
    B --> F
    F --> X[Specimen XLSX + audit record]

Timesheets are tabular. A parser that understands the layout is faster, free and reproducible, and it is right most of the time. The LLM handles the leftovers: noisy OCR, odd layouts, ambiguous rows. So the cost model is:

cost per document = P(AI invoked) × cost per AI call
                  ≈ 0.40 × $0.0055  (small model, ~3k in / ~500 out tokens)
                  ≈ $0.0022   →  about $22 per month at 10,000 documents

Quoting a flat "cost per LLM call" would have overstated the bill 2–10×, because clean digital files never reach the model.

Guardrails on the AI path:

Concern Control
Privacy Emails, URLs, SSNs and phone numbers are redacted before any external call. Only a text grid is sent, never the file and never a signed URL.
Determinism Temperature 0, forced tool use, JSON-schema output, versioned prompt
Trust The AI output is re-parsed and re-scored by the same deterministic pipeline, and it wins only if it scores strictly higher
Cost A pre-flight token estimate is checked against a per-upload USD budget. If it's over, the call is skipped
Availability Short timeout plus a circuit breaker. Any failure returns null , so the deterministic result ships
Audit Provider, model, prompt version, tokens, cost estimate, confidence and decision reason are stored per upload
Logs Status codes only: no prompts, no document text, no model output

With no API key configured, the module runs fully deterministic at zero AI cost. AI is a switch, not a dependency.

Phase How AI was used What I kept for myself
Proposal Drafted the cost model and compared providers (Anthropic, OpenAI, Google, open-weight) per invoked call Scope boundaries, the decision to keep AI off the critical path, the provider recommendation
Contract Turned the sample export into explicit rules (blank-not-inferred, overnight, native datetimes) Signing off the contract as the source of truth
Build Claude Code implemented the layered NestJS module against the house patterns (guards, response envelopes, repositories) Architecture, the simplification calls, reviewing every diff
Review An AI code review of the rewrite found 10 real bugs Triage, fix design, regression tests
Verification Ran the parser against the real input template instead of trusting unit tests Deciding what "done" means

The review is the part I'd highlight. A "lean" rewrite removed about 8,800 lines: extra provider abstractions, a Python OCR sidecar, dead indirection. Simplifying that aggressively also dropped things that mattered:

  • The JWT guard disappeared from the controller, so every route was unauthenticated and crashed onuser._id .
  • An SDK import had no matching dependency, so the app would have crashed on boot .
  • Storage returned file:// paths instead of private cloud URLs.
  • Scanned PDFs produced empty exports marked "completed" : silent data loss.
  • A phone-number redaction regex also matched ISO dates and runs of decimal hours, so the LLM received corrupted data.
  • Weekday detection matched on a 3-letter prefix, so "Mon thly Total" became a second Monday and hours were counted twice.
  • A timezone "correction" shifted every exported time by five hours, because the Excel library writes dates from UTC.

Each fix came with a regression test that fails on the old code. Then I ran the parser against the real supplied template, not synthetic fixtures. It returned zero rows: that layout uses day numbers (1 In / 1 Out / 1 Hours), not weekday names. The unit tests were green, and the acceptance criterion was not. That gap is now the top item on the backlog instead of a production surprise.

Decision Alternative Why
Deterministic-first, LLM fallback LLM extracts everything Cheaper, reproducible, auditable. The LLM covers the long tail
Re-parse the AI output with the same parser Trust the model's JSON One definition of "correct", and it makes AI and non-AI results comparable
Deterministic wins ties Prefer the newer or smarter path Stable output. AI has to be better to be adopted
Store floating wall-clock times (printed time in UTC fields) Apply a business-timezone offset Excel writers serialise from UTC. The export must show what was printed
Reject scanned PDFs with a clear 422 OCR them via a vision model No rasteriser in the stack, and sending raw files breaks the "text only" privacy rule. A clear error beats a silent empty export
One concrete AI service Provider interface + factory + DI tokens One active vendor. Swapping means rewriting ~150 lines. The export contract doesn't change
Blank beats inferred Look up missing IDs or departments in HR The source document is the source of truth, and the export stays auditable

NestJS 11 · TypeScript 5 · MongoDB (Mongoose) · Google Cloud Storage (private, signed URLs) · ExcelJS · SheetJS · mammoth · pdf-parse · tesseract.js · Anthropic Messages API (tool use) · Jest

Dated: 27-Sep-2026

── more in #ai-agents 4 stories · sorted by recency
── more on @jawad ul hadi 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-intelligent…] indexed:0 read:6min 2026-09-27 · —