Turn loss runs and insurance submissions into underwriting-ready data
Loss run reports are how carriers report claims history, and every one is laid out differently across dozens of pages of dense tables. This cookbook covers how you can create an end-to-end pipeline in Reducto that turns the whole report in one call into clean, structured tables you can load straight into code.
Insurance teams rarely receive one clean document. A renewal submission might include an ACORD application, loss runs from several carriers, policy declarations, schedules of values, endorsements, and supporting claims documents, often as one long PDF. The layouts change from carrier to carrier, but the data your underwriters and claims teams need does not.
This cookbook shows how to route each upload before doing heavier processing, preserve every claims table, and use Deep Extract to turn carrier-specific loss runs into reliable, reviewable data. You can then extend the same pattern to the rest of the insurance submission.
What you'll build
- A lightweight classification layer that routes documents before Parse
- A splitter for combined submission packets
- A carrier-agnostic loss run parser that preserves dense claims tables
- Deep Extract claim records with source-ready fields for underwriting and analytics
- A repeatable pipeline for underwriting submissions, claims intake, policy servicing, and portfolio review
Setup
Grab an API key from studio.reducto.ai -> API Keys -> Create new API key, then set it and install the SDK:
bashexport REDUCTO_API_KEY="your-api-key-here" pip install reducto
Use any carrier's loss run report: a downloaded PDF, a scan, or an Excel export. No template setup or per-carrier tuning required. Need a sample? Open this loss run report in Reducto Studio, where the report is already loaded. Run the pipeline right there, or download it as a PDF to follow the API steps below.
Step 1: Classify
Classify is the fast routing step at the top of the pipeline, which tells you what type of document you’re dealing with before you parse it or choose an extraction schema.
Define categories around the decisions your workflow needs to make:
pythonclassification = client.classify.run( input=upload.file_id, classification_schema=[ { "category": "loss_run", "criteria": [ "claim-level history", "loss dates and claim status", "paid, reserve, or incurred amounts", ], }, { "category": "application", "criteria": [ "applicant and risk details", "coverage requested", "ACORD form or insurance application", ], }, { "category": "policy_document", "criteria": [ "policy number and effective dates", "limits, deductibles, forms, or endorsements", "declarations or coverage terms", ], }, { "category": "schedule", "criteria": [ "repeating rows of locations, vehicles, properties, or assets", "exposure values or insured values", ], }, { "category": "submission_packet", "criteria": [ "contains multiple distinct insurance document types", "combined application, loss run, policy, or schedule pages", ], }, { "category": "other", "criteria": [ "does not match the insurance document types above", ], }, ], ) document_type = classification.result.category print(document_type)
Include an other category because Classify always returns the best match from the categories you provide. If the result is submission_packet, split it before applying document-specific processing.
Step 2: Split
When a broker sends one PDF containing several documents, use Split to identify the sections before you select the Parse configuration and extraction schema for each document type:
pythonpacket = client.upload(file=Path("renewal_submission.pdf")) split_result = client.split.run( input=packet.file_id, split_description=[ { "name": "Application", "description": "Insurance application or ACORD application pages", }, { "name": "Loss Runs", "description": "Carrier loss run reports with claim history tables", "partition_key": "carrier_or_policy", }, { "name": "Policy Documents", "description": "Declarations, coverage forms, and endorsements", }, { "name": "Schedules", "description": "Schedules of locations, values, vehicles, or assets", }, { "name": "Supporting Documents", "description": "Any remaining exhibits or supporting correspondence", }, ], settings={"table_cutoff": "preserve"}, split_rules=( "Keep each source document together. A page may belong to only one " "section, except for a shared separator or cover page." ), ) for section in split_result.result.splits: print(section.name, section.pages, section.conf)
table_cutoff: "preserve" is useful when the distinguishing information appears deep inside a schedule or loss table. For faster routing on simpler packets, use the default.
Step 3: Parse
Once routing tells you that the document is a loss run, parse it with settings suited to dense, multi-page tables. HTML table output preserves merged headers and column relationships that Markdown can flatten. Chunking stays disabled so Deep Extract can later see the report as one coherent claims history.
pythonparse_result = client.parse.run( input=upload.file_id, formatting={ "table_output_format": "html", }, settings={ "extraction_mode": "hybrid", "ocr_system": "standard", }, spreadsheet={ "clustering": "accurate", "split_large_tables": {"enabled": True, "size": 50}, }, retrieval={ "chunking": {"chunk_mode": "disabled"}, }, ) print( f"Parsed {parse_result.usage.num_pages} pages. " f"Review the result in Studio: {parse_result.studio_link}" )
If a scan has skewed rows or broken table alignment, add agentic table correction:
enhance={"agentic": [{"scope": "table"}]}
Deep Extract can only verify information that Parse can see, so getting the table structure right first is important.
Step 4: Deep Extract
Deep Extract runs an agentic verification loop that checks and refines the structured result against the source. That extra verification is valuable for long loss runs, where a single-pass extraction can miss a row, shift values between columns, or stop before the final page. You can still use standard Extract for short and simple documents.
Carrier labels vary: one report might say Total Incurred, while another splits the same concept across paid and outstanding columns. Define one schema for the fields your system expects:
pythonclaim_schema = { "type": "object", "properties": { "named_insured": { "type": "string", "description": "Named insured shown on the loss run", }, "policy_number": { "type": "string", "description": "Policy number associated with the report", }, "valuation_date": { "type": "string", "description": "Date through which claim values are reported", }, "claims": { "type": "array", "description": "Every claim row across every page of the report", "items": { "type": "object", "properties": { "claim_number": {"type": "string"}, "loss_date": {"type": "string"}, "status": {"type": "string"}, "description": {"type": "string"}, "paid": {"type": "number"}, "reserve": {"type": "number"}, "expense": {"type": "number"}, "incurred": {"type": "number"}, }, }, }, }, }
Reuse the completed Parse job so the document is not parsed twice, and give the agentic loop concrete verification criteria:
pythonextract_result = client.extract.run( input=f"jobid://{parse_result.job_id}", instructions={ "schema": claim_schema, "system_prompt": ( "Extract every claim on every page. Verify that every source claim " "row appears exactly once in the result. Preserve the values as " "reported by the carrier. Use null when a value is absent; do not " "calculate or infer missing financial amounts." ), }, settings={"deep_extract": True}, ) loss_run = extract_result.result[0] print(f"Deep Extract normalized {len(loss_run['claims'])} claims")
The explicit verification criteria give Deep Extract a concrete definition of “done.” The “do not calculate” instruction is equally important: extract what the carrier reported, then perform reconciliation and derived calculations in deterministic application code.
Prefer no code?
Build the same flow in Reducto Studio: upload a representative packet, configure Classify, Split, Parse, and Deep Extract, then compare the result with the source side by side. Once the output matches the way your team works, deploy the configuration as an API endpoint for the rest of your documents.
Where this goes next
- Add citations when reviewers need to jump from a normalized value to the source page.
- Keep schemas specific to the decision: underwriting, claims, policy servicing, and compliance teams often need different views of the same document.
- Version schemas and prompts so downstream systems can distinguish old and new output contracts.
- Add deterministic checks for financial relationships such as
paid + reserve, but keep those calculations outside extraction. - Feed clean, typed results into your underwriting workbench, claims platform, warehouse, RAG index, or agent.