Loss Runs
Loss runs are the historical claims record that underwriters rely on to evaluate a risk. The @openinsure/loss-runs package handles the full lifecycle: requesting loss runs from prior carriers, extracting data from PDF and CSV formats, analyzing trends, and flagging risks.
Loss Run Lifecycle
Section titled “Loss Run Lifecycle”Producer submits new business │ ▼Request loss runs from prior carriers │ ▼Carrier responds (PDF or CSV attachment) │ ▼Extract structured data (AI + parser) │ ▼5-year loss analysis + trend detection │ ▼Risk flags surfaced in UW queue │ ▼Underwriter reviews → Quote / Decline / ReferRequesting Loss Runs
Section titled “Requesting Loss Runs”Loss run requests are sent via the submission workflow when a submission is received:
POST /v1/submissions/:id/request-loss-runsAuthorization: Bearer <token>Content-Type: application/json
{ "priorCarrier": { "name": "Acme Mutual Insurance", "claimsEmail": "lossruns@acmemutual.com", "policyNumber": "GL-2022-001234" }, "yearsRequested": 5, "effectiveDateRange": { "from": "2020-01-01", "to": "2025-01-01" }}This sends a templated email to the prior carrier’s claims department and creates a pending request record. When the carrier replies with an attachment, the shared mailbox cron routes the attachment to the loss run extractor.
Extraction
Section titled “Extraction”The @openinsure/loss-runs package includes both a structured CSV extractor and an AI-powered PDF extractor.
CSV extraction
Section titled “CSV extraction”import { extractFromCSV } from '@openinsure/loss-runs';
const lossRun = await extractFromCSV(csvString, { mapping: { claimNumber: 'Claim #', dateOfLoss: 'Loss Date', status: 'Status', paidLoss: 'Paid Indemnity', paidExpense: 'Paid Expense', reserve: 'Outstanding Reserve', totalIncurred: 'Total Incurred', },});// lossRun.years[].claims[] — structured claim-level data// lossRun.years[].totals — annual summaryPDF extraction (AI)
Section titled “PDF extraction (AI)”PDF loss runs are rendered to images and processed by the AI extraction bridge:
import { extractFromPDF } from '@openinsure/loss-runs';
const lossRun = await extractFromPDF(pdfBuffer, { carrier: 'Acme Mutual', policyNumber: 'GL-2022-001234',});// Same output structure as CSV extraction// extractionConfidence: 0.0–1.0 (below 0.75 → manual review)Loss Analysis
Section titled “Loss Analysis”Once extracted, the analyzeLossRun() function produces a 5-year summary:
import { analyzeLossRun } from '@openinsure/loss-runs';
const analysis = analyzeLossRun(lossRun, { earnedPremium: [180_000, 195_000, 210_000, 225_000, 240_000], // 5 years});
// analysis.lossRatioByYear: [0.42, 0.38, 0.61, 0.35, 0.44]// analysis.fiveYearLossRatio: 0.44// analysis.largestSingleClaim: 95_000// analysis.openClaimsCount: 2// analysis.trendDirection: 'STABLE' | 'DETERIORATING' | 'IMPROVING'// analysis.flags: [...]Risk Flags
Section titled “Risk Flags”The analyzer automatically generates flags based on the loss history:
| Flag | Severity | Trigger |
|---|---|---|
HIGH_LOSS_RATIO | Critical | 5-year loss ratio > 70% |
DETERIORATING_TREND | Warning | Loss ratio increased ≥ 15 points in last 2 years |
OPEN_CLAIMS | Warning | ≥ 2 open claims at time of submission |
CATASTROPHIC_LOSS | Critical | Single claim > 50% of annual premium |
FREQUENCY | Warning | > 3 claims per year (small commercial) |
RAPID_RESERVE_DEVELOPMENT | Warning | Open reserves grew > 40% since last loss run |
CARRIER_NONRENEWAL | Info | Current carrier did not offer renewal |
analysis.flags.forEach((flag) => { console.log(flag.code); // 'HIGH_LOSS_RATIO' console.log(flag.severity); // 'CRITICAL' console.log(flag.description); // '5-year loss ratio of 84% exceeds 70% threshold' console.log(flag.year); // 2024 (if year-specific)});Flags are surfaced in the UW queue with color-coded badges and require acknowledgment before an underwriter can bind the policy.
Loss Run Generator
Section titled “Loss Run Generator”For policyholders requesting their own loss run (to submit with a new application elsewhere), the generateLossRun() function produces a formatted loss run from OpenInsure’s claim records:
import { generateLossRun } from '@openinsure/loss-runs';
const lossRunPDF = await generateLossRun({ policyId: 'pol_01J8...', yearsRequested: 5, asOfDate: '2025-06-01', requestedBy: 'Acme Roofing LLC', format: 'pdf', // 'pdf' | 'csv' | 'json'});POST /v1/policies/:id/loss-runAuthorization: Bearer <token>Content-Type: application/json
{ "yearsRequested": 5, "asOfDate": "2025-06-01", "requestedBy": "Acme Roofing LLC", "format": "pdf"}The generated loss run is stored in R2 and a download link is returned. Producers can also trigger this from the producer portal via Policy → Request Loss Run.
UW Queue Integration
Section titled “UW Queue Integration”Loss run analysis results automatically populate the submission’s Risk Scorecard in the Underwriting Workbench:
- Loss ratio by year (bar chart)
- Trend indicator (arrow + color)
- Flag summary with drill-down to individual claim data
- Comparison to program benchmark loss ratio