Extracts shipment details, carrier info, and freight line items from bills of lading.
A bill of lading is a legal contract and receipt issued by a carrier that documents the shipment of goods, including carrier and shipper information, consignee details, itemized freight descriptions with weights and classifications, and transportation terms. This template takes in Bill of Ladings and outputs markdown (.md) with the document's parsed text and layout structure, and JSON (.json) containing structured fields including carrier details, addresses, freight line items, and BOL identifiers per the extraction schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledfalsechunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemano schema definedextractionRulesno custom rulesadvancedOptions.advancedMultimodalEnabledfalseadvancedOptions.reviewAgent.enabledfalsebaseProcessor"extraction_performance"You can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Bill of Lading Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": false
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT"
}
]
}# 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.import { ExtendClient, extendDate, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* processBillofLading
* Ingests a Bill of Lading, parses it, and extracts all logistics fields.
*
* @param filePath Local path to the BOL PDF (e.g., "/tmp/bol_123.pdf")
* @returns Fully-typed BOL data object ready for TMS insertion
*/
export async function processBillofLading(filePath: string) {
// Convert local file to data URL for SDK consumption
const fileBuffer = fs.readFileSync(filePath);
const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;
console.log(`[BOL] Processing file: ${filePath}`);
// ============================================================================
// STEP 1: PARSE
// ============================================================================
console.log("[BOL] Step 1: Parsing Bill of Lading...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: false, // Standard BOL format; light OCR is sufficient
},
},
},
chunkingStrategy: {
type: "document", // Treat entire BOL as one logical unit
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
// Collect all parsed markdown chunks
const markdownContent = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(
`[BOL] Parsed ${parseRun.output.chunks.length} chunks (${markdownContent.length} chars total)`
);
// ============================================================================
// STEP 2: EXTRACT
// ============================================================================
console.log("[BOL] Step 2: Extracting structured BOL fields...");
// Define the Bill of Lading schema using Zod
const bolSchema = z.object({
// Header: Carrier & Shipper Info
carrier_name: z.string().nullable().describe("Legal name of the motor carrier (e.g., 'YRC Worldwide')"),
carrier_phone: z.string().nullable().describe("Carrier contact phone number"),
shipper_name: z.string().nullable().describe("Full legal name of the shipper"),
shipper_address: z.string().nullable().describe("Complete street address of the shipper"),
shipper_city: z.string().nullable().describe("City of shipper origin"),
shipper_state: z.string().nullable().describe("US state code (e.g., 'CA', 'TX')"),
shipper_zip: z.string().nullable().describe("ZIP code of shipper"),
// Consignee Info
consignee_name: z.string().nullable().describe("Full legal name of the consignee (recipient)"),
consignee_address: z.string().nullable().describe("Complete street address of consignee"),
consignee_city: z.string().nullable().describe("City of consignee destination"),
consignee_state: z.string().nullable().describe("US state code of consignee"),
consignee_zip: z.string().nullable().describe("ZIP code of consignee"),
consignee_phone: z.string().nullable().describe("Consignee contact phone number"),
// Shipment Control Numbers & Dates
pro_number: z.string().nullable().describe("Unique PRO (Progressive) number assigned by carrier for tracking"),
bol_number: z.string().nullable().describe("Bill of Lading reference number (may differ from PRO)"),
shipment_date: extendDate().describe("Date shipment originated (ISO yyyy-mm-dd)"),
delivery_date: extendDate().nullable().describe("Expected or actual delivery date (ISO yyyy-mm-dd)"),
// Freight Summary
total_weight_lbs: z.number().nullable().describe("Total shipment weight in pounds"),
total_piece_count: z.number().nullable().describe("Total number of pieces/packages in shipment"),
freight_class: z.string().nullable().describe("NMFC freight class (e.g., '50', '55', '60') for rate determination"),
// Charges & Payment
freight_charge: extendCurrency().describe("Total freight charge amount (includes LH/DH if applicable)"),
special_charge_description: z.string().nullable().describe("Description of any special charges (e.g., 'Liftgate', 'Residential Delivery')"),
special_charge_amount: extendCurrency().nullable().describe("Amount of special charges"),
prepaid_collect: z.enum(["Prepaid", "Collect", "Unknown"]).nullable().describe("Whether freight is prepaid or collect"),
// Line Items (individual freight pieces)
line_items: z.array(
z.object({
sequence: z.number().nullable().describe("Line item sequence number on BOL"),
piece_count: z.number().nullable().describe("Number of pieces in this line item"),
weight_per_piece_lbs: z.number().nullable().describe("Weight per piece in pounds"),
total_weight_lbs: z.number().nullable().describe("Total weight of this line item (pieces × weight per piece)"),
description: z.string().nullable().describe("Item description (e.g., 'Palletized electronics equipment')"),
class_code: z.string().nullable().describe("NMFC class code for this line item"),
commodity_code: z.string().nullable().describe("Optional commodity or tariff code"),
})
).describe("Itemized freight details from BOL"),
// Additional Fields
handling_instructions: z.string().nullable().describe("Special handling instructions (e.g., 'Fragile', 'Keep Dry')"),
reference_number_1: z.string().nullable().describe("Shipper's reference number (PO, job, etc.)"),
reference_number_2: z.string().nullable().describe("Optional secondary reference number"),
// Signatory & Attestation
shipper_signature_name: z.string().nullable().describe("Printed name of shipper representative who signed"),
carrier_representative_name: z.string().nullable().describe("Printed name of carrier representative who received shipment"),
signature_date: extendDate().nullable().describe("Date BOL was signed (ISO yyyy-mm-dd)"),
});
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: bolSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const bolData = extractRun.output.value;
console.log("[BOL] Extraction complete.");
console.log("[BOL] Extracted BOL:", JSON.stringify(bolData, null, 2));
// ============================================================================
// OUTPUT
// ============================================================================
return {
status: "success",
pro_number: bolData.pro_number,
shipment_date: bolData.shipment_date,
shipper: {
name: bolData.shipper_name,
address: bolData.shipper_address,
city: bolData.shipper_city,
state: bolData.shipper_state,
zip: bolData.shipper_zip,
},
consignee: {
name: bolData.consignee_name,
address: bolData.consignee_address,
city: bolData.consignee_city,
state: bolData.consignee_state,
zip: bolData.consignee_zip,
phone: bolData.consignee_phone,
},
carrier: {
name: bolData.carrier_name,
phone: bolData.carrier_phone,
},
freight: {
total_weight_lbs: bolData.total_weight_lbs,
total_piece_count: bolData.total_piece_count,
freight_class: bolData.freight_class,
line_items: bolData.line_items,
},
charges: {
freight_charge: bolData.freight_charge,
special_charge_description: bolData.special_charge_description,
special_charge_amount: bolData.special_charge_amount,
prepaid_collect: bolData.prepaid_collect,
},
references: {
reference_number_1: bolData.reference_number_1,
reference_number_2: bolData.reference_number_2,
handling_instructions: bolData.handling_instructions,
},
raw_extracted_data: bolData,
};
}
// Auto-invoke if run directly (for testing)
if (require.main === module) {
const testFilePath = process.argv[2] || "./bol_sample.pdf";
processBillofLading(testFilePath)
.then((result) => {
console.log("\n=== FINAL RESULT ===");
console.log(JSON.stringify(result, null, 2));
})
.catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
}import os
import json
from typing import Optional, Any
from extend_ai import Extend
client = Extend(token=os.environ["EXTEND_API_KEY"])
def process_bill_of_lading(file_path: str) -> dict[str, Any]:
"""
Ingests a Bill of Lading, parses it, and extracts all logistics fields.
Args:
file_path: Local path to the BOL PDF (e.g., "/tmp/bol_123.pdf")
Returns:
Fully-typed BOL data object ready for TMS insertion
"""
# Read file and convert to data URL for SDK consumption
with open(file_path, "rb") as f:
file_buffer = f.read()
data_url = f"data:application/octet-stream;base64,{__import__('base64').b64encode(file_buffer).decode('utf-8')}"
print(f"[BOL] Processing file: {file_path}")
# ============================================================================
# STEP 1: PARSE
# ============================================================================
print("[BOL] Step 1: Parsing Bill of Lading...")
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"block_options": {
"text": {
"agentic": {
"enabled": False, # Standard BOL format; light OCR is sufficient
},
},
},
"chunking_strategy": {
"type": "document", # Treat entire BOL as one logical unit
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
# Collect all parsed markdown chunks
markdown_content = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
print(
f"[BOL] Parsed {len(parse_run.output.chunks)} chunks ({len(markdown_content)} chars total)"
)
# ============================================================================
# STEP 2: EXTRACT
# ============================================================================
print("[BOL] Step 2: Extracting structured BOL fields...")
# Define the Bill of Lading schema as a plain JSON-schema-like dict
bol_schema = {
"type": "object",
"properties": {
# Header: Carrier & Shipper Info
"carrier_name": {"type": ["string", "null"], "description": "Legal name of the motor carrier (e.g., 'YRC Worldwide')"},
"carrier_phone": {"type": ["string", "null"], "description": "Carrier contact phone number"},
"shipper_name": {"type": ["string", "null"], "description": "Full legal name of the shipper"},
"shipper_address": {"type": ["string", "null"], "description": "Complete street address of the shipper"},
"shipper_city": {"type": ["string", "null"], "description": "City of shipper origin"},
"shipper_state": {"type": ["string", "null"], "description": "US state code (e.g., 'CA', 'TX')"},
"shipper_zip": {"type": ["string", "null"], "description": "ZIP code of shipper"},
# Consignee Info
"consignee_name": {"type": ["string", "null"], "description": "Full legal name of the consignee (recipient)"},
"consignee_address": {"type": ["string", "null"], "description": "Complete street address of consignee"},
"consignee_city": {"type": ["string", "null"], "description": "City of consignee destination"},
"consignee_state": {"type": ["string", "null"], "description": "US state code of consignee"},
"consignee_zip": {"type": ["string", "null"], "description": "ZIP code of consignee"},
"consignee_phone": {"type": ["string", "null"], "description": "Consignee contact phone number"},
# Shipment Control Numbers & Dates
"pro_number": {"type": ["string", "null"], "description": "Unique PRO (Progressive) number assigned by carrier for tracking"},
"bol_number": {"type": ["string", "null"], "description": "Bill of Lading reference number (may differ from PRO)"},
"shipment_date": {"type": "string", "format": "date", "description": "Date shipment originated (ISO yyyy-mm-dd)"},
"delivery_date": {"type": ["string", "null"], "format": "date", "description": "Expected or actual delivery date (ISO yyyy-mm-dd)"},
# Freight Summary
"total_weight_lbs": {"type": ["number", "null"], "description": "Total shipment weight in pounds"},
"total_piece_count": {"type": ["number", "null"], "description": "Total number of pieces/packages in shipment"},
"freight_class": {"type": ["string", "null"], "description": "NMFC freight class (e.g., '50', '55', '60') for rate determination"},
# Charges & Payment
"freight_charge": {"type": "string", "format": "currency", "description": "Total freight charge amount (includes LH/DH if applicable)"},
"special_charge_description": {"type": ["string", "null"], "description": "Description of any special charges (e.g., 'Liftgate', 'Residential Delivery')"},
"special_charge_amount": {"type": ["string", "null"], "format": "currency", "description": "Amount of special charges"},
"prepaid_collect": {"type": ["string", "null"], "enum": ["Prepaid", "Collect", "Unknown"], "description": "Whether freight is prepaid or collect"},
# Line Items (individual freight pieces)
"line_items": {
"type": "array",
"description": "Itemized freight details from BOL",
"items": {
"type": "object",
"properties": {
"sequence": {"type": ["number", "null"], "description": "Line item sequence number on BOL"},
"piece_count": {"type": ["number", "null"], "description": "Number of pieces in this line item"},
"weight_per_piece_lbs": {"type": ["number", "null"], "description": "Weight per piece in pounds"},
"total_weight_lbs": {"type": ["number", "null"], "description": "Total weight of this line item (pieces × weight per piece)"},
"description": {"type": ["string", "null"], "description": "Item description (e.g., 'Palletized electronics equipment')"},
"class_code": {"type": ["string", "null"], "description": "NMFC class code for this line item"},
"commodity_code": {"type": ["string", "null"], "description": "Optional commodity or tariff code"},
},
},
},
# Additional Fields
"handling_instructions": {"type": ["string", "null"], "description": "Special handling instructions (e.g., 'Fragile', 'Keep Dry')"},
"reference_number_1": {"type": ["string", "null"], "description": "Shipper's reference number (PO, job, etc.)"},
"reference_number_2": {"type": ["string", "null"], "description": "Optional secondary reference number"},
# Signatory & Attestation
"shipper_signature_name": {"type": ["string", "null"], "description": "Printed name of shipper representative who signed"},
"carrier_representative_name": {"type": ["string", "null"], "description": "Printed name of carrier representative who received shipment"},
"signature_date": {"type": ["string", "null"], "format": "date", "description": "Date BOL was signed (ISO yyyy-mm-dd)"},
},
}
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": bol_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
bol_data = extract_run.output.value
print("[BOL] Extraction complete.")
print(f"[BOL] Extracted BOL: {json.dumps(bol_data, indent=2)}")
# ============================================================================
# OUTPUT
# ============================================================================
return {
"status": "success",
"pro_number": bol_data.get("pro_number"),
"shipment_date": bol_data.get("shipment_date"),
"shipper": {
"name": bol_data.get("shipper_name"),
"address": bol_data.get("shipper_address"),
"city": bol_data.get("shipper_city"),
"state": bol_data.get("shipper_state"),
"zip": bol_data.get("shipper_zip"),
},
"consignee": {
"name": bol_data.get("consignee_name"),
"address": bol_data.get("consignee_address"),
"city": bol_data.get("consignee_city"),
"state": bol_data.get("consignee_state"),
"zip": bol_data.get("consignee_zip"),
"phone": bol_data.get("consignee_phone"),
},
"carrier": {
"name": bol_data.get("carrier_name"),
"phone": bol_data.get("carrier_phone"),
},
"freight": {
"total_weight_lbs": bol_data.get("total_weight_lbs"),
"total_piece_count": bol_data.get("total_piece_count"),
"freight_class": bol_data.get("freight_class"),
"line_items": bol_data.get("line_items"),
},
"charges": {
"freight_charge": bol_data.get("freight_charge"),
"special_charge_description": bol_data.get("special_charge_description"),
"special_charge_amount": bol_data.get("special_charge_amount"),
"prepaid_collect": bol_data.get("prepaid_collect"),
},
"references": {
"reference_number_1": bol_data.get("reference_number_1"),
"reference_number_2": bol_data.get("reference_number_2"),
"handling_instructions": bol_data.get("handling_instructions"),
},
"raw_extracted_data": bol_data,
}
if __name__ == "__main__":
import sys
test_file_path = sys.argv[1] if len(sys.argv) > 1 else "./bol_sample.pdf"
try:
result = process_bill_of_lading(test_file_path)
print("\n=== FINAL RESULT ===")
print(json.dumps(result, indent=2))
except Exception as err:
print(f"Error: {err}")
sys.exit(1)// This code uses the Extend REST API directly (https://api.extend.ai) because
// Extend does not publish an official Java SDK yet.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
public class BillOfLadingProcessor {
private static final String EXTEND_API_BASE = "https://api.extend.ai";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final HttpClient httpClient = HttpClient.newHttpClient();
/**
* processBillofLading
* Ingests a Bill of Lading, parses it, and extracts all logistics fields.
*
* @param filePath Local path to the BOL PDF (e.g., "/tmp/bol_123.pdf")
* @return Fully-typed BOL data object ready for TMS insertion
*/
public static Map<String, Object> processBillofLading(String filePath)
throws IOException, InterruptedException {
// Convert local file to data URL
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64 = Base64.getEncoder().encodeToString(fileBytes);
String dataUrl = "data:application/octet-stream;base64," + base64;
System.out.println("[BOL] Processing file: " + filePath);
// ========================================================================
// STEP 1: PARSE
// ========================================================================
System.out.println("[BOL] Step 1: Parsing Bill of Lading...");
Map<String, Object> parseResponse = createAndPollParseRun(dataUrl);
String parseStatus = (String) parseResponse.get("status");
if (!parseStatus.equals("PROCESSED")) {
throw new RuntimeException("Parse failed with status: " + parseStatus);
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> chunks =
(List<Map<String, Object>>) parseResponse.get("chunks");
StringBuilder markdownContent = new StringBuilder();
for (Map<String, Object> chunk : chunks) {
markdownContent.append(chunk.get("content")).append("\n\n");
}
System.out.println("[BOL] Parsed " + chunks.size() + " chunks (" +
markdownContent.length() + " chars total)");
// ========================================================================
// STEP 2: EXTRACT
// ========================================================================
System.out.println("[BOL] Step 2: Extracting structured BOL fields...");
Map<String, Object> extractResponse = createAndPollExtractRun(dataUrl);
String extractStatus = (String) extractResponse.get("status");
if (!extractStatus.equals("PROCESSED")) {
throw new RuntimeException("Extraction failed with status: " + extractStatus);
}
@SuppressWarnings("unchecked")
Map<String, Object> bolData =
(Map<String, Object>) extractResponse.get("value");
System.out.println("[BOL] Extraction complete.");
System.out.println("[BOL] Extracted BOL: " + prettyPrintJson(bolData));
// ========================================================================
// OUTPUT
// ========================================================================
return buildBolOutput(bolData);
}
private static Map<String, Object> createAndPollParseRun(String dataUrl)
throws IOException, InterruptedException {
String requestBody = buildParseRequestBody(dataUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/parseRuns"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new RuntimeException("Parse request failed: " + response.statusCode() +
" " + response.body());
}
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = parseJsonObject(response.body());
String runId = (String) responseMap.get("id");
return pollUntilProcessed(runId, "parseRuns");
}
private static Map<String, Object> createAndPollExtractRun(String dataUrl)
throws IOException, InterruptedException {
String requestBody = buildExtractRequestBody(dataUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/extractRuns"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new RuntimeException("Extract request failed: " + response.statusCode() +
" " + response.body());
}
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = parseJsonObject(response.body());
String runId = (String) responseMap.get("id");
return pollUntilProcessed(runId, "extractRuns");
}
private static Map<String, Object> pollUntilProcessed(String runId, String endpoint)
throws IOException, InterruptedException {
while (true) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/" + endpoint + "/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Poll request failed: " + response.statusCode());
}
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = parseJsonObject(response.body());
String status = (String) responseMap.get("status");
if (status.equals("PROCESSED")) {
@SuppressWarnings("unchecked")
Map<String, Object> output =
(Map<String, Object>) responseMap.get("output");
responseMap.put("chunks", output.get("chunks"));
responseMap.put("value", output.get("value"));
return responseMap;
} else if (status.equals("FAILED") || status.equals("ERROR")) {
throw new RuntimeException("Run failed with status: " + status);
}
Thread.sleep(1000);
}
}
private static String buildParseRequestBody(String dataUrl) {
return "{"
+ "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
+ "\"config\": {"
+ "\"blockOptions\": {"
+ "\"text\": {\"agentic\": {\"enabled\": false}}"
+ "},"
+ "\"chunkingStrategy\": {\"type\": \"document\"}"
+ "}"
+ "}";
}
private static String buildExtractRequestBody(String dataUrl) {
String schema = "{"
+ "\"type\": \"object\","
+ "\"properties\": {"
+ "\"bill_of_lading_number\": {\"type\": [\"string\", \"null\"], \"description\": \"BOL number\"},"
+ "\"carrier_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Name of the carrier\"},"
+ "\"carrier_scac_code\": {\"type\": [\"string\", \"null\"], \"description\": \"SCAC code\"},"
+ "\"shipper_address\": {\"type\": [\"string\", \"null\"], \"description\": \"Shipper address\"},"
+ "\"consignee_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Consignee name\"},"
+ "\"destination_address\": {\"type\": [\"string\", \"null\"], \"description\": \"Destination address\"},"
+ "\"delivering_carrier\": {\"type\": [\"string\", \"null\"], \"description\": \"Delivering carrier\"},"
+ "\"trailer_number\": {\"type\": [\"string\", \"null\"], \"description\": \"Trailer number\"},"
+ "\"freight_items\": {\"type\": \"array\", \"description\": \"Freight line items\", \"items\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": [\"string\", \"null\"]}, \"weight\": {\"type\": [\"string\", \"null\"]}, \"class\": {\"type\": [\"string\", \"null\"]}}}},"
+ "\"cod_amount\": {\"type\": [\"string\", \"null\"], \"description\": \"COD amount\"},"
+ "\"bill_type\": {\"type\": [\"string\", \"null\"], \"description\": \"Bill type\"}"
+ "}"
+ "}";
return "{"
+ "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
+ "\"config\": {\"schema\": " + schema + "}"
+ "}";
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseJsonObject(String json) {
json = json.trim();
if (!json.startsWith("{")) {
throw new RuntimeException("Invalid JSON: " + json);
}
Map<String, Object> result = new HashMap<>();
int depth = 0;
StringBuilder currentKey = new StringBuilder();
StringBuilder currentValue = new StringBuilder();
boolean inKey = true;
boolean inQuotes = false;
boolean escaped = false;
for (int i = 1; i < json.length() - 1; i++) {
char c = json.charAt(i);
if (escaped) {
currentValue.append(c);
escaped = false;
continue;
}
if (c == '\\' && inQuotes) {
escaped = true;
currentValue.append(c);
continue;
}
if (c == '"') {
inQuotes = !inQuotes;
if (inKey) {
currentKey.append(c);
} else {
currentValue.append(c);
}
continue;
}
if (!inQuotes) {
if (c == ':' && inKey && depth == 0) {
inKey = false;
currentKey.setLength(currentKey.length() - 1);
currentValue = new StringBuilder();
continue;
}
if ((c == ',' || c == '}') && depth == 0 && !inKey) {
String key = currentKey.toString().trim();
String value = currentValue.toString().trim();
result.put(key, parseJsonValue(value));
currentKey = new StringBuilder();
currentValue = new StringBuilder();
inKey = true;
if (c == '}') break;
continue;
}
if (c == '{' || c == '[') depth++;
if (c == '}' || c == ']') depth--;
}
if (inKey) {
currentKey.append(c);
} else {
currentValue.append(c);
}
}
return result;
}
private static Object parseJsonValue(String value) {
value = value.trim();
if (value.equals("null")) {
return null;
}
if (value.equals("true")) {
return true;
}
if (value.equals("false")) {
return false;
}
if (value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
try {
if (value.contains(".")) {
return Double.parseDouble(value);
} else {
return Long.parseLong(value);
}
} catch (NumberFormatException e) {
return value;
}
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static String prettyPrintJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{\n");
int count = 0;
for (Map.Entry<String, Object> entry : map.entrySet()) {
sb.append(" \"").append(entry.getKey()).append("\": ");
if (entry.getValue() == null) {
sb.append("null");
} else if (entry.getValue() instanceof String) {
sb.append("\"").append(entry.getValue()).append("\"");
} else {
sb.append(entry.getValue());
}
if (++count < map.size()) sb.append(",");
sb.append("\n");
}
sb.append("}");
return sb.toString();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> buildBolOutput(Map<String, Object> bolData) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("status", "success");
result.put("pro_number", bolData.get("bill_of_lading_number"));
Map<String, Object> shipper = new LinkedHashMap<>();
shipper.put("address", bolData.get("shipper_address"));
result.put("shipper", shipper);
Map<String, Object> consignee = new LinkedHashMap<>();
consignee.put("name", bolData.get("consignee_name"));
consignee.put("address", bolData.get("destination_address"));
result.put("consignee", consignee);
Map<String, Object> carrier = new LinkedHashMap<>();
carrier.put("name", bolData.get("carrier_name"));
carrier.put("scac_code", bolData.get("carrier_scac_code"));
result.put("carrier", carrier);
Map<String, Object> freight = new LinkedHashMap<>();
freight.put("line_items", bolData.get("freight_items"));
result.put("freight", freight);
Map<String, Object> charges = new LinkedHashMap<>();
charges.put("cod_amount", bolData.get("cod_amount"));
result.put("charges", charges);
result.put("raw_extracted_data", bolData);
return result;
}
public static void main(String[] args) {
try {
String testFilePath = (args.length > 0) ? args[0] : "./bol_sample.pdf";
Map<String, Object> result = processBillofLading(testFilePath);
System.out.println("\n=== FINAL RESULT ===");
System.out.println(prettyPrintJson(result));
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}// This code uses Extend's REST API directly because Extend has no official Go SDK yet.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
// ParseRunResponse represents the response from a parse run
type ParseRunResponse struct {
Status string `json:"status"`
Output struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
} `json:"output"`
}
// ExtractRunResponse represents the response from an extract run
type ExtractRunResponse struct {
Status string `json:"status"`
Output json.RawMessage `json:"output"`
}
// LineItem represents a freight line item
type LineItem struct {
Sequence *int `json:"sequence"`
PieceCount *int `json:"piece_count"`
WeightPerPieceLbs *int `json:"weight_per_piece_lbs"`
TotalWeightLbs *int `json:"total_weight_lbs"`
Description *string `json:"description"`
ClassCode *string `json:"class_code"`
CommodityCode *string `json:"commodity_code"`
}
// BolData represents the extracted Bill of Lading data
type BolData struct {
CarrierName *string `json:"carrier_name"`
CarrierPhone *string `json:"carrier_phone"`
ShipperName *string `json:"shipper_name"`
ShipperAddress *string `json:"shipper_address"`
ShipperCity *string `json:"shipper_city"`
ShipperState *string `json:"shipper_state"`
ShipperZip *string `json:"shipper_zip"`
ConsigneeName *string `json:"consignee_name"`
ConsigneeAddress *string `json:"consignee_address"`
ConsigneeCity *string `json:"consignee_city"`
ConsigneeState *string `json:"consignee_state"`
ConsigneeZip *string `json:"consignee_zip"`
ConsigneePhone *string `json:"consignee_phone"`
ProNumber *string `json:"pro_number"`
BolNumber *string `json:"bol_number"`
ShipmentDate *string `json:"shipment_date"`
DeliveryDate *string `json:"delivery_date"`
TotalWeightLbs *int `json:"total_weight_lbs"`
TotalPieceCount *int `json:"total_piece_count"`
FreightClass *string `json:"freight_class"`
FreightCharge *string `json:"freight_charge"`
SpecialChargeDescription *string `json:"special_charge_description"`
SpecialChargeAmount *string `json:"special_charge_amount"`
PrepaidCollect *string `json:"prepaid_collect"`
LineItems []LineItem `json:"line_items"`
HandlingInstructions *string `json:"handling_instructions"`
ReferenceNumber1 *string `json:"reference_number_1"`
ReferenceNumber2 *string `json:"reference_number_2"`
ShipperSignatureName *string `json:"shipper_signature_name"`
CarrierRepresentativeName *string `json:"carrier_representative_name"`
SignatureDate *string `json:"signature_date"`
}
// ProcessBillofLading ingests a BOL, parses it, and extracts all logistics fields
func ProcessBillofLading(filePath string) (map[string]interface{}, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
// Read file and convert to data URL
fileBuffer, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
dataURL := "data:application/octet-stream;base64," + base64.StdEncoding.EncodeToString(fileBuffer)
log.Printf("[BOL] Processing file: %s", filePath)
// ========================================================================
// STEP 1: PARSE
// ========================================================================
log.Println("[BOL] Step 1: Parsing Bill of Lading...")
parseReqBody := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
"config": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]bool{
"enabled": false,
},
},
},
"chunkingStrategy": map[string]string{
"type": "document",
},
},
}
parseReqData, _ := json.Marshal(parseReqBody)
parseReq, _ := http.NewRequest("POST", "https://api.extend.ai/parse_runs", bytes.NewBuffer(parseReqData))
parseReq.Header.Set("Authorization", "Bearer "+apiKey)
parseReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Minute}
parseResp, err := client.Do(parseReq)
if err != nil {
return nil, fmt.Errorf("parse request failed: %w", err)
}
defer parseResp.Body.Close()
parseRespBody, _ := ioutil.ReadAll(parseResp.Body)
var parseRun ParseRunResponse
if err := json.Unmarshal(parseRespBody, &parseRun); err != nil {
return nil, fmt.Errorf("failed to unmarshal parse response: %w", err)
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
// Collect all parsed markdown chunks
markdownContent := ""
for i, chunk := range parseRun.Output.Chunks {
if i > 0 {
markdownContent += "\n\n"
}
markdownContent += chunk.Content
}
log.Printf("[BOL] Parsed %d chunks (%d chars total)", len(parseRun.Output.Chunks), len(markdownContent))
// ========================================================================
// STEP 2: EXTRACT
// ========================================================================
log.Println("[BOL] Step 2: Extracting structured BOL fields...")
// Schema definition matching the provided Bill of Lading schema
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"carrier_name": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Legal name of the motor carrier",
},
"carrier_phone": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Carrier contact phone number",
},
"shipper_name": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Full legal name of the shipper",
},
"shipper_address": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Complete street address of the shipper",
},
"shipper_city": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "City of shipper origin",
},
"shipper_state": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "US state code",
},
"shipper_zip": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "ZIP code of shipper",
},
"consignee_name": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Full legal name of the consignee",
},
"consignee_address": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Complete street address of consignee",
},
"consignee_city": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "City of consignee destination",
},
"consignee_state": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "US state code of consignee",
},
"consignee_zip": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "ZIP code of consignee",
},
"consignee_phone": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Consignee contact phone number",
},
"pro_number": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Unique PRO number assigned by carrier",
},
"bol_number": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Bill of Lading reference number",
},
"shipment_date": map[string]interface{}{
"type": "string",
"description": "Date shipment originated (ISO yyyy-mm-dd)",
},
"delivery_date": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Expected or actual delivery date (ISO yyyy-mm-dd)",
},
"total_weight_lbs": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Total shipment weight in pounds",
},
"total_piece_count": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Total number of pieces/packages",
},
"freight_class": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "NMFC freight class",
},
"freight_charge": map[string]interface{}{
"type": "string",
"description": "Total freight charge amount",
},
"special_charge_description": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Description of any special charges",
},
"special_charge_amount": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Amount of special charges",
},
"prepaid_collect": map[string]interface{}{
"type": []interface{}{"string", "null"},
"enum": []string{"Prepaid", "Collect", "Unknown"},
"description": "Whether freight is prepaid or collect",
},
"line_items": map[string]interface{}{
"type": "array",
"description": "Itemized freight details from BOL",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"sequence": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Line item sequence number",
},
"piece_count": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Number of pieces in this line item",
},
"weight_per_piece_lbs": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Weight per piece in pounds",
},
"total_weight_lbs": map[string]interface{}{
"type": []interface{}{"number", "null"},
"description": "Total weight of this line item",
},
"description": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Item description",
},
"class_code": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "NMFC class code for this line item",
},
"commodity_code": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Optional commodity or tariff code",
},
},
},
},
"handling_instructions": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Special handling instructions",
},
"reference_number_1": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Shipper's reference number",
},
"reference_number_2": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Optional secondary reference number",
},
"shipper_signature_name": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Printed name of shipper representative",
},
"carrier_representative_name": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Printed name of carrier representative",
},
"signature_date": map[string]interface{}{
"type": []interface{}{"string", "null"},
"description": "Date BOL was signed (ISO yyyy-mm-dd)",
},
},
}
extractReqBody := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
"config": map[string]interface{}{
"schema": schema,
},
}
extractReqData, _ := json.Marshal(extractReqBody)
extractReq, _ := http.NewRequest("POST", "https://api.extend.ai/extract_runs", bytes.NewBuffer(extractReqData))
extractReq.Header.Set("Authorization", "Bearer "+apiKey)
extractReq.Header.Set("Content-Type", "application/json")
extractResp, err := client.Do(extractReq)
if err != nil {
return nil, fmt.Errorf("extract request failed: %w", err)
}
defer extractResp.Body.Close()
extractRespBody, _ := ioutil.ReadAll(extractResp.Body)
var extractRun ExtractRunResponse
if err := json.Unmarshal(extractRespBody, &extractRun); err != nil {
return nil, fmt.Errorf("failed to unmarshal extract response: %w", err)
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
}
var bolValue struct {
Value BolData `json:"value"`
}
if err := json.Unmarshal(extractRun.Output, &bolValue); err != nil {
return nil, fmt.Errorf("failed to unmarshal BOL data: %w", err)
}
bolData := bolValue.Value
log.Println("[BOL] Extraction complete.")
// ========================================================================
// OUTPUT
// ========================================================================
result := map[string]interface{}{
"status": "success",
"pro_number": bolData.ProNumber,
"shipment_date": bolData.ShipmentDate,
"shipper": map[string]interface{}{
"name": bolData.ShipperName,
"address": bolData.ShipperAddress,
"city": bolData.ShipperCity,
"state": bolData.ShipperState,
"zip": bolData.ShipperZip,
},
"consignee": map[string]interface{}{
"name": bolData.ConsigneeName,
"address": bolData.ConsigneeAddress,
"city": bolData.ConsigneeCity,
"state": bolData.ConsigneeState,
"zip": bolData.ConsigneeZip,
"phone": bolData.ConsigneePhone,
},
"carrier": map[string]interface{}{
"name": bolData.CarrierName,
"phone": bolData.CarrierPhone,
},
"freight": map[string]interface{}{
"total_weight_lbs": bolData.TotalWeightLbs,
"total_piece_count": bolData.TotalPieceCount,
"freight_class": bolData.FreightClass,
"line_items": bolData.LineItems,
},
"charges": map[string]interface{}{
"freight_charge": bolData.FreightCharge,
"special_charge_description": bolData.SpecialChargeDescription,
"special_charge_amount": bolData.SpecialChargeAmount,
"prepaid_collect": bolData.PrepaidCollect,
},
"references": map[string]interface{}{
"reference_number_1": bolData.ReferenceNumber1,
"reference_number_2": bolData.ReferenceNumber2,
"handling_instructions": bolData.HandlingInstructions,
},
"raw_extracted_data": bolData,
}
return result, nil
}
func main() {
testFilePath := "./bol_sample.pdf"
if len(os.Args) > 1 {
testFilePath = os.Args[1]
}
result, err := ProcessBillofLading(testFilePath)
if err != nil {
log.Fatalf("Error: %v", err)
}
fmt.Println("\n=== FINAL RESULT ===")
resultJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(resultJSON))
}// Deploy the "Bill of Lading" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/bill-of-lading.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// npx tsx provision.ts
//
// Generated by doc1 (template: bill-of-lading).
import fs from "node:fs";
import path from "node:path";
const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }
const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "bill-of-lading.json");
type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
: {};
function saveState() {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
async function api(method: string, pathName: string, body?: unknown) {
const res = await fetch(API + pathName, {
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
"x-extend-api-version": VERSION,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
return data;
}
// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
"name": "Bill of Lading Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": false
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"bill_of_lading_number": {
"type": [
"string",
"null"
],
"description": "Shipper's Bill of Lading Number (e.g., INTL-BL-09284)"
},
"carrier_name": {
"type": [
"string",
"null"
],
"description": "Name of the carrier (e.g., Continental Logistics Group)"
},
"carrier_scac_code": {
"type": [
"string",
"null"
],
"description": "Standard Carrier Alpha Code for the carrier"
},
"shipper_address": {
"type": [
"string",
"null"
],
"description": "Complete shipper address including street, city, state, and zip"
},
"consignee_name": {
"type": [
"string",
"null"
],
"description": "Name of the consignee/receiver"
},
"destination_address": {
"type": [
"string",
"null"
],
"description": "Destination address including street, city, state, and zip"
},
"delivering_carrier": {
"type": [
"string",
"null"
],
"description": "Name of the delivering carrier (e.g., Oakland)"
},
"trailer_number": {
"type": [
"string",
"null"
],
"description": "Trailer identification number"
},
"freight_items": {
"type": "array",
"description": "Array of freight line items with handling units, packages, description, weight, and class",
"items": {
"type": "object",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"weight": {
"type": [
"string",
"null"
]
},
"class": {
"type": [
"string",
"null"
]
}
}
}
},
"cod_amount": {
"type": [
"string",
"null"
],
"description": "Collect on Delivery amount if applicable"
},
"bill_type": {
"type": [
"string",
"null"
],
"description": "Type of bill of lading (e.g., UNIFORM STRAIGHT BILL OF LADING)"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": false
},
"advancedMultimodalEnabled": false
}
}
}
}
]
};
async function main() {
console.log(`Deploying "${WORKFLOW.name}"…`);
if (state.workflowId) {
console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
} else {
// Reuse an existing workflow with the same name if one exists (e.g. a
// previous run's state file was lost) instead of creating a duplicate.
try {
const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
const existing = items.find((x) => x.name === WORKFLOW.name);
if (existing?.id) {
state.workflowId = existing.id; saveState();
console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
}
} catch { /* lookup is best-effort; fall through to create */ }
if (!state.workflowId) {
const created = await api("POST", "/workflows", WORKFLOW);
const wfId = created.id ?? created.workflow?.id;
if (!wfId) throw new Error("Could not read created workflow id from response");
state.workflowId = wfId; saveState();
console.log(`+ created workflow (${wfId})`);
}
}
// Deploy the current draft as a new version so the workflow is runnable —
// best-effort: some accounts/plans may not require this explicit step.
await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});
console.log("\nDone. Run documents through it with:");
console.log(` POST ${API}/workflow_runs { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}
main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
#!/usr/bin/env python3
"""
Deploy the "Bill of Lading" pipeline to your Extend account.
The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
step carries its extractor/classifier/splitter config INLINE, so this is a
single API call. No processors to create or wire up beforehand.
Idempotent: the created workflow id is cached in .extend/bill-of-lading.json,
so re-running updates the existing workflow instead of duplicating it.
Usage:
export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
python provision.py
Generated by doc1 (template: bill-of-lading).
"""
import json
import os
import sys
from pathlib import Path
from typing import Any, Optional
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "bill-of-lading.json"
def load_state() -> dict[str, Any]:
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {}
def save_state(state: dict[str, Any]) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
WORKFLOW = {
"name": "Bill of Lading Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [{"step": "parse1"}],
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {"text": {"agentic": {"enabled": False}}},
"chunkingStrategy": {"type": "document"},
}
},
"next": [{"step": "extraction2"}],
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"bill_of_lading_number": {
"type": ["string", "null"],
"description": "Shipper's Bill of Lading Number (e.g., INTL-BL-09284)",
},
"carrier_name": {
"type": ["string", "null"],
"description": "Name of the carrier (e.g., Continental Logistics Group)",
},
"carrier_scac_code": {
"type": ["string", "null"],
"description": "Standard Carrier Alpha Code for the carrier",
},
"shipper_address": {
"type": ["string", "null"],
"description": "Complete shipper address including street, city, state, and zip",
},
"consignee_name": {
"type": ["string", "null"],
"description": "Name of the consignee/receiver",
},
"destination_address": {
"type": ["string", "null"],
"description": "Destination address including street, city, state, and zip",
},
"delivering_carrier": {
"type": ["string", "null"],
"description": "Name of the delivering carrier (e.g., Oakland)",
},
"trailer_number": {
"type": ["string", "null"],
"description": "Trailer identification number",
},
"freight_items": {
"type": "array",
"description": "Array of freight line items with handling units, packages, description, weight, and class",
"items": {
"type": "object",
"properties": {
"description": {"type": ["string", "null"]},
"weight": {"type": ["string", "null"]},
"class": {"type": ["string", "null"]},
},
},
},
"cod_amount": {
"type": ["string", "null"],
"description": "Collect on Delivery amount if applicable",
},
"bill_type": {
"type": ["string", "null"],
"description": "Type of bill of lading (e.g., UNIFORM STRAIGHT BILL OF LADING)",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {"enabled": False},
"advancedMultimodalEnabled": False,
},
}
},
},
],
}
async def main() -> None:
client = Extend(token=API_KEY)
state = load_state()
print(f'Deploying "{WORKFLOW["name"]}…')
if state.get("workflowId"):
workflow_id = state["workflowId"]
print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
await client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
else:
# Reuse an existing workflow with the same name if one exists.
existing_id: Optional[str] = None
try:
workflows = await client.workflows.list(name=WORKFLOW["name"])
items = workflows.data if hasattr(workflows, "data") else getattr(workflows, "items", [])
for item in items:
if getattr(item, "name", None) == WORKFLOW["name"]:
existing_id = getattr(item, "id", None)
break
except Exception:
pass # lookup is best-effort; fall through to create
if existing_id:
state["workflowId"] = existing_id
save_state(state)
print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps')
await client.workflows.update(id=existing_id, steps=WORKFLOW["steps"])
else:
created = await client.workflows.create(**WORKFLOW)
workflow_id = getattr(created, "id", None) or (
getattr(getattr(created, "workflow", None), "id", None)
)
if not workflow_id:
raise ValueError("Could not read created workflow id from response")
state["workflowId"] = workflow_id
save_state(state)
print(f"+ created workflow ({workflow_id})")
# Deploy the current draft as a new version so the workflow is runnable.
try:
await client.workflows.deploy(id=state["workflowId"])
except Exception:
pass # best-effort: some accounts/plans may not require this explicit step
print("\nDone. Run documents through it with:")
print(f' POST https://api.extend.ai/workflow_runs {{ "workflow": {{ "id": "{state["workflowId"]}" }}, "file": {{ "url": "https://…" }} }}')
print("Or open the workflow in the Extend dashboard to review and deploy it.")
if __name__ == "__main__":
import asyncio
try:
asyncio.run(main())
except Exception as e:
print(str(e) if str(e) else repr(e), file=sys.stderr)
sys.exit(1)// Uses Extend REST API directly (https://api.extend.ai) because Extend has no official Java SDK yet.
// Call the API via HttpClient without third-party dependencies.
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class BillOfLadingProvisioner {
private static final String API = "https://api.extend.ai";
private static final String VERSION = "2026-02-09";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("bill-of-lading.json");
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
private static final HttpClient HTTP = HttpClient.newHttpClient();
private static class State {
String workflowId;
}
private static State loadState() throws IOException {
State state = new State();
if (Files.exists(STATE_FILE)) {
String content = Files.readString(STATE_FILE);
// Simple JSON parsing for { "workflowId": "..." }
if (content.contains("\"workflowId\"")) {
int start = content.indexOf("\"workflowId\"") + 14;
int end = content.indexOf("\"", start);
state.workflowId = content.substring(start, end);
}
}
return state;
}
private static void saveState(State state) throws IOException {
Files.createDirectories(STATE_DIR);
String json = String.format("{%n \"workflowId\": \"%s\"%n}", state.workflowId);
Files.writeString(STATE_FILE, json);
}
private static String toJson(Object obj) {
if (obj instanceof String) return "\"" + ((String) obj).replace("\"", "\\\"") + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof Boolean) return obj.toString();
if (obj == null) return "null";
if (obj instanceof Map) {
StringBuilder sb = new StringBuilder("{");
Map<String, Object> map = (Map<String, Object>) obj;
boolean first = true;
for (Map.Entry<String, Object> e : map.entrySet()) {
if (!first) sb.append(",");
sb.append(String.format("%n \"%s\": %s", e.getKey(), toJson(e.getValue())));
first = false;
}
sb.append("\n}");
return sb.toString();
}
if (obj instanceof List) {
StringBuilder sb = new StringBuilder("[");
List<Object> list = (List<Object>) obj;
for (int i = 0; i < list.size(); i++) {
if (i > 0) sb.append(",");
sb.append(toJson(list.get(i)));
}
sb.append("]");
return sb.toString();
}
return obj.toString();
}
private static Map<String, Object> parseJsonObject(String json) {
Map<String, Object> result = new HashMap<>();
json = json.trim();
if (!json.startsWith("{") || !json.endsWith("}")) return result;
String content = json.substring(1, json.length() - 1).trim();
if (content.isEmpty()) return result;
// Simple key-value parsing for { "id": "...", "data": [...] }
if (content.contains("\"id\"")) {
int start = content.indexOf("\"id\"") + 6;
int end = content.indexOf("\"", start);
if (end > start) {
result.put("id", content.substring(start, end));
}
}
if (content.contains("\"workflow\"")) {
result.put("workflow", new HashMap<>());
}
if (content.contains("\"data\"")) {
result.put("data", new ArrayList<>());
}
if (content.contains("\"items\"")) {
result.put("items", new ArrayList<>());
}
return result;
}
private static Map<String, Object> apiCall(String method, String pathName, String bodyJson)
throws IOException, InterruptedException {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(API + pathName))
.method(method, bodyJson != null ? HttpRequest.BodyPublishers.ofString(bodyJson) : HttpRequest.BodyPublishers.noBody())
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (bodyJson != null) {
builder.header("Content-Type", "application/json");
}
HttpRequest request = builder.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJsonObject(response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String errorMsg = response.body().length() > 300 ? response.body().substring(0, 300) : response.body();
throw new RuntimeException(String.format("%s %s failed (%d): %s", method, pathName, response.statusCode(), errorMsg));
}
return data;
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("bill_of_lading_number", Map.of(
"type", List.of("string", "null"),
"description", "Shipper's Bill of Lading Number (e.g., INTL-BL-09284)"
));
properties.put("carrier_name", Map.of(
"type", List.of("string", "null"),
"description", "Name of the carrier (e.g., Continental Logistics Group)"
));
properties.put("carrier_scac_code", Map.of(
"type", List.of("string", "null"),
"description", "Standard Carrier Alpha Code for the carrier"
));
properties.put("shipper_address", Map.of(
"type", List.of("string", "null"),
"description", "Complete shipper address including street, city, state, and zip"
));
properties.put("consignee_name", Map.of(
"type", List.of("string", "null"),
"description", "Name of the consignee/receiver"
));
properties.put("destination_address", Map.of(
"type", List.of("string", "null"),
"description", "Destination address including street, city, state, and zip"
));
properties.put("delivering_carrier", Map.of(
"type", List.of("string", "null"),
"description", "Name of the delivering carrier (e.g., Oakland)"
));
properties.put("trailer_number", Map.of(
"type", List.of("string", "null"),
"description", "Trailer identification number"
));
Map<String, Object> freightItemProperties = new LinkedHashMap<>();
freightItemProperties.put("description", Map.of("type", List.of("string", "null")));
freightItemProperties.put("weight", Map.of("type", List.of("string", "null")));
freightItemProperties.put("class", Map.of("type", List.of("string", "null")));
properties.put("freight_items", Map.of(
"type", "array",
"description", "Array of freight line items with handling units, packages, description, weight, and class",
"items", Map.of(
"type", "object",
"properties", freightItemProperties
)
));
properties.put("cod_amount", Map.of(
"type", List.of("string", "null"),
"description", "Collect on Delivery amount if applicable"
));
properties.put("bill_type", Map.of(
"type", List.of("string", "null"),
"description", "Type of bill of lading (e.g., UNIFORM STRAIGHT BILL OF LADING)"
));
schema.put("properties", properties);
Map<String, Object> extractorConfig = new LinkedHashMap<>();
extractorConfig.put("schema", schema);
extractorConfig.put("baseProcessor", "extraction_performance");
extractorConfig.put("advancedOptions", Map.of(
"reviewAgent", Map.of("enabled", false),
"advancedMultimodalEnabled", false
));
Map<String, Object> parseConfig = new LinkedHashMap<>();
parseConfig.put("blockOptions", Map.of(
"text", Map.of("agentic", Map.of("enabled", false))
));
parseConfig.put("chunkingStrategy", Map.of("type", "document"));
List<Map<String, Object>> steps = new ArrayList<>();
steps.add(Map.of(
"name", "startTrigger1",
"type", "TRIGGER",
"next", List.of(Map.of("step", "parse1"))
));
steps.add(Map.of(
"name", "parse1",
"type", "PARSE",
"config", Map.of("parseConfig", parseConfig),
"next", List.of(Map.of("step", "extraction2"))
));
steps.add(Map.of(
"name", "extraction2",
"type", "EXTRACT",
"config", Map.of("extractorConfig", extractorConfig)
));
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Bill of Lading Processing Pipeline");
workflow.put("steps", steps);
return workflow;
}
public static void main(String[] args) throws IOException, InterruptedException {
State state = loadState();
Map<String, Object> workflow = buildWorkflow();
String workflowName = (String) workflow.get("name");
System.out.println("Deploying \"" + workflowName + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
String updateBody = toJson(Map.of("steps", workflow.get("steps")));
apiCall("POST", "/workflows/" + state.workflowId, updateBody);
} else {
// Try to find existing workflow with same name
try {
String query = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
Map<String, Object> list = apiCall("GET", "/workflows?name=" + query, null);
List<Map<String, Object>> items = (List<Map<String, Object>>) (list.get("data") != null ? list.get("data") : list.get("items"));
if (items != null) {
for (Map<String, Object> item : items) {
if (workflowName.equals(item.get("name"))) {
String existingId = (String) item.get("id");
if (existingId != null) {
state.workflowId = existingId;
saveState(state);
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
String updateBody = toJson(Map.of("steps", workflow.get("steps")));
apiCall("POST", "/workflows/" + existingId, updateBody);
return;
}
}
}
}
} catch (Exception e) {
// lookup is best-effort
}
if (state.workflowId == null || state.workflowId.isEmpty()) {
String workflowJson = toJson(workflow);
Map<String, Object> created = apiCall("POST", "/workflows", workflowJson);
String wfId = (String) created.get("id");
if (wfId == null && created.get("workflow") instanceof Map) {
wfId = (String) ((Map<String, Object>) created.get("workflow")).get("id");
}
if (wfId == null) {
throw new RuntimeException("Could not read created workflow id from response");
}
state.workflowId = wfId;
saveState(state);
System.out.println("+ created workflow (" + wfId + ")");
}
}
// Deploy current draft as new version (best-effort)
try {
apiCall("POST", "/workflows/" + state.workflowId + "/versions", toJson(new HashMap<>()));
} catch (Exception e) {
// best-effort
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
}
}// This uses the Extend REST API directly because Extend has no official Go SDK yet.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID *string `json:"workflowId,omitempty"`
}
var (
apiKey string
stateDir string
stateFile string
state State
)
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
cwd, _ := os.Getwd()
stateDir = filepath.Join(cwd, ".extend")
stateFile = filepath.Join(stateDir, "bill-of-lading.json")
if data, err := os.ReadFile(stateFile); err == nil {
json.Unmarshal(data, &state)
}
}
func saveState() error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, _ := json.MarshalIndent(state, "", " ")
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
var reqBody io.Reader
if body != nil {
data, _ := json.Marshal(body)
reqBody = bytes.NewReader(data)
}
req, _ := http.NewRequest(method, API+pathName, reqBody)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if resp.StatusCode >= 400 {
respStr := string(respBody)
if len(respStr) > 300 {
respStr = respStr[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
}
return data, nil
}
func main() {
workflow := map[string]interface{}{
"name": "Bill of Lading Processing Pipeline",
"steps": []map[string]interface{}{
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": []map[string]interface{}{
{"step": "parse1"},
},
},
{
"name": "parse1",
"type": "PARSE",
"config": map[string]interface{}{
"parseConfig": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{
"enabled": false,
},
},
},
"chunkingStrategy": map[string]interface{}{
"type": "document",
},
},
},
"next": []map[string]interface{}{
{"step": "extraction2"},
},
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": map[string]interface{}{
"extractorConfig": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"bill_of_lading_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Shipper's Bill of Lading Number (e.g., INTL-BL-09284)",
},
"carrier_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name of the carrier (e.g., Continental Logistics Group)",
},
"carrier_scac_code": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Standard Carrier Alpha Code for the carrier",
},
"shipper_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Complete shipper address including street, city, state, and zip",
},
"consignee_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name of the consignee/receiver",
},
"destination_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Destination address including street, city, state, and zip",
},
"delivering_carrier": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name of the delivering carrier (e.g., Oakland)",
},
"trailer_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Trailer identification number",
},
"freight_items": map[string]interface{}{
"type": "array",
"description": "Array of freight line items with handling units, packages, description, weight, and class",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"description": map[string]interface{}{
"type": []string{"string", "null"},
},
"weight": map[string]interface{}{
"type": []string{"string", "null"},
},
"class": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
},
"cod_amount": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Collect on Delivery amount if applicable",
},
"bill_type": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Type of bill of lading (e.g., UNIFORM STRAIGHT BILL OF LADING)",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]interface{}{
"enabled": false,
},
"advancedMultimodalEnabled": false,
},
},
},
},
},
}
fmt.Printf("Deploying \"%s\"…\n", workflow["name"])
if state.WorkflowID != nil {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", *state.WorkflowID)
apiCall("POST", fmt.Sprintf("/workflows/%s", *state.WorkflowID), map[string]interface{}{"steps": workflow["steps"]})
} else {
listPath := fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflow["name"].(string)))
if list, err := apiCall("GET", listPath, nil); err == nil {
var items []map[string]interface{}
if data, ok := list["data"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
} else if data, ok := list["items"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
}
for _, item := range items {
if name, ok := item["name"].(string); ok && name == workflow["name"].(string) {
if id, ok := item["id"].(string); ok {
state.WorkflowID = &id
saveState()
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow["name"], id)
apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{"steps": workflow["steps"]})
break
}
}
}
}
if state.WorkflowID == nil {
created, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
var wfID string
if id, ok := created["id"].(string); ok {
wfID = id
} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := wf["id"].(string); ok {
wfID = id
}
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = &wfID
saveState()
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
apiCall("POST", fmt.Sprintf("/workflows/%s/versions", *state.WorkflowID), map[string]interface{}{})
fmt.Println("\nDone. Run documents through it with:")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, *state.WorkflowID)
fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}A Bill of Lading (BOL) is a critical shipping document that serves as a contract between the shipper and carrier, documenting the receipt of goods for transport. This template captures carrier information, origin/destination details, consignee data, and itemized freight descriptions with weights and classifications.