# Bill of Lading Processing — Extend AI Skill

## What this pipeline does

This pipeline ingests a Uniform Straight Bill of Lading (BOL) document, parses it into structured markdown chunks, and extracts critical shipping logistics fields including carrier details, origin/destination, consignee information, and itemized freight data with weights and classifications. The output is a fully-typed JSON object ready for downstream TMS (Transportation Management System) integration or database insertion.

## When to use this

- **Freight forwarding automation**: Automatically extract BOL data into your TMS without manual data entry
- **Shipment reconciliation**: Parse multiple BOLs in batch to reconcile with purchase orders and delivery confirmations
- **Compliance & auditing**: Generate structured, searchable records of all BOL fields for regulatory filing
- **Multi-carrier operations**: Handle BOLs from different carriers (FedEx Freight, YRC, J.B. Hunt) in a single pipeline
- **EDI preparation**: Convert scanned or digital BOLs into structured data for EDI 204 (Motor Carrier Shipment Information) feeds

## Processor pipeline

### Step 1: Parse (parse_performance)
**Purpose**: Convert the BOL PDF (scanned or digital) into clean markdown and identify logical document boundaries.

**Config choices**:
- `chunkingStrategy: "document"` — treats the entire BOL as one logical unit (no chunk splitting). BOLs are single-page or double-page documents; splitting by page would fragment header from line items.
- `blockOptions.text.agentic.enabled: false` — disables agentic OCR. BOLs are highly standardized forms with consistent layouts; light OCR is sufficient and faster.
- Processor: `extraction_performance` — balances accuracy and latency for structured extraction.

**Why this config**: BOLs follow Uniform Straight Bill of Lading AAMVA standard format. The document structure is predictable: header block (carrier, shipper, consignee), middle block (itemized freight), footer (signatures). No need for expensive agentic scanning.

### Step 2: Extract (extraction_performance)
**Purpose**: Pull structured fields from the parsed BOL into a Zod schema with full type safety.

**Config choices**:
- `baseProcessor: extraction_performance` — optimized for accuracy on structured forms. BOL fields (PRO number, weight, classification codes) are critical for logistics operations.
- Schema: Full Zod object with nullable strings for optional fields, `extendCurrency()` for freight charges, `extendDate()` for date fields, arrays for line items.
- No review agent — BOL fields are unambiguous (PRO number is always a number, weight is always numeric). Human review is unnecessary for standard formats.

**Why this config**: BOLs are high-value, high-frequency documents. Accuracy beats latency. The standardized format means extraction is deterministic.

---

## TypeScript implementation



---

## CLI equivalent

```bash
# Step 1: Parse the BOL
extend parse bol_sample.pdf \
  --chunk-strategy document \
  --disable-agentic-ocr

# Step 2: Extract using the inline schema
extend extract bol_sample.pdf \
  --schema bol_schema.json \
  --processor extraction_performance
```

Where `bol_schema.json` contains:
```json
{
  "pro_number": { "type": "string", "description": "Unique PRO (Progressive) number assigned by carrier for tracking" },
  "bol_number": { "type": ["string", "null"], "description": "Bill of Lading reference number" },
  "shipment_date": { "type": "string", "format": "date", "description": "Date shipment originated (ISO yyyy-mm-dd)" },
  "shipper_name": { "type": ["string", "null"], "description": "Full legal name of the shipper" },
  "consignee_name": { "type": ["string", "null"], "description": "Full legal name of the consignee" },
  "total_weight_lbs": { "type": ["number", "null"], "description": "Total shipment weight in pounds" },
  "freight_class": { "type": ["string", "null"], "description": "NMFC freight class (e.g., '50', '55', '60')" },
  "line_items": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "piece_count": { "type": ["number", "null"], "description": "Number of pieces in this line item" },
        "weight_per_piece_lbs": { "type": ["number", "null"] },
        "description": { "type": ["string", "null"] }
      }
    }
  }
}
```

---

## Schema

The extraction schema is defined using **Zod** for full type safety. Key design decisions:

### Core Identifier Fields
- **`pro_number`** (string, nullable): The carrier's unique tracking number. This is the primary key for logistics operations and TMS lookups.
- **`bol_number`** (string, nullable): May differ from PRO; captured separately for shipper reconciliation.

### Shipper & Consignee Blocks
Both are nullable strings because scanned BOLs occasionally have illegible or missing fields. Address fields are parsed as separate fields (street, city, state, ZIP) rather than one concatenated string to enable database normalization and address validation.

### Freight Classification
- **`freight_class`** (string, nullable): NMFC class code (50–500) determines freight rates. Critical for billing accuracy.
- **`line_items`** (array of objects): Each item captures piece count, weight, description, and class code. This mirrors the BOL's physical layout and enables itemized billing verification.

### Charges
- **`freight_charge`** (extendCurrency): Uses Extend's currency helper to parse "$X,XXX.XX" formats and return `{ amount: number, iso_4217_currency_code: string }`.
- **`prepaid_collect`** (enum): Restricted to "Prepaid", "Collect", or "Unknown" to ensure clean data entry into accounting systems.

### Dates
- **`shipment_date`**, **`delivery_date`**, **`signature_date`**: Use `extendDate()` helper to parse various date formats (2024-01-15, 01/15/2024, Jan 15 2024) into ISO yyyy-mm-dd.

**Why nullable fields?**: Scanned BOLs from older carriers or handwritten sections may omit optional fields. Nullable fields prevent extraction failures and allow downstream systems to handle missing data gracefully.

---

## Accuracy tips

1. **Describe each field as a logistics professional would**: Instead of "Item weight", use "Total weight of this line item (pieces × weight per piece)". The extraction engine learns from examples — detailed descriptions prevent confusing shipment total weight with individual piece weights.

2. **Separate address into atomic fields**: Never ask for "full_address" as a single string.