Financial & BankingParse → Extract

Driver's License Extractor

Extracts personal identification and licensing data from driver license documents.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document781343825-Premium-Texas-Driver-License-Template.pdf

Step-by-step

A driver's license is an official government-issued document that verifies an individual's identity and authorization to operate motor vehicles, containing personal details, physical characteristics, license classification, and validity dates. This template takes in Driver's License and outputs markdown (.md) capturing the document's full text and layout, and JSON (.json) with structured identity and license fields including name, date of birth, address, license number, class, and expiration date per the extraction schema by using Extend's Parse, Extract primitives.

Input
Driver's License
Compatible document types (full list)
.pdf.docx.xlsx.png.jpg.jpeg.tiff.tif.svg.heic.heif.bmp.gif.webp.psd.xls.xltm.xltx.ods.doc.wpd.dotx.odt.pptx.ppt.ppm.csv.txt.html.xml.rtf.lis.md.eml.pcx
Step 1

Parse

Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.

InputSource document — PDF, image, spreadsheet, presentation, or scan
Config
blockOptions.text.agentic.enabledtruechanged
chunkingStrategy.type"document"
engine"parse_performance"
OutputMarkdown chunked by page or section, plus typed blocks (text, table, figure) with bounding boxes

You can learn more about Parse configuration in Extend's Parse documentation.

Step 2

Extract

Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.

InputOutput of the Parse step
Config
schemacustom schema — 12 fieldschanged
advancedOptions.advancedMultimodalEnabledtruechanged
advancedOptions.reviewAgent.enabledtruechanged
baseProcessor"extraction_performance"
OutputJSON shaped to the extraction schema, with per-field confidence scores and citations grounding each value to its source location

You can learn more about Extract configuration in Extend's Extract documentation.

Example code

{
  "name": "Driver License Template Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "State of issuance"
              },
              "gender": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Gender (M/F)"
              },
              "height": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Height in feet and inches format"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full street address including city, state, and zip code"
              },
              "eye_color": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Eye color abbreviation"
              },
              "last_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver's last name"
              },
              "first_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver's first name"
              },
              "issue_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "License issue date in MM/DD/YYYY format"
              },
              "date_of_birth": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Date of birth in MM/DD/YYYY format"
              },
              "license_class": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver license class/type"
              },
              "license_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver license number (DLN)"
              },
              "expiration_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "License expiration date in MM/DD/YYYY format"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Driver License Template Processing — Extend AI Skill

## What this pipeline does

Extracts structured identity and license information from US driver's licenses (digital scans or photographs). The pipeline parses the image into readable text via OCR, then extracts 12 key fields (name, license number, DOB, expiration, address, etc.) into a validated JSON object ready for identity verification, KYC workflows, or document management systems.

## When to use this

- **Identity verification workflows** — validate driver license authenticity and extract identity for KYC/AML compliance
- **Rental or employment onboarding** — pull name, DOB, address, and license class for background checks
- **Document management systems** — organize and index licenses by state, expiration date, or license class
- **Fraud detection pipelines** — extract license number and DOB to cross-reference against known fraudulent IDs
- **Batch processing** — extract structured data from hundreds of license scans for bulk uploads to HR or rental systems

## Processor pipeline

### Step 1: Parse (agentic_ocr mode)
**Processor:** `parseRuns.createAndPoll()`  
**Purpose:** Convert driver license image (photograph or scan) into structured markdown and OCR coordinates.  
**Key config choices:**
- `mode: "agentic_ocr"` — required for licenses because they often have:
  - Colored backgrounds, holograms, and security features that confuse light OCR
  - Handwritten notes or signatures
  - Variable layouts across US states (front and back, vertical/horizontal orientation)
- `outputType: "markdown"` — produces clean text blocks that extraction schema can reference

**Why this config:** Driver licenses are security documents with anti-tampering elements. Agentic OCR handles reflections, rotations, and state-specific layouts that light OCR would fail on. The markdown output preserves spatial relationships (e.g., "address field below name").

### Step 2: Extract (extraction_performance)
**Processor:** `extractRuns.createAndPoll()`  
**Purpose:** Pull 12 identity fields into structured JSON, with null values for missing/unreadable fields.  
**Key config choices:**
- `baseProcessor: "extraction_performance"` — prioritizes accuracy over latency; licenses are identity documents where precision matters more than speed
- Zod schema with nullable string and custom date/currency helpers where applicable
- Each field has a highly specific `describe()` annotation (e.g., "Driver license number (DLN)" not just "License number") to minimize hallucination

**Why this config:** Licenses contain legal names, expiration dates, and addresses that must be 100% accurate. The extraction_performance processor uses multi-pass refinement and structured validation to catch edge cases like "DL #" vs "DLN" label variations across states.

---

## TypeScript implementation



---

## CLI equivalent

```bash
# Step 1: Parse the license with agentic OCR
extend parse driver_license.jpg

# Step 2: Extract structured fields using schema
extend extract driver_license.jpg --schema license_schema.json
```

**license_schema.json** (save this file):
```json
{
  "type": "object",
  "properties": {
    "first_name": {
      "type": ["string", "null"],
      "description": "Driver's first name"
    },
    "last_name": {
      "type": ["string", "null"],
      "description": "Driver's last name"
    },
    "license_number": {
      "type": ["string", "null"],
      "description": "Driver license number (DLN)"
    },
    "date_of_birth": {
      "type": ["string", "null"],
      "description": "Date of birth in MM/DD/YYYY format"
    },
    "gender": {
      "type": ["string", "null"],
      "description": "Gender (M/F)"
    },
    "height": {
      "type": ["string", "null"],
      "description": "Height in feet and inches format"
    },
    "eye_color": {
      "type": ["string", "null"],
      "description": "Eye color abbreviation"
    },
    "address": {
      "type": ["string", "null"],
      "description": "Full street address including city, state, and zip code"
    },
    "license_class": {
      "type": ["string", "null"],
      "description": "Driver license class/type"
    },
    "issue_date": {
      "type": ["string", "null"],
      "description": "License issue date in MM/DD/YYYY format"
    },
    "expiration_date": {
      "type": ["string", "null"],
      "description": "License expiration date in MM/DD/YYYY format"
    },
    "state": {
      "type": ["string", "null"],
      "description": "State of issuance"
    }
  }
}
```

---

## Schema

The extraction schema targets 12 core fields present on all US driver's licenses:

```json
{
  "type": "object",
  "properties": {
    "first_name": {
      "type": ["string", "null"],
      "description": "Driver's first name as printed on the license"
    },
    "last_name": {
      "type": ["string", "null"],
      "description": "Driver's last name as printed on the license"
    },
    "license_number": {
      "type": ["string", "null"],
      "description": "Unique driver license number (DLN), often labeled as 'DL #' or 'DLN' on the card"
    },
    "date_of_birth": {
      "type": ["string", "null"],
      "description": "Date of birth in MM/DD/YYYY format, found near DOB label or code"
    },
    "gender": {
      "type": ["string", "null"],
      "description": "Gender marker (M for male, F for female) as printed on the license"
    },
    "height": {
      "type": ["string", "null"],
      "description": "Height in feet and inches format, e.g., 5'10'' or 5'10\", often labeled as HGT"
    },
    "eye_color": {
      "type": ["string", "null"],
      "description": "Eye color as three-letter code (BRN=brown, BLU=blue, GRN=green, HAZ=hazel, etc.), labeled EYES"
    },
    "address": {
      "type": ["string", "null"],
      "description": "Full street address including city, state, and zip code on one line, e.g., '123 Main St, Springfield, IL 62701'"
    },
    "license_class": {
      "type": ["string", "null"],
      "description": "Driver license class or type (D=standard, CDL=commercial, M=motorcycle), often a single letter"
    },
    "issue_date": {
      "type": ["string", "null"],
      "description": "Date the license was issued in MM/DD/YYYY format, labeled ISS or similar"
    },
    "expiration_date": {
      "type": ["string", "null"],
      "description": "License expiration date in MM/DD/YYYY format, labeled EXP or similar, critical for validity checks"
    },
    "state": {
      "type": ["string", "null"],
      "description": "State of issuance as two-letter abbreviation (CA, TX, NY, etc.), usually printed prominently at top or bottom"
    }
  }
}
```

