Paperboy Current Process
This document describes the end-to-end journey a PDF takes through Paperboy, from upload to final data export.
Pipeline Overview
All artifacts are stored in Azure Blob Storage under the path:
Step 1 — Upload
Trigger: User uploads a PDF via the UI or API.
Endpoints:
POST /api/v1/files/upload— single filePOST /api/v1/files/upload-multiple— batch upload
What happens:
- The file is received as a multipart form upload and its MIME type is validated (PDF only).
- The PDF buffer is uploaded to Azure Blob Storage at:
- A new
FileDatarecord is inserted into the database with:
- If an Agentic workflow was previously paused waiting for this upload (
status = 'awaiting_upload'), it is automatically resumed.
FileData record at this point:
Step 2 — Classification
Trigger:
- Direct API call:
POST /api/v1/files/:id/classify - Agentic chat: the
classify_filetool is invoked by the Agentic Loop when the LLM decides to classify
What happens:
2a. Text Extraction
The full PDF is downloaded from Blob Storage and all text is extracted page-by-page using pdf-parse via extractPDFText().
2b. Chunked Classification (LLM)
Because documents can be large, the text is processed in 10-page chunks. For each chunk:
- A prompt is built with the page range (e.g.
"Pages 1–10 of 45") and the chunk’s text. - The prompt is sent to Azure OpenAI (
temperature: 0.3) using thepdf_classificationskill loaded from the database. - The LLM returns a structured JSON response identifying the form types present in those pages.
- If parsing fails for a chunk, it is labelled
Unidentified Form Dataand processing continues.
2c. Merge & Normalise
After all chunks are processed:
- All per-chunk
forms[]arrays are concatenated. - Consecutive forms of the same type are merged into a single entry.
- Any
Unidentified Form Dataentries are renamed toExtra form data. - Non-contiguous unidentified page ranges are expressed using a
page_ranges[]array instead ofpage_start/page_end.
2d. Produce classification.json
The merged result is serialised and uploaded to Blob Storage:
classification.json schema:
Key fields:
2e. Update Database
The FileData record is updated:
Step 3 — Document Splitting
Trigger: Happens automatically at the end of Step 2 when is_multi_form === true OR forms.length > 1.
There is no separate API call — splitting is embedded inside the classification step.
What happens:
3a. Build Page Ranges
The forms[] array from classification.json is walked to build a list of page ranges:
Both the page_start/page_end and page_ranges[] variants are handled.
3b. Validate Ranges
All page ranges are validated to be within document bounds. Ranges outside the document’s page count are discarded.
3c. Split the PDF
splitPDFByPageRanges() in pdfProcessor.ts uses pdf-lib to create one new PDFDocument per form:
- For each page range, a new empty PDF is created.
- The relevant pages are copied from the source document (converting from 1-indexed to 0-indexed internally).
- The result is serialised to a
Buffer.
3d. Upload Child PDFs & Create Child Records
For each split document:
- The child PDF is uploaded to Blob Storage at:
e.g. tax2024_Form_1040.pdf, tax2024_Schedule_C.pdf
- A new child
FileDatarecord is inserted, linked to the parent:
After this step the original PDF has been divided into N child PDFs, each covering exactly one form type.
Step 4 — Extraction
Trigger:
- Direct API call:
POST /api/v1/files/:id/extract - Agentic chat:
extract_filetool invoked by the Agentic Loop
Scope: Extraction operates on each child file independently. If the file has no children (single-form document), it is extracted directly.
What happens:
4a. Determine Form Type
The form type is resolved in this order:
- Read
file.form_typedirectly from theFileDatarow (set during splitting). - Fall back to downloading
classification.jsonfrom Blob and indexingforms[file.form_index].
4b. Select Extraction Skill
The form type string is mapped to a specific LLM skill stored in the Skills database table:
4c. LLM Extraction
- The child PDF buffer is downloaded from Blob Storage.
- Text is extracted via
extractPDFText(). - The skill’s system and user prompts are loaded from the DB, with
{formType}and{pdfText}substituted. - The prompt is sent to Azure OpenAI (
temperature: 0.1for precision). - The response is parsed as structured JSON.
4d. Produce extraction.json
The parsed extraction result is uploaded to Blob Storage:
Example extraction.json (Form 1040):
4e. Update Database
Step 5 — Export (CSV)
Trigger:
- Direct API call:
POST /api/v1/files/:id/export - Agentic chat:
export_filetool
What happens:
- The
extraction.jsonis downloaded from Blob Storage. - The nested JSON object is flattened into key-value rows.
- A CSV is produced using
csv-writerand uploaded to:
- For multi-form documents, each child’s extraction is exported to its own CSV.
FileData.export_csv_pathis updated.
Agentic Orchestration
All steps above can be triggered individually via REST or driven automatically by the Agentic Loop — a ReAct-style LLM agent.
How it works:
- A user sends a chat message (e.g.
"Process tax2024.pdf for John Smith"). - The Agentic Loop iterates up to 10 rounds:
- Asks the LLM what tool to run next.
- Executes the tool (single or in parallel batches via
Promise.allSettled). - Feeds the result back to the LLM.
- Repeats until the LLM signals
completeorneeds_info.
- If a required file has not been uploaded yet, the loop pauses (
awaiting_upload) and resumes automatically when the file arrives. - Each step is recorded in
AgentExecutions.execution_stepsfor audit and replay.
Typical automated sequence: