Extracts personal identification and licensing data from driver license documents.
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.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemacustom schema — 12 fieldschangedadvancedOptions.advancedMultimodalEnabledtruechangedadvancedOptions.reviewAgent.enabledtruechangedbaseProcessor"extraction_performance"You can learn more about Extract configuration in Extend's Extract documentation.
{
"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 (< 5import { 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,
}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.