**Field naming & accuracy notes:**

- **license_number**: Varies by state format (alphanumeric, all digits, or mixed). Extract exactly as printed, including hyphens or spaces. Critical for uniqueness validation.
- **date_of_birth** & **expiration_date**: Always request MM/DD/YYYY format in describe(). Extraction model may return various formats; post-processing to validate and normalize is recommended.
- **eye_color**: Use standard DMV three-letter codes. If the license shows full word (e.g., "BLUE"), extract it as-is and normalize downstream (BLU).
- **height**: Many licenses use apostrophe notation (5'10'') or quote marks (5'10"). Preserve the format as printed; normalize during validation.
- **address**: Pull the entire address line as a single field. Some licenses span multiple lines; join with ", " and remove line breaks.
- **gender**: Typically "M" or "F" but may vary (some older licenses use full words). Extract as-is.
- **state**: Always a two-letter abbreviation. Non-negotiable for compliance workflows.

---

## Accuracy tips

1. **Use agentic_ocr mode — non-negotiable.** Driver licenses have colored backgrounds, security holograms, and variable layouts across 50+ states. Light OCR fails ~15–20% of the time on these. Agentic OCR handles reflections, rotations, and anti-tampering elements.

2. **Validate dates post-extraction.** The model extracts date strings but may return MM/DD/YY instead of MM/DD/YYYY, or US-formatted vs ISO. Always parse and re-validate: `new Date("MM/DD/YYYY")` and reject invalid dates.

3. **Normalize eye color to DMV codes.** If extraction returns "Blue" instead of "BLU", normalize with a lookup table. Licenses are inconsistent in how eye color is encoded.

4. **Cross-check state against license format.** California DL numbers are 5–8 digits; Texas uses alphanumeric. After extraction, validate that `license_number` format matches `state`. Flag mismatches as potential fraud.

5. **Handle multi-line addresses carefully.** Some licenses print address across 2–3 lines with labels (ADDR, CITY, ZIP). Instruct extraction to join all lines into a single comma-separated string; post-process to remove "ADDR" or other labels.

6. **Watch for OCR substitutions in names.** The letters "O" and "0" (zero), "I" and "1" (one), and "l" (lowercase L) are frequently confused. For production KYC, manually review names flagged as unusual or non-English.

7. **Set realistic null expectations.** Some licenses omit height or eye color fields. Do not treat nulls as errors; they are valid outcomes. Flag only if *all* core fields (first_name, last_name, license_number, state) are null.

8. **Test with front and back separately if bundled.** If you receive a PDF with both front and back of the license, consider splitting before extraction. The front has the portrait and core identity fields; the back has endorsements and restrictions. For standard KYC, extract the front only.

9. **Validate expiration against current date for workflow routing.** Implement post-extraction logic:
   ```
   if (expiration_date < today) { route to "expired_license" queue }
   ```

10. **Use extraction_performance, not extraction_light.** Identity documents demand 98%+ accuracy. The performance processor is worth the 2–3 second latency increase.

---

## Trade-offs & alternatives

### Sync vs. Async
- **Sync parse + async extract** (shown in this skill): Fastest for single licenses (~8–12 seconds total). Good for real-time onboarding flows.
- **Fully async workflow**: Use `createAndPoll()` with async/await patterns for batch processing of 100+ licenses. No difference in accuracy; just latency distribution.

### Accuracy vs. Speed
| Scenario | Recommendation |
|---|---|
| Real-time KYC (< 5
import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

/**
 * Extracts structured identity and license information from a US driver's license image.
 * Accepts a local file path, reads it as base64, and returns parsed/extracted fields.
 */
export async function processDriverLicenseTemplate(filePath: string) {
  // Read local file and convert to base64 data URL
  const fileBuffer = fs.readFileSync(filePath);
  const base64 = fileBuffer.toString("base64");
  const mimeType = "image/jpeg"; // adjust if PNG or PDF
  const dataUrl = `data:${mimeType};base64,${base64}`;

  console.log(`Processing driver license: ${path.basename(filePath)}`);

  // Step 1: Parse the license image with agentic OCR
  // This handles colored backgrounds, security features, and variable state layouts
  console.log("Step 1: Parsing license image...");
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
  });

  if (parseRun.status !== "PROCESSED") {
    throw new Error(`Parse failed with status: ${parseRun.status}`);
  }

  console.log(`Parsed ${parseRun.output.chunks.length} text chunks`);
  for (const chunk of parseRun.output.chunks) {
    console.log(`  [Chunk] ${chunk.content.substring(0, 60)}...`);
  }

  // Step 2: Extract structured fields into JSON with Zod validation
  // Uses extraction_performance for accuracy (priority over speed for identity docs)
  console.log("\nStep 2: Extracting structured fields...");
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: z.object({
        first_name: z.string().nullable().describe("Driver's first name"),
        last_name: z.string().nullable().describe("Driver's last name"),
        license_number: z
          .string()
          .nullable()
          .describe("Driver license number (DLN)"),
        date_of_birth: z
          .string()
          .nullable()
          .describe("Date of birth in MM/DD/YYYY format"),
        gender: z
          .string()
          .nullable()
          .describe("Gender (M/F or M/Female, etc.)"),
        height: z
          .string()
          .nullable()
          .describe("Height in feet and inches format, e.g., 5'10''"),
        eye_color: z
          .string()
          .nullable()
          .describe("Eye color abbreviation (e.g., BLU, BRN, GRN, HAZ)"),
        address: z
          .string()
          .nullable()
          .describe(
            "Full street address including city, state, and zip code on one line"
          ),
        license_class: z
          .string()
          .nullable()
          .describe(
            "Driver license class/type (e.g., D, CDL, M for motorcycle)"
          ),
        issue_date: z
          .string()
          .nullable()
          .describe("License issue date in MM/DD/YYYY format"),
        expiration_date: z
          .string()
          .nullable()
          .describe("License expiration date in MM/DD/YYYY format"),
        state: z
          .string()
          .nullable()
          .describe("State of issuance (two-letter abbreviation, e.g., CA, TX)"),
      }),
    },
  });

  if (extractRun.status !== "PROCESSED") {
    throw new Error(`Extract failed with status: ${extractRun.status}`);
  }

  const licenseData = extractRun.output.value;

  // Step 3: Validate and display extracted data
  console.log("\nExtracted License Data:");
  console.log(JSON.stringify(licenseData, null, 2));

  // Example: Check for expiration
  if (licenseData.expiration_date) {
    const [month, day, year] = licenseData.expiration_date.split("/");
    const expDate = new Date(`${year}-${month}-${day}`);
    const now = new Date();
    if (expDate < now) {
      console.log("⚠️  WARNING: License is expired");
    } else {
      console.log(`✓ License valid until ${licenseData.expiration_date}`);
    }
  }

  return licenseData;
}

// Main entry point for testing
const filePath = process.argv[2] || "__FILE_PATH__";
processDriverLicenseTemplate(filePath)
  .then(() => console.log("\n✓ Processing complete"))
  .catch((err) => {
    console.error("Error:", err.message);
    process.exit(1);
  });
import os
import base64
from pathlib import Path
from datetime import datetime
from extend_ai import Extend

client = Extend(token=os.environ["EXTEND_API_KEY"])


def process_driver_license_template(file_path: str) -> dict:
    """
    Extracts structured identity and license information from a US driver's license image.
    Accepts a local file path, reads it as base64, and returns parsed/extracted fields.
    """
    # Read local file and convert to base64 data URL
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    
    base64_data = base64.b64encode(file_buffer).decode("utf-8")
    mime_type = "image/jpeg"  # adjust if PNG or PDF
    data_url = f"data:{mime_type};base64,{base64_data}"

    file_name = Path(file_path).name
    print(f"Processing driver license: {file_name}")

    # Step 1: Parse the license image with agentic OCR
    # This handles colored backgrounds, security features, and variable state layouts
    print("Step 1: Parsing license image...")
    parse_run = client.parse_runs.create_and_poll(
        file={"url": data_url}
    )

    if parse_run.status != "PROCESSED":
        raise Exception(f"Parse failed with status: {parse_run.status}")

    print(f"Parsed {len(parse_run.output['chunks'])} text chunks")
    for chunk in parse_run.output["chunks"]:
        content_preview = chunk["content"][:60]
        print(f"  [Chunk] {content_preview}...")

    # Step 2: Extract structured fields into JSON
    # Uses extraction_performance for accuracy (priority over speed for identity docs)
    print("\nStep 2: Extracting structured fields...")
    
    schema = {
        "type": "object",
        "properties": {
            "first_name": {
                "type": ["string", "null"],
                "description": "Driver's first name",
            },
            "last_name": {
                "type": ["string", "null"],
                "description": "Driver's last name",
            },
            "license_number": {
                "type": ["string", "null"],
                "description": "Driver license number (DLN)",
            },
            "date_of_birth": {
                "type": ["string", "null"],
                "description": "Date of birth in MM/DD/YYYY format",
            },
            "gender": {
                "type": ["string", "null"],
                "description": "Gender (M/F or M/Female, etc.)",
            },
            "height": {
                "type": ["string", "null"],
                "description": "Height in feet and inches format, e.g., 5'10''",
            },
            "eye_color": {
                "type": ["string", "null"],
                "description": "Eye color abbreviation (e.g., BLU, BRN, GRN, HAZ)",
            },
            "address": {
                "type": ["string", "null"],
                "description": "Full street address including city, state, and zip code on one line",
            },
            "license_class": {
                "type": ["string", "null"],
                "description": "Driver license class/type (e.g., D, CDL, M for motorcycle)",
            },
            "issue_date": {
                "type": ["string", "null"],
                "description": "License issue date in MM/DD/YYYY format",
            },
            "expiration_date": {
                "type": ["string", "null"],
                "description": "License expiration date in MM/DD/YYYY format",
            },
            "state": {
                "type": ["string", "null"],
                "description": "State of issuance (two-letter abbreviation, e.g., CA, TX)",
            },
        },
    }

    extract_run = client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={"schema": schema},
    )

    if extract_run.status != "PROCESSED":
        raise Exception(f"Extract failed with status: {extract_run.status}")

    license_data = extract_run.output["value"]

    # Step 3: Validate and display extracted data
    print("\nExtracted License Data:")
    import json
    print(json.dumps(license_data, indent=2))

    # Example: Check for expiration
    if license_data.get("expiration_date"):
        exp_date_str = license_data["expiration_date"]
        exp_date = datetime.strptime(exp_date_str, "%m/%d/%Y")
        now = datetime.now()
        if exp_date < now:
            print("⚠️  WARNING: License is expired")
        else:
            print(f"✓ License valid until {exp_date_str}")

    return license_data


# Main entry point for testing
if __name__ == "__main__":
    import sys
    
    file_path = sys.argv[1] if len(sys.argv) > 1 else "__FILE_PATH__"
    
    try:
        process_driver_license_template(file_path)
        print("\n✓ Processing complete")
    except Exception as err:
        print(f"Error: {err}")
        sys.exit(1)
// NOTE: This uses the Extend REST API directly because Extend has no official Java SDK yet.
// All operations mirror the TypeScript reference's SDK calls over HTTPS to https://api.extend.ai

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.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Map;

public class DriverLicenseProcessor {
    private static final String API_BASE = "https://api.extend.ai";
    private static final String API_KEY = System.getenv("EXTEND_API_KEY");
    private static final HttpClient httpClient = HttpClient.newHttpClient();
    private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");

    public static void main(String[] args) throws IOException, InterruptedException {
        String filePath = args.length > 0 ? args[0] : "__FILE_PATH__";
        try {
            processDriverLicenseTemplate(filePath);
            System.out.println("\n✓ Processing complete");
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            System.exit(1);
        }
    }

    public static Map<String, Object> processDriverLicenseTemplate(String filePath)
            throws IOException, InterruptedException {
        // Read local file and convert to base64 data URL
        byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
        String base64 = Base64.getEncoder().encodeToString(fileBuffer);
        String mimeType = "image/jpeg";
        String dataUrl = "data:" + mimeType + ";base64," + base64;

        System.out.println("Processing driver license: " + Paths.get(filePath).getFileName());

        // Step 1: Parse the license image with agentic OCR
        System.out.println("Step 1: Parsing license image...");
        Map<String, Object> parseResult = performParseRun(dataUrl);
        String parseStatus = (String) parseResult.get("status");

        if (!"PROCESSED".equals(parseStatus)) {
            throw new RuntimeException("Parse failed with status: " + parseStatus);
        }

        java.util.List<Map<String, Object>> chunks = 
            (java.util.List<Map<String, Object>>) ((Map<String, Object>) parseResult.get("output")).get("chunks");
        System.out.println("Parsed " + chunks.size() + " text chunks");
        for (Map<String, Object> chunk : chunks) {
            String content = (String) ((Map<String, Object>) chunk.get("content")).get("text");
            if (content == null) {
                content = chunk.get("content").toString();
            }
            int len = Math.min(60, content.length());
            System.out.println("  [Chunk] " + content.substring(0, len) + "...");
        }

        // Step 2: Extract structured fields into JSON
        System.out.println("\nStep 2: Extracting structured fields...");
        String schema = buildExtractionSchema();
        Map<String, Object> extractResult = performExtractRun(dataUrl, schema);
        String extractStatus = (String) extractResult.get("status");

        if (!"PROCESSED".equals(extractStatus)) {
            throw new RuntimeException("Extract failed with status: " + extractStatus);
        }

        Map<String, Object> licenseData = 
            (Map<String, Object>) ((Map<String, Object>) extractResult.get("output")).get("value");

        // Step 3: Validate and display extracted data
        System.out.println("\nExtracted License Data:");
        System.out.println(toJsonString(licenseData));

        // Example: Check for expiration
        String expirationDate = (String) licenseData.get("expiration_date");
        if (expirationDate != null && !expirationDate.isEmpty()) {
            try {
                LocalDate expDate = LocalDate.parse(expirationDate, dateFormatter);
                LocalDate now = LocalDate.now();
                if (expDate.isBefore(now)) {
                    System.out.println("⚠️  WARNING: License is expired");
                } else {
                    System.out.println("✓ License valid until " + expirationDate);
                }
            } catch (Exception e) {
                // Date parsing failed; continue
            }
        }

        return licenseData;
    }

    private static Map<String, Object> performParseRun(String dataUrl)
            throws IOException, InterruptedException {
        String body = "{\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"}}";
        return makeRequest("POST", "/api/v1/parse-runs", body, true);
    }

    private static Map<String, Object> performExtractRun(String dataUrl, String schema)
            throws IOException, InterruptedException {
        String configJson = "{\"schema\":" + schema + "}";
        String body = "{\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},\"config\":" + configJson + "}";
        Map<String, Object> initialResult = makeRequest("POST", "/api/v1/extract-runs", body, false);
        String runId = (String) initialResult.get("id");
        return pollExtractRun(runId);
    }

    private static Map<String, Object> pollExtractRun(String runId)
            throws IOException, InterruptedException {
        while (true) {
            Map<String, Object> result = makeRequest("GET", "/api/v1/extract-runs/" + runId, "", false);
            String status = (String) result.get("status");
            if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
                return result;
            }
            Thread.sleep(1000);
        }
    }

    private static Map<String, Object> makeRequest(String method, String path, String body, boolean poll)
            throws IOException, InterruptedException {
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + path))
                .header("Authorization", "Bearer " + API_KEY)
                .header("Content-Type", "application/json");

        if ("POST".equals(method)) {
            builder.POST(HttpRequest.BodyPublishers.ofString(body));
        } else {
            builder.GET();
        }

        HttpRequest request = builder.build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200 && response.statusCode() != 201) {
            throw new RuntimeException("API error: " + response.statusCode() + " " + response.body());
        }

        Map<String, Object> result = parseJson(response.body());

        if (poll && "POST".equals(method)) {
            String runId = (String) result.get("id");
            return pollParseRun(runId);
        }

        return result;
    }

    private static Map<String, Object> pollParseRun(String runId)
            throws IOException, InterruptedException {
        while (true) {
            Map<String, Object> result = makeRequest("GET", "/api/v1/parse-runs/" + runId, "", false);
            String status = (String) result.get("status");
            if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
                return result;
            }
            Thread.sleep(1000);
        }
    }

    private static String buildExtractionSchema() {
        return "{"
                + "\"type\":\"object\","
                + "\"properties\":{"
                + "\"first_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Driver's first name\"},"
                + "\"last_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Driver's last name\"},"
                + "\"license_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Driver license number (DLN)\"},"
                + "\"date_of_birth\":{\"type\":[\"string\",\"null\"],\"description\":\"Date of birth in MM/DD/YYYY format\"},"
                + "\"gender\":{\"type\":[\"string\",\"null\"],\"description\":\"Gender (M/F or M/Female, etc.)\"},"
                + "\"height\":{\"type\":[\"string\",\"null\"],\"description\":\"Height in feet and inches format, e.g., 5'10''\"},"
                + "\"eye_color\":{\"type\":[\"string\",\"null\"],\"description\":\"Eye color abbreviation (e.g., BLU, BRN, GRN, HAZ)\"},"
                + "\"address\":{\"type\":[\"string\",\"null\"],\"description\":\"Full street address including city, state, and zip code on one line\"},"
                + "\"license_class\":{\"type\":[\"string\",\"null\"],\"description\":\"Driver license class/type (e.g., D, CDL, M for motorcycle)\"},"
                + "\"issue_date\":{\"type\":[\"string\",\"null\"],\"description\":\"License issue date in MM/DD/YYYY format\"},"
                + "\"expiration_date\":{\"type\":[\"string\",\"null\"],\"description\":\"License expiration date in MM/DD/YYYY format\"},"
                + "\"state\":{\"type\":[\"string\",\"null\"],\"description\":\"State of issuance (two-letter abbreviation, e.g., CA, TX)\"}"
                + "}"
                + "}";
    }

    private static Map<String, Object> parseJson(String json) {
        // Simple JSON parser for the response structure
        json = json.trim();
        if (json.startsWith("{") && json.endsWith("}")) {
            Map<String, Object> map = new java.util.LinkedHashMap<>();
            String content = json.substring(1, json.length() - 1);
            int depth = 0;
            StringBuilder key = new StringBuilder();
            StringBuilder value = new StringBuilder();
            boolean inValue = false;
            boolean inString = false;

            for (int i = 0; i < content.length(); i++) {
                char c = content.charAt(i);
                if (c == '"' && (i == 0 || content.charAt(i - 1) != '\\')) {
                    inString = !inString;
                    if (!inValue) key.append(c);
                    else value.append(c);
                } else if (c == ':' && !inString && !inValue) {
                    inValue = true;
                } else if ((c == ',' || i == content.length() - 1) && !inString && depth == 0) {
                    if (i == content.length() - 1 && c != ',') {
                        value.append(c);
                    }
                    String k = key.toString().trim().replaceAll("\"", "");
                    String v = value.toString().trim();
                    if (!k.isEmpty()) {
                        map.put(k, parseValue(v));
                    }
                    key = new StringBuilder();
                    value = new StringBuilder();
                    inValue = false;
                } else {
                    if (inValue) {
                        if ((c == '{' || c == '[') && !inString) depth++;
                        if ((c == '}' || c == ']') && !inString) depth--;
                        value.append(c);
                    } else {
                        key.append(c);
                    }
                }
            }
            return map;
        }
        return new java.util.LinkedHashMap<>();
    }

    private static Object parseValue(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);
        }
        if (value.startsWith("{")) return parseJson(value);
        if (value.startsWith("[")) {
            java.util.List<Object> list = new java.util.ArrayList<>();
            String content = value.substring(1, value.length() - 1).trim();
            if (!content.isEmpty()) {
                int depth = 0;
                StringBuilder item = new StringBuilder();
                boolean inString = false;
                for (char c : content.toCharArray()) {
                    if (c == '"' && item.length() == 0) inString = !inString;
                    else if ((c == ',' || c == '}' || c == ']') && !inString && depth == 0) {
                        if (c != ',' && c != '}' && c != ']') item.append(c);
                        String trimmed = item.toString().trim();
                        if (!trimmed.isEmpty()) list.add(parseValue(trimmed));
                        item = new StringBuilder();
                    } else {
                        if ((c == '{' || c == '[') && !inString) depth++;
                        if ((c == '}' || c == ']') && !inString) depth--;
                        item.append(c);
                    }
                }
                if (item.length() > 0) {
                    String trimmed = item.toString().trim();
                    if (!trimmed.isEmpty()) list.add(parseValue(trimmed));
                }
            }
            return list;
        }
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            try {
                return Double.parseDouble(value);
            } catch (NumberFormatException e2) {
                return value;
            }
        }
    }

    private static String toJsonString(Map<String, Object> map) {
        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (!first) sb.append(",");
            sb.append("\"").append(entry.getKey()).append("\":");
            sb.append(toJsonValue(entry.getValue()));
            first = false;
        }
        sb.append("}");
        return sb.toString();
    }

    private static String toJsonValue(Object obj) {
        if (obj == null) return "null";
        if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
        if (obj instanceof Boolean || obj instanceof Number) return obj.toString();
        if (obj instanceof Map) return toJsonString((Map<String, Object>) obj);
        if (obj instanceof java.util.List) {
            java.util.List<?> list = (java.util.List<?>) obj;
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < list.size(); i++) {
                if (i > 0) sb.append(",");
                sb.append(toJsonValue(list.get(i)));
            }
            sb.append("]");
            return sb.toString();
        }
        return "\"" + escapeJson(obj.toString()) + "\"";
    }

    private static String escapeJson(String str) {
        return str.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }
}
// This code calls the Extend REST API directly because Extend has no official Go SDK yet.
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const apiBaseURL = "https://api.extend.ai"

// LicenseData represents the extracted driver license information
type LicenseData struct {
	FirstName      *string `json:"first_name"`
	LastName       *string `json:"last_name"`
	LicenseNumber  *string `json:"license_number"`
	DateOfBirth    *string `json:"date_of_birth"`
	Gender         *string `json:"gender"`
	Height         *string `json:"height"`
	EyeColor       *string `json:"eye_color"`
	Address        *string `json:"address"`
	LicenseClass   *string `json:"license_class"`
	IssueDate      *string `json:"issue_date"`
	ExpirationDate *string `json:"expiration_date"`
	State          *string `json:"state"`
}

// ParseRunResponse represents the response from the parse endpoint
type ParseRunResponse struct {
	Status string `json:"status"`
	Output struct {
		Chunks []struct {
			Content string `json:"content"`
		} `json:"chunks"`
	} `json:"output"`
}

// ExtractRunResponse represents the response from the extract endpoint
type ExtractRunResponse struct {
	Status string      `json:"status"`
	Output struct {
		Value LicenseData `json:"value"`
	} `json:"output"`
}

// PollRequest represents the request body for polling endpoints
type PollRequest struct {
	File struct {
		URL string `json:"url"`
	} `json:"file"`
}

// ExtractRequest represents the request body for extract with schema
type ExtractRequest struct {
	File struct {
		URL string `json:"url"`
	} `json:"file"`
	Config struct {
		Schema map[string]interface{} `json:"schema"`
	} `json:"config"`
}

func makeRequest(method, endpoint string, body interface{}, apiKey string) ([]byte, error) {
	var bodyReader io.Reader
	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal body: %w", err)
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

	req, err := http.NewRequest(method, apiBaseURL+endpoint, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
	}

	return respBody, nil
}

func pollParseRun(filePath, dataURL, apiKey string) (*ParseRunResponse, error) {
	fmt.Printf("Step 1: Parsing license image...\n")

	payload := PollRequest{}
	payload.File.URL = dataURL

	respBody, err := makeRequest("POST", "/parse_runs", payload, apiKey)
	if err != nil {
		return nil, err
	}

	var result ParseRunResponse
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal parse response: %w", err)
	}

	if result.Status != "PROCESSED" {
		return nil, fmt.Errorf("parse failed with status: %s", result.Status)
	}

	fmt.Printf("Parsed %d text chunks\n", len(result.Output.Chunks))
	for _, chunk := range result.Output.Chunks {
		content := chunk.Content
		if len(content) > 60 {
			content = content[:60]
		}
		fmt.Printf("  [Chunk] %s...\n", content)
	}

	return &result, nil
}

func pollExtractRun(dataURL, apiKey string) (*ExtractRunResponse, error) {
	fmt.Printf("\nStep 2: Extracting structured fields...\n")

	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"first_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Driver's first name",
			},
			"last_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Driver's last name",
			},
			"license_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Driver license number (DLN)",
			},
			"date_of_birth": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Date of birth in MM/DD/YYYY format",
			},
			"gender": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Gender (M/F or M/Female, etc.)",
			},
			"height": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Height in feet and inches format, e.g., 5'10''",
			},
			"eye_color": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Eye color abbreviation (e.g., BLU, BRN, GRN, HAZ)",
			},
			"address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Full street address including city, state, and zip code on one line",
			},
			"license_class": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Driver license class/type (e.g., D, CDL, M for motorcycle)",
			},
			"issue_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "License issue date in MM/DD/YYYY format",
			},
			"expiration_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "License expiration date in MM/DD/YYYY format",
			},
			"state": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "State of issuance (two-letter abbreviation, e.g., CA, TX)",
			},
		},
	}

	payload := ExtractRequest{}
	payload.File.URL = dataURL
	payload.Config.Schema = schema

	respBody, err := makeRequest("POST", "/extract_runs", payload, apiKey)
	if err != nil {
		return nil, err
	}

	var result ExtractRunResponse
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal extract response: %w", err)
	}

	if result.Status != "PROCESSED" {
		return nil, fmt.Errorf("extract failed with status: %s", result.Status)
	}

	return &result, nil
}

func processDriverLicenseTemplate(filePath string) (*LicenseData, error) {
	// Read local file and convert to base64 data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
	mimeType := "image/jpeg" // adjust if PNG or PDF
	dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)

	fmt.Printf("Processing driver license: %s\n", filepath.Base(filePath))

	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
	}

	// Step 1: Parse the license image
	_, err = pollParseRun(filePath, dataURL, apiKey)
	if err != nil {
		return nil, err
	}

	// Step 2: Extract structured fields
	extractResult, err := pollExtractRun(dataURL, apiKey)
	if err != nil {
		return nil, err
	}

	licenseData := &extractResult.Output.Value

	// Step 3: Validate and display extracted data
	fmt.Printf("\nExtracted License Data:\n")
	jsonBytes, _ := json.MarshalIndent(licenseData, "", "  ")
	fmt.Println(string(jsonBytes))

	// Example: Check for expiration
	if licenseData.ExpirationDate != nil && *licenseData.ExpirationDate != "" {
		parts := strings.Split(*licenseData.ExpirationDate, "/")
		if len(parts) == 3 {
			expDate, err := time.Parse("01/02/2006", *licenseData.ExpirationDate)
			if err == nil {
				now := time.Now()
				if expDate.Before(now) {
					fmt.Println("⚠️  WARNING: License is expired")
				} else {
					fmt.Printf("✓ License valid until %s\n", *licenseData.ExpirationDate)
				}
			}
		}
	}

	return licenseData, nil
}

func main() {
	filePath := "__FILE_PATH__"
	if len(os.Args) > 1 {
		filePath = os.Args[1]
	}

	_, err := processDriverLicenseTemplate(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("\n✓ Processing complete")
}
// Deploy the "Driver License Template" 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/driver-license-template.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: driver-license-template).

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, "driver-license-template.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": "Driver License Template Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "State of issuance"
              },
              "gender": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Gender (M/F)"
              },
              "height": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Height in feet and inches format"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full street address including city, state, and zip code"
              },
              "eye_color": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Eye color abbreviation"
              },
              "last_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver's last name"
              },
              "first_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver's first name"
              },
              "issue_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "License issue date in MM/DD/YYYY format"
              },
              "date_of_birth": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Date of birth in MM/DD/YYYY format"
              },
              "license_class": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver license class/type"
              },
              "license_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Driver license number (DLN)"
              },
              "expiration_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "License expiration date in MM/DD/YYYY format"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

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 "Driver License Template" 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/driver-license-template.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: driver-license-template).
"""

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("Error: Set EXTEND_API_KEY first.", file=sys.stderr)
    sys.exit(1)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "driver-license-template.json"


def load_state() -> dict[str, Any]:
    """Load state from file if it exists."""
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text())
    return {}


def save_state(state: dict[str, Any]) -> None:
    """Save state to file."""
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


WORKFLOW = {
    "name": "Driver License Template Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {
                                "enabled": True
                            }
                        }
                    },
                    "chunkingStrategy": {
                        "type": "document"
                    },
                }
            },
            "next": [{"step": "extraction2"}],
        },
        {
            "name": "extraction2",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": ["string", "null"],
                                "description": "State of issuance",
                            },
                            "gender": {
                                "type": ["string", "null"],
                                "description": "Gender (M/F)",
                            },
                            "height": {
                                "type": ["string", "null"],
                                "description": "Height in feet and inches format",
                            },
                            "address": {
                                "type": ["string", "null"],
                                "description": "Full street address including city, state, and zip code",
                            },
                            "eye_color": {
                                "type": ["string", "null"],
                                "description": "Eye color abbreviation",
                            },
                            "last_name": {
                                "type": ["string", "null"],
                                "description": "Driver's last name",
                            },
                            "first_name": {
                                "type": ["string", "null"],
                                "description": "Driver's first name",
                            },
                            "issue_date": {
                                "type": ["string", "null"],
                                "description": "License issue date in MM/DD/YYYY format",
                            },
                            "date_of_birth": {
                                "type": ["string", "null"],
                                "description": "Date of birth in MM/DD/YYYY format",
                            },
                            "license_class": {
                                "type": ["string", "null"],
                                "description": "Driver license class/type",
                            },
                            "license_number": {
                                "type": ["string", "null"],
                                "description": "Driver license number (DLN)",
                            },
                            "expiration_date": {
                                "type": ["string", "null"],
                                "description": "License expiration date in MM/DD/YYYY format",
                            },
                        },
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {
                            "enabled": True
                        },
                        "advancedMultimodalEnabled": True,
                    },
                }
            },
        },
    ],
}


async def main() -> None:
    """Provision or update the driver license workflow."""
    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 workflows.items if hasattr(workflows, "items") else []
            for item in items:
                if getattr(item, "name", None) == WORKFLOW["name"]:
                    existing_id = getattr(item, "id", None)
                    break
        except Exception:
            pass

        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)
            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 —
    # best-effort: some accounts/plans may not require this explicit step.
    try:
        await client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass

    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(f"Error: {e}", file=sys.stderr)
        sys.exit(1)
// This uses Extend's REST API directly (base URL https://api.extend.ai) with Java's built-in
// java.net.http.HttpClient because Extend has no official Java SDK yet.
//
// Deploy the "Driver License Template" 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/driver-license-template.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)
//   javac Provision.java && java Provision
//
// Generated by doc1 (template: driver-license-template).

import java.io.*;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;

public class Provision {
  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("driver-license-template.json");
  private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  static class State {
    String workflowId;
  }

  static State state = new State();

  public static void main(String[] args) {
    try {
      if (API_KEY == null || API_KEY.isEmpty()) {
        System.err.println("Set EXTEND_API_KEY first.");
        System.exit(1);
      }

      loadState();

      Map<String, Object> workflow = buildWorkflow();
      System.out.println("Deploying \"" + (String) workflow.get("name") + "\"…");

      if (state.workflowId != null && !state.workflowId.isEmpty()) {
        System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
        Map<String, Object> updateBody = new HashMap<>();
        updateBody.put("steps", workflow.get("steps"));
        api("POST", "/workflows/" + state.workflowId, updateBody);
      } else {
        try {
          String encodedName = URLEncoder.encode((String) workflow.get("name"), StandardCharsets.UTF_8);
          Map<String, Object> list = api("GET", "/workflows?name=" + encodedName, null);
          List<?> items = (List<?>) list.getOrDefault("data", list.getOrDefault("items", new ArrayList<>()));

          String existingId = null;
          String workflowName = (String) workflow.get("name");
          for (Object item : items) {
            if (item instanceof Map) {
              Map<String, Object> itemMap = (Map<String, Object>) item;
              if (workflowName.equals(itemMap.get("name"))) {
                existingId = (String) itemMap.get("id");
                break;
              }
            }
          }

          if (existingId != null) {
            state.workflowId = existingId;
            saveState();
            System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
            Map<String, Object> updateBody = new HashMap<>();
            updateBody.put("steps", workflow.get("steps"));
            api("POST", "/workflows/" + existingId, updateBody);
          }
        } catch (Exception e) {
          // lookup is best-effort; fall through to create
        }

        if (state.workflowId == null || state.workflowId.isEmpty()) {
          Map<String, Object> created = api("POST", "/workflows", workflow);
          String wfId = (String) created.get("id");
          if (wfId == null) {
            Map<String, Object> wfMap = (Map<String, Object>) created.get("workflow");
            if (wfMap != null) {
              wfId = (String) wfMap.get("id");
            }
          }
          if (wfId == null) {
            throw new Exception("Could not read created workflow id from response");
          }
          state.workflowId = wfId;
          saveState();
          System.out.println("+ 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.
      try {
        api("POST", "/workflows/" + state.workflowId + "/versions", 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.");
    } catch (Exception e) {
      System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
      System.exit(1);
    }
  }

  private static void loadState() throws IOException {
    if (Files.exists(STATE_FILE)) {
      String content = new String(Files.readAllBytes(STATE_FILE), StandardCharsets.UTF_8);
      Map<String, Object> loaded = parseJson(content);
      if (loaded.containsKey("workflowId")) {
        state.workflowId = (String) loaded.get("workflowId");
      }
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    Map<String, Object> toSave = new HashMap<>();
    if (state.workflowId != null) {
      toSave.put("workflowId", state.workflowId);
    }
    String json = toJson(toSave);
    Files.write(STATE_FILE, json.getBytes(StandardCharsets.UTF_8));
  }

  private static Map<String, Object> api(String method, String pathName, Object body)
      throws IOException, InterruptedException {
    HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .method(method, body != null
            ? HttpRequest.BodyPublishers.ofString(toJson(body))
            : HttpRequest.BodyPublishers.noBody())
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

    if (body != null) {
      requestBuilder.header("Content-Type", "application/json");
    }

    HttpResponse<String> response = HTTP_CLIENT.send(requestBuilder.build(),
        HttpResponse.BodyHandlers.ofString());

    Map<String, Object> data;
    try {
      data = parseJson(response.body());
    } catch (Exception e) {
      data = new HashMap<>();
    }

    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String errorMsg = toJson(data);
      if (errorMsg.length() > 300) {
        errorMsg = errorMsg.substring(0, 300);
      }
      throw new IOException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
    }

    return data;
  }

  private static Map<String, Object> buildWorkflow() {
    Map<String, Object> workflow = new HashMap<>();
    workflow.put("name", "Driver License Template Processing Pipeline");

    List<Map<String, Object>> steps = new ArrayList<>();

    // startTrigger1
    Map<String, Object> trigger = new HashMap<>();
    trigger.put("name", "startTrigger1");
    trigger.put("type", "TRIGGER");
    List<Map<String, String>> nextTrigger = new ArrayList<>();
    Map<String, String> nextStep1 = new HashMap<>();
    nextStep1.put("step", "parse1");
    nextTrigger.add(nextStep1);
    trigger.put("next", nextTrigger);
    steps.add(trigger);

    // parse1
    Map<String, Object> parse = new HashMap<>();
    parse.put("name", "parse1");
    parse.put("type", "PARSE");
    Map<String, Object> parseConfig = new HashMap<>();
    Map<String, Object> blockOptions = new HashMap<>();
    Map<String, Object> textOptions = new HashMap<>();
    Map<String, Object> agenticOptions = new HashMap<>();
    agenticOptions.put("enabled", true);
    textOptions.put("agentic", agenticOptions);
    blockOptions.put("text", textOptions);
    parseConfig.put("blockOptions", blockOptions);
    Map<String, Object> chunkingStrategy = new HashMap<>();
    chunkingStrategy.put("type", "document");
    parseConfig.put("chunkingStrategy", chunkingStrategy);
    Map<String, Object> parseConfigWrapper = new HashMap<>();
    parseConfigWrapper.put("parseConfig", parseConfig);
    parse.put("config", parseConfigWrapper);
    List<Map<String, String>> nextParse = new ArrayList<>();
    Map<String, String> nextStep2 = new HashMap<>();
    nextStep2.put("step", "extraction2");
    nextParse.add(nextStep2);
    parse.put("next", nextParse);
    steps.add(parse);

    // extraction2
    Map<String, Object> extraction = new HashMap<>();
    extraction.put("name", "extraction2");
    extraction.put("type", "EXTRACT");
    Map<String, Object> extractConfig = new HashMap<>();
    Map<String, Object> schema = buildSchema();
    Map<String, Object> extractorConfig = new HashMap<>();
    extractorConfig.put("schema", schema);
    extractorConfig.put("baseProcessor", "extraction_performance");
    Map<String, Object> advancedOptions = new HashMap<>();
    Map<String, Object> reviewAgent = new HashMap<>();
    reviewAgent.put("enabled", true);
    advancedOptions.put("reviewAgent", reviewAgent);
    advancedOptions.put("advancedMultimodalEnabled", true);
    extractorConfig.put("advancedOptions", advancedOptions);
    extractConfig.put("extractorConfig", extractorConfig);
    extraction.put("config", extractConfig);
    steps.add(extraction);

    workflow.put("steps", steps);
    return workflow;
  }

  private static Map<String, Object> buildSchema() {
    Map<String, Object> schema = new HashMap<>();
    schema.put("type", "object");

    Map<String, Object> properties = new HashMap<>();

    properties.put("state", createPropertyField("State of issuance"));
    properties.put("gender", createPropertyField("Gender (M/F)"));
    properties.put("height", createPropertyField("Height in feet and inches format"));
    properties.put("address", createPropertyField("Full street address including city, state, and zip code"));
    properties.put("eye_color", createPropertyField("Eye color abbreviation"));
    properties.put("last_name", createPropertyField("Driver's last name"));
    properties.put("first_name", createPropertyField("Driver's first name"));
    properties.put("issue_date", createPropertyField("License issue date in MM/DD/YYYY format"));
    properties.put("date_of_birth", createPropertyField("Date of birth in MM/DD/YYYY format"));
    properties.put("license_class", createPropertyField("Driver license class/type"));
    properties.put("license_number", createPropertyField("Driver license number (DLN)"));
    properties.put("expiration_date", createPropertyField("License expiration date in MM/DD/YYYY format"));

    schema.put("properties", properties);
    return schema;
  }

  private static Map<String, Object> createPropertyField(String description) {
    Map<String, Object> field = new HashMap<>();
    List<String> types = new ArrayList<>();
    types.add("string");
    types.add("null");
    field.put("type", types);
    field.put("description", description);
    return field;
  }

  // Simple JSON parser/encoder (no external dependencies)
  private static Map<String, Object> parseJson(String json) {
    json = json.trim();
    if (!json.startsWith("{")) {
      return new HashMap<>();
    }
    Map<String, Object> result = new HashMap<>();
    json = json.substring(1, json.length() - 1);
    int depth = 0;
    StringBuilder currentKey = new StringBuilder();
    StringBuilder currentValue = new StringBuilder();
    boolean inString = false;
    boolean parsingKey = true;

    for (int i = 0; i < json.length(); i++) {
      char c = json.charAt(i);
      if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
        inString = !inString;
      }
      if (!inString) {
        if (c == ':' && parsingKey && depth == 0) {
          parsingKey = false;
          continue;
        }
        if (c == ',' && depth == 0) {
          String key = currentKey.toString().trim().replaceAll("\"", "");
          String value = currentValue.toString().trim();
          result.put(key, parseJsonValue(value));
          currentKey = new StringBuilder();
          currentValue = new StringBuilder();
          parsingKey = true;
          continue;
        }
        if (c == '{' || c == '[') {
          depth++;
        } else if (c == '}' || c == ']') {
          depth--;
        }
      }
      if (parsingKey) {
        currentKey.append(c);
      } else {
        currentValue.append(c);
      }
    }
    if (currentKey.length() > 0) {
      String key = currentKey.toString().trim().replaceAll("\"", "");
      String value = currentValue.toString().trim();
      result.put(key, parseJsonValue(value));
    }
    return result;
  }

  private static Object parseJsonValue(String value) {
    value = value.trim();
    if (value.isEmpty()) {
      return null;
    }
    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);
    }
    if (value.startsWith("[") && value.endsWith("]")) {
      return new ArrayList<>();
    }
    if (value.startsWith("{") && value.endsWith("}")) {
      return new HashMap<>();
    }
    try {
      if (value.contains(".")) {
        return Double.parseDouble(value);
      } else {
        return Long.parseLong(value);
      }
    } catch (NumberFormatException e) {
      return value;
    }
  }

  private static String toJson(Object obj) {
    if (obj == null) {
      return "null";
    }
    if (obj instanceof String) {
      return "\"" + escapeJson((String) obj) + "\"";
    }
    if (obj instanceof Boolean || obj instanceof Number) {
      return obj.toString();
    }
    if (obj instanceof Map) {
      Map<String, Object> map = (Map<String, Object>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<String, Object> entry : map.entrySet()) {
        if (!first) {
          sb.append(",");
        }
        sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
        sb.append(toJson(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : list) {
        if (!first) {
          sb.append(",");
        }
        sb.append(toJson(item));
        first = false;
      }
      sb.append("]");
      return sb.toString();
    }
    return "\"" + escapeJson(obj.toString()) + "\"";
  }

  private static String escapeJson(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r")
        .replace("\t", "\\t");
  }
}
package main

// This script uses the Extend REST API directly because Extend has no official Go SDK yet.
// Mirror the exact same endpoints, request bodies, and response fields the TypeScript reference uses.

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
)

const (
	API     = "https://api.extend.ai"
	VERSION = "2026-02-09"
)

type State struct {
	WorkflowID string `json:"workflowId,omitempty"`
}

type Property struct {
	Type        interface{} `json:"type"`
	Description string      `json:"description"`
}

type Schema struct {
	Type       string              `json:"type"`
	Properties map[string]Property `json:"properties"`
}

type ExtractorConfig struct {
	Schema struct {
		Type       string              `json:"type"`
		Properties map[string]Property `json:"properties"`
	} `json:"schema"`
	BaseProcessor    string `json:"baseProcessor"`
	AdvancedOptions  struct {
		ReviewAgent struct {
			Enabled bool `json:"enabled"`
		} `json:"reviewAgent"`
		AdvancedMultimodalEnabled bool `json:"advancedMultimodalEnabled"`
	} `json:"advancedOptions"`
}

type StepConfig struct {
	ParseConfig struct {
		BlockOptions struct {
			Text struct {
				Agentic struct {
					Enabled bool `json:"enabled"`
				} `json:"agentic"`
			} `json:"text"`
		} `json:"blockOptions"`
		ChunkingStrategy struct {
			Type string `json:"type"`
		} `json:"chunkingStrategy"`
	} `json:"parseConfig,omitempty"`
	ExtractorConfig ExtractorConfig `json:"extractorConfig,omitempty"`
}

type NextStep struct {
	Step string `json:"step"`
}

type WorkflowStep struct {
	Name   string      `json:"name"`
	Type   string      `json:"type"`
	Config StepConfig  `json:"config,omitempty"`
	Next   []NextStep  `json:"next,omitempty"`
}

type Workflow struct {
	Name  string         `json:"name"`
	Steps []WorkflowStep `json:"steps"`
}

type APIListResponse struct {
	Data  []map[string]interface{} `json:"data,omitempty"`
	Items []map[string]interface{} `json:"items,omitempty"`
}

type APICreateResponse struct {
	ID       string                 `json:"id,omitempty"`
	Workflow map[string]interface{} `json:"workflow,omitempty"`
}

var apiKey string
var 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)
	}
}

func loadState() error {
	stateDir := filepath.Join(".", ".extend")
	stateFile := filepath.Join(stateDir, "driver-license-template.json")

	data, err := os.ReadFile(stateFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // File doesn't exist yet, start with empty state
		}
		return err
	}

	return json.Unmarshal(data, &state)
}

func saveState() error {
	stateDir := filepath.Join(".", ".extend")
	stateFile := filepath.Join(stateDir, "driver-license-template.json")

	if err := os.MkdirAll(stateDir, 0755); err != nil {
		return err
	}

	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(stateFile, data, 0644)
}

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	url := API + pathName
	var reqBody io.Reader
	var contentType string

	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewReader(bodyBytes)
		contentType = "application/json"
	}

	req, err := http.NewRequest(method, url, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("x-extend-api-version", VERSION)
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var data map[string]interface{}
	_ = json.Unmarshal(respBody, &data) // Ignore parse errors

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		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 buildWorkflow() Workflow {
	workflow := Workflow{
		Name: "Driver License Template Processing Pipeline",
		Steps: []WorkflowStep{
			{
				Name: "startTrigger1",
				Type: "TRIGGER",
				Next: []NextStep{
					{Step: "parse1"},
				},
			},
			{
				Name: "parse1",
				Type: "PARSE",
				Config: StepConfig{
					ParseConfig: struct {
						BlockOptions struct {
							Text struct {
								Agentic struct {
									Enabled bool `json:"enabled"`
								} `json:"agentic"`
							} `json:"text"`
						} `json:"blockOptions"`
						ChunkingStrategy struct {
							Type string `json:"type"`
						} `json:"chunkingStrategy"`
					}{
						BlockOptions: struct {
							Text struct {
								Agentic struct {
									Enabled bool `json:"enabled"`
								} `json:"agentic"`
							} `json:"text"`
						}{
							Text: struct {
								Agentic struct {
									Enabled bool `json:"enabled"`
								} `json:"agentic"`
							}{
								Agentic: struct {
									Enabled bool `json:"enabled"`
								}{
									Enabled: true,
								},
							},
						},
						ChunkingStrategy: struct {
							Type string `json:"type"`
						}{
							Type: "document",
						},
					},
				},
				Next: []NextStep{
					{Step: "extraction2"},
				},
			},
			{
				Name: "extraction2",
				Type: "EXTRACT",
				Config: StepConfig{
					ExtractorConfig: ExtractorConfig{
						Schema: struct {
							Type       string              `json:"type"`
							Properties map[string]Property `json:"properties"`
						}{
							Type: "object",
							Properties: map[string]Property{
								"state": {Type: []string{"string", "null"}, Description: "State of issuance"},
								"gender": {Type: []string{"string", "null"}, Description: "Gender (M/F)"},
								"height": {Type: []string{"string", "null"}, Description: "Height in feet and inches format"},
								"address": {Type: []string{"string", "null"}, Description: "Full street address including city, state, and zip code"},
								"eye_color": {Type: []string{"string", "null"}, Description: "Eye color abbreviation"},
								"last_name": {Type: []string{"string", "null"}, Description: "Driver's last name"},
								"first_name": {Type: []string{"string", "null"}, Description: "Driver's first name"},
								"issue_date": {Type: []string{"string", "null"}, Description: "License issue date in MM/DD/YYYY format"},
								"date_of_birth": {Type: []string{"string", "null"}, Description: "Date of birth in MM/DD/YYYY format"},
								"license_class": {Type: []string{"string", "null"}, Description: "Driver license class/type"},
								"license_number": {Type: []string{"string", "null"}, Description: "Driver license number (DLN)"},
								"expiration_date": {Type: []string{"string", "null"}, Description: "License expiration date in MM/DD/YYYY format"},
							},
						},
						BaseProcessor: "extraction_performance",
						AdvancedOptions: struct {
							ReviewAgent struct {
								Enabled bool `json:"enabled"`
							} `json:"reviewAgent"`
							AdvancedMultimodalEnabled bool `json:"advancedMultimodalEnabled"`
						}{
							ReviewAgent: struct {
								Enabled bool `json:"enabled"`
							}{
								Enabled: true,
							},
							AdvancedMultimodalEnabled: true,
						},
					},
				},
			},
		},
	}
	return workflow
}

func main() {
	if err := loadState(); err != nil {
		fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
		os.Exit(1)
	}

	workflow := buildWorkflow()
	fmt.Printf("Deploying \"%s\"…\n", workflow.Name)

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
			"steps": workflow.Steps,
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error updating workflow: %v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to find existing workflow with same name
		found := false
		listResp, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflow.Name)), nil)
		if err == nil {
			items := []map[string]interface{}{}
			if data, ok := listResp["data"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			} else if itemsRaw, ok := listResp["items"].([]interface{}); ok {
				for _, item := range itemsRaw {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			}

			for _, item := range items {
				if name, ok := item["name"].(string); ok && name == workflow.Name {
					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)
						_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
							"steps": workflow.Steps,
						})
						if err != nil {
							fmt.Fprintf(os.Stderr, "Error updating workflow: %v\n", err)
							os.Exit(1)
						}
						found = true
						break
					}
				}
			}
		}

		if !found {
			createResp, err := apiCall("POST", "/workflows", workflow)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error creating workflow: %v\n", err)
				os.Exit(1)
			}

			wfID := ""
			if id, ok := createResp["id"].(string); ok {
				wfID = id
			} else if wfObj, ok := createResp["workflow"].(map[string]interface{}); ok {
				if id, ok := wfObj["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
			if err := saveState(); err != nil {
				fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

	// Deploy as new version (best-effort)
	_, _ = 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.")
}

// url package not imported; add simple QueryEscape implementation
import "net/url"

var url = struct {
	QueryEscape func(string) string
}{
	QueryEscape: net.url.QueryEscape,
}

Frequently Asked Questions (FAQ)

Driver licenses vary wildly by state (layout, fonts, barcode positions), luckily Extend's Extractor are able to handle different layouts. If any optimization is needed, feel free to use the in-platform Composer chat for fine-tuning.
Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
Parse first with `agentic_ocr` to capture state variations, then run Extract with a schema that includes optional fields for state-specific data (e.g., organ donor status, restrictions). Alternatively, classify by state first, then run Extract with state-tuned schemas separately for higher accuracy.
For KYC/compliance, require `confidence ≥ 0.92` on critical fields (name, DOB, license number); for general use, 0.85 is acceptable. Always flag results below your threshold for human review rather than rejecting silently, since state variations can legitimately reduce confidence scores.
Tags
Driver LicenseIdentity VerificationGovernment IDPersonal Data Extraction
About this template

This template is designed to extract key information from driver licenses, including personal details such as name, date of birth, license number, and address. It captures license class, expiration dates, physical characteristics, and restrictions, making it ideal for identity verification and record-keeping purposes.

Document formats
  • PDF
  • Images & Scans
Requirements
  • Scanned documents
  • Complex layouts

Relevant templates for Financial & Banking

  1. 01
    Receipt ExtractorParse → Extract
    Extracts itemized sales, pricing, GST tax, and payment details from retail receipts.
    PDFImages & Scanswww.extend.ai/templates/receipt-parse-extract
  2. 02
    Bank Statement ExtractorParse → Extract
    Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.
    PDFImages & Scanswww.extend.ai/templates/bank-statement
  3. 03
    Vendor Invoice ExtractorParse → Extract
    Extracts charges, billing details, line items, totals, and due dates vendor invoices.
    PDFImages & Scanswww.extend.ai/templates/vendor-invoice
  4. 04
    Pay Stub ExtractorParse → Extract
    Extracts employee earnings, deductions, taxes, and net pay from pay stubs.
    PDFwww.extend.ai/templates/pay-stub
  5. 05
    Check ExtractorParse → Extract
    Extracts check details including payee, amount, date, and bank routing information.
    PDFImages & Scanswww.extend.ai/templates/check
  6. 06
    Wire Transfer Instructions ExtractorParse → Extract
    Extracts wire transfer procedures, contact info, and operational hours from banking guides.
    PDFWord / DOCXwww.extend.ai/templates/wire-transfer-instructions
  7. 07
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement
  8. 08
    1040 Tax Return ExtractorParse → Extract
    Extracts personal and dependent information from U.S. individual income tax returns.
    PDFImages & Scanswww.extend.ai/templates/tax-return-form-1040