Extracts personal identification and licensing data from driver license documents.
A driver's license is an official government-issued identification document that certifies an individual's identity, physical characteristics, and authorization to operate motor vehicles, including license class, restrictions, 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 licensing fields including name, address, date of birth, 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 typing import Optional
from extend_ai import Extend
client = Extend(token=os.environ["EXTEND_API_KEY"])
async 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
file_buffer = Path(file_path).read_bytes()
base64_str = base64.b64encode(file_buffer).decode("utf-8")
mime_type = "image/jpeg" # adjust if PNG or PDF
data_url = f"data:{mime_type};base64,{base64_str}"
print(f"Processing driver license: {Path(file_path).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 = await 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 = chunk["content"]
print(f" [Chunk] {content[:60]}...")
# Step 2: Extract structured fields into JSON with schema validation
# Uses extraction_performance for accuracy (priority over speed for identity docs)
print("\nStep 2: Extracting structured fields...")
extract_run = await client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"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)",
},
},
}
},
)
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 {license_data['expiration_date']}")
return license_data
# Main entry point for testing
if __name__ == "__main__":
import sys
import asyncio
file_path = sys.argv[1] if len(sys.argv) > 1 else "__FILE_PATH__"
try:
result = asyncio.run(process_driver_license_template(file_path))
print("\n✓ Processing complete")
except Exception as err:
print(f"Error: {err}")
sys.exit(1)// Extend AI REST API integration for Driver License Template extraction.
// Extend does not publish an official Java SDK; this code calls the REST API directly
// using only java.net.http.HttpClient and built-in JSON parsing (no third-party libraries).
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.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Scanner;
public class DriverLicenseProcessor {
private static final String API_BASE_URL = "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 class LicenseData {
public String first_name;
public String last_name;
public String license_number;
public String date_of_birth;
public String gender;
public String height;
public String eye_color;
public String address;
public String license_class;
public String issue_date;
public String expiration_date;
public String state;
}
public static void main(String[] args) throws Exception {
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 LicenseData processDriverLicenseTemplate(String filePath)
throws Exception {
// Read local file and convert to base64 data URL
Path path = Paths.get(filePath);
byte[] fileBuffer = Files.readAllBytes(path);
String base64 = Base64.getEncoder().encodeToString(fileBuffer);
String mimeType = "image/jpeg"; // adjust if PNG or PDF
String dataUrl = "data:" + mimeType + ";base64," + base64;
System.out.println("Processing driver license: " + path.getFileName());
// Step 1: Parse the license image with agentic OCR
System.out.println("Step 1: Parsing license image...");
ParseRunResponse parseRun = createAndPollParseRun(dataUrl);
if (!"PROCESSED".equals(parseRun.status)) {
throw new Exception("Parse failed with status: " + parseRun.status);
}
System.out.println("Parsed " + parseRun.output.chunks.length + " text chunks");
for (int i = 0; i < parseRun.output.chunks.length; i++) {
String content = parseRun.output.chunks[i].content;
String preview =
content.length() > 60 ? content.substring(0, 60) + "..." : content;
System.out.println(" [Chunk] " + preview);
}
// Step 2: Extract structured fields into JSON
System.out.println("\nStep 2: Extracting structured fields...");
ExtractRunResponse extractRun = createAndPollExtractRun(dataUrl);
if (!"PROCESSED".equals(extractRun.status)) {
throw new Exception("Extract failed with status: " + extractRun.status);
}
LicenseData licenseData = extractRun.output.value;
// Step 3: Validate and display extracted data
System.out.println("\nExtracted License Data:");
System.out.println(jsonStringify(licenseData));
// Example: Check for expiration
if (licenseData.expiration_date != null && !licenseData.expiration_date.isEmpty()) {
String[] parts = licenseData.expiration_date.split("/");
if (parts.length == 3) {
LocalDate expDate = LocalDate.of(Integer.parseInt(parts[2]),
Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
LocalDate now = LocalDate.now();
if (expDate.isBefore(now)) {
System.out.println("⚠️ WARNING: License is expired");
} else {
System.out.println("✓ License valid until " + licenseData.expiration_date);
}
}
}
return licenseData;
}
private static ParseRunResponse createAndPollParseRun(String dataUrl)
throws Exception {
String requestBody = "{\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"}}";
// Create the initial parse run
HttpRequest createRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/parse_runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> createResponse =
httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString());
ParseRunResponse run = parseParseRunResponse(createResponse.body());
// Poll until completion
while (!"PROCESSED".equals(run.status) && !"FAILED".equals(run.status)) {
Thread.sleep(2000);
HttpRequest pollRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/parse_runs/" + run.id))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> pollResponse =
httpClient.send(pollRequest, HttpResponse.BodyHandlers.ofString());
run = parseParseRunResponse(pollResponse.body());
}
return run;
}
private static ExtractRunResponse createAndPollExtractRun(String dataUrl)
throws Exception {
String schemaJson = buildExtractionSchema();
String requestBody = "{\"file\": {\"url\": \"" + escapeJson(dataUrl)
+ "\"}, \"config\": {\"schema\": " + schemaJson + "}}";
// Create the initial extract run
HttpRequest createRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/extract_runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> createResponse =
httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString());
ExtractRunResponse run = parseExtractRunResponse(createResponse.body());
// Poll until completion
while (!"PROCESSED".equals(run.status) && !"FAILED".equals(run.status)) {
Thread.sleep(2000);
HttpRequest pollRequest = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/extract_runs/" + run.id))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> pollResponse =
httpClient.send(pollRequest, HttpResponse.BodyHandlers.ofString());
run = parseExtractRunResponse(pollResponse.body());
}
return run;
}
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 ParseRunResponse parseParseRunResponse(String json) {
ParseRunResponse response = new ParseRunResponse();
response.id = extractJsonField(json, "\"id\"", "id");
response.status = extractJsonField(json, "\"status\"", "status");
// Parse chunks from output
String outputStr = extractJsonValue(json, "\"output\"");
String chunksStr = extractJsonValue(outputStr, "\"chunks\"");
response.output = new ParseOutput();
response.output.chunks = parseChunks(chunksStr);
return response;
}
private static Chunk[] parseChunks(String chunksJson) {
// Simple chunk array parsing (assumes simple structure)
java.util.List<Chunk> chunks = new java.util.ArrayList<>();
int start = 0;
while ((start = chunksJson.indexOf("{", start)) != -1) {
int end = chunksJson.indexOf("}", start) + 1;
String chunkObj = chunksJson.substring(start, end);
Chunk chunk = new Chunk();
chunk.content = extractJsonField(chunkObj, "\"content\"", "content");
chunks.add(chunk);
start = end;
}
return chunks.toArray(new Chunk[0]);
}
private static ExtractRunResponse parseExtractRunResponse(String json) {
ExtractRunResponse response = new ExtractRunResponse();
response.id = extractJsonField(json, "\"id\"", "id");
response.status = extractJsonField(json, "\"status\"", "status");
String outputStr = extractJsonValue(json, "\"output\"");
String valueStr = extractJsonValue(outputStr, "\"value\"");
response.output = new ExtractOutput();
response.output.value = parseLicenseData(valueStr);
return response;
}
private static LicenseData parseLicenseData(String json) {
LicenseData data = new LicenseData();
data.first_name = extractJsonField(json, "\"first_name\"", "first_name");
data.last_name = extractJsonField(json, "\"last_name\"", "last_name");
data.license_number = extractJsonField(json, "\"license_number\"", "license_number");
data.date_of_birth = extractJsonField(json, "\"date_of_birth\"", "date_of_birth");
data.gender = extractJsonField(json, "\"gender\"", "gender");
data.height = extractJsonField(json, "\"height\"", "height");
data.eye_color = extractJsonField(json, "\"eye_color\"", "eye_color");
data.address = extractJsonField(json, "\"address\"", "address");
data.license_class = extractJsonField(json, "\"license_class\"", "license_class");
data.issue_date = extractJsonField(json, "\"issue_date\"", "issue_date");
data.expiration_date = extractJsonField(json, "\"expiration_date\"", "expiration_date");
data.state = extractJsonField(json, "\"state\"", "state");
return data;
}
private static String extractJsonField(String json, String fieldKey, String fieldName) {
int keyIndex = json.indexOf(fieldKey);
if (keyIndex == -1) {
return null;
}
int colonIndex = json.indexOf(":", keyIndex);
if (colonIndex == -1) {
return null;
}
int quoteStart = json.indexOf("\"", colonIndex);
if (quoteStart == -1) {
return null;
}
int quoteEnd = json.indexOf("\"", quoteStart + 1);
if (quoteEnd == -1) {
return null;
}
return json.substring(quoteStart + 1, quoteEnd);
}
private static String extractJsonValue(String json, String key) {
int keyIndex = json.indexOf(key);
if (keyIndex == -1) {
return "";
}
int colonIndex = json.indexOf(":", keyIndex);
if (colonIndex == -1) {
return "";
}
int start = colonIndex + 1;
while (start < json.length()
&& (json.charAt(start) == ' ' || json.charAt(start) == '\n')) {
start++;
}
if (json.charAt(start) == '{') {
int braceCount = 1;
int pos = start + 1;
while (pos < json.length() && braceCount > 0) {
if (json.charAt(pos) == '{') {
braceCount++;
} else if (json.charAt(pos) == '}') {
braceCount--;
}
pos++;
}
return json.substring(start, pos);
} else if (json.charAt(start) == '[') {
int bracketCount = 1;
int pos = start + 1;
while (pos < json.length() && bracketCount > 0) {
if (json.charAt(pos) == '[') {
bracketCount++;
} else if (json.charAt(pos) == ']') {
bracketCount--;
}
pos++;
}
return json.substring(start, pos);
}
return "";
}
private static String escapeJson(String str) {
return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
.replace("\r", "\\r");
}
private static String jsonStringify(Object obj) {
if (obj instanceof LicenseData) {
LicenseData data = (LicenseData) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
if (data.first_name != null) {
if (!first)
sb.append(",");
sb.append("\"first_name\": ").append(jsonString(data.first_name));
first = false;
}
if (data.last_name != null) {
if (!first)
sb.append(",");
sb.append("\"last_name\": ").append(jsonString(data.last_name));
first = false;
}
if (data.license_number != null) {
if (!first)
sb.append(",");
sb.append("\"license_number\": ").append(jsonString(data.license_number));
first = false;
}
if (data.date_of_birth != null) {
if (!first)
sb.append(",");
sb.append("\"date_of_birth\": ").append(jsonString(data.date_of_birth));
first = false;
}
if (data.gender != null) {
if (!first)
sb.append(",");
sb.append("\"gender\": ").append(jsonString(data.gender));
first = false;
}
if (data.height != null) {
if (!first)
sb.append(",");
sb.append("\"height\": ").append(jsonString(data.height));
first = false;
}
if (data.eye_color != null) {
if (!first)
sb.append(",");
sb.append("\"eye_color\": ").append(jsonString(data.eye_color));
first = false;
}
if (data.address != null) {
if (!first)
sb.append(",");
sb.append("\"address\": ").append(jsonString(data.address));
first = false;
}
if (data.license_class != null) {
if (!first)
sb.append(",");
sb.append("\"license_class\": ").append(jsonString(data.license_class));
first = false;
}
if (data.issue_date != null) {
if (!first)
sb.append(",");
sb.append("\"issue_date\": ").append(jsonString(data.issue_date));
first = false;
}
if (data.expiration_date != null) {
if (!first)
sb.append(",");
sb.append("\"expiration_date\": ").append(jsonString(data.expiration_date));
first = false;
}
if (data.state != null) {
if (!first)
sb.append(",");
sb.append("\"state\": ").append(jsonString(data.state));
first = false;
}
sb.append("}");
return sb.toString();
}
return "{}";
}
private static String jsonString(String str) {
return "\"" + str.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
static class ParseRunResponse {
String id;
String status;
ParseOutput output;
}
static class ParseOutput {
Chunk[] chunks;
}
static class Chunk {
String content;
}
static class ExtractRunResponse {
String id;
String status;
ExtractOutput output;
}
static class ExtractOutput {
LicenseData value;
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with Bearer token authentication.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
const (
extendAPIBase = "https://api.extend.ai"
)
// LicenseData represents the extracted driver's 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"`
}
// ParseRunOutput represents the output from a parse run
type ParseRunOutput struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
}
// ParseRun represents a parse run response
type ParseRun struct {
Status string `json:"status"`
Output ParseRunOutput `json:"output"`
}
// ExtractRunOutput represents the output from an extract run
type ExtractRunOutput struct {
Value LicenseData `json:"value"`
}
// ExtractRun represents an extract run response
type ExtractRun struct {
Status string `json:"status"`
Output ExtractRunOutput `json:"output"`
}
func processDriverLicenseTemplate(filePath string) (*LicenseData, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
// Read local file and convert to base64 data URL
fileBuffer, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
base64Str := base64.StdEncoding.EncodeToString(fileBuffer)
mimeType := "image/jpeg" // adjust if PNG or PDF
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
fmt.Printf("Processing driver license: %s\n", filepath.Base(filePath))
// Step 1: Parse the license image with agentic OCR
fmt.Println("Step 1: Parsing license image...")
parseRun, err := createAndPollParseRun(apiKey, dataURL)
if err != nil {
return nil, fmt.Errorf("parse run failed: %w", err)
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
fmt.Printf("Parsed %d text chunks\n", len(parseRun.Output.Chunks))
for _, chunk := range parseRun.Output.Chunks {
content := chunk.Content
if len(content) > 60 {
content = content[:60]
}
fmt.Printf(" [Chunk] %s...\n", content)
}
// Step 2: Extract structured fields into JSON
fmt.Println("\nStep 2: Extracting structured fields...")
extractRun, err := createAndPollExtractRun(apiKey, dataURL)
if err != nil {
return nil, fmt.Errorf("extract run failed: %w", err)
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extract failed with status: %s", extractRun.Status)
}
licenseData := &extractRun.Output.Value
// Step 3: Validate and display extracted data
fmt.Println("\nExtracted License Data:")
jsonData, err := json.MarshalIndent(licenseData, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal license data: %w", err)
}
fmt.Println(string(jsonData))
// Example: Check for expiration
if licenseData.ExpirationDate != nil && *licenseData.ExpirationDate != "" {
parts := strings.Split(*licenseData.ExpirationDate, "/")
if len(parts) == 3 {
dateStr := fmt.Sprintf("%s-%s-%s", parts[2], parts[0], parts[1])
expDate, err := time.Parse("2006-01-02", dateStr)
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 createAndPollParseRun(apiKey, dataURL string) (*ParseRun, error) {
// Create parse run request
reqBody := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", extendAPIBase+"/v1/parseRuns", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("parse run creation failed with status %d: %s", resp.StatusCode, string(respBody))
}
var parseRun ParseRun
err = json.NewDecoder(resp.Body).Decode(&parseRun)
if err != nil {
return nil, err
}
// Poll until processed (with simple polling; a production implementation would use exponential backoff)
for i := 0; i < 120; i++ {
if parseRun.Status == "PROCESSED" || parseRun.Status == "FAILED" {
break
}
time.Sleep(1 * time.Second)
pollReq, err := http.NewRequest("GET", extendAPIBase+"/v1/parseRuns", nil)
if err != nil {
return nil, err
}
pollReq.Header.Set("Authorization", "Bearer "+apiKey)
pollResp, err := client.Do(pollReq)
if err != nil {
return nil, err
}
defer pollResp.Body.Close()
err = json.NewDecoder(pollResp.Body).Decode(&parseRun)
if err != nil {
return nil, err
}
}
return &parseRun, nil
}
func createAndPollExtractRun(apiKey, dataURL string) (*ExtractRun, error) {
// Create extract run request with schema
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)",
},
},
}
reqBody := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
"config": map[string]interface{}{
"schema": schema,
},
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", extendAPIBase+"/v1/extractRuns", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("extract run creation failed with status %d: %s", resp.StatusCode, string(respBody))
}
var extractRun ExtractRun
err = json.NewDecoder(resp.Body).Decode(&extractRun)
if err != nil {
return nil, err
}
// Poll until processed
for i := 0; i < 120; i++ {
if extractRun.Status == "PROCESSED" || extractRun.Status == "FAILED" {
break
}
time.Sleep(1 * time.Second)
pollReq, err := http.NewRequest("GET", extendAPIBase+"/v1/extractRuns", nil)
if err != nil {
return nil, err
}
pollReq.Header.Set("Authorization", "Bearer "+apiKey)
pollResp, err := client.Do(pollReq)
if err != nil {
return nil, err
}
defer pollResp.Body.Close()
err = json.NewDecoder(pollResp.Body).Decode(&extractRun)
if err != nil {
return nil, err
}
}
return &extractRun, 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/kyc-verification-agent.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: kyc-verification-agent).
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, "kyc-verification-agent.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); });
import os
import json
import sys
from pathlib import Path
from typing import Optional, Any
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "kyc-verification-agent.json"
def load_state() -> dict[str, Any]:
if STATE_FILE.exists():
with open(STATE_FILE, "r") as f:
return json.load(f)
return {}
def save_state(state: dict[str, Any]) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, 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:
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(
workflow_id, {"steps": WORKFLOW["steps"]}
)
else:
# Try to find an existing workflow with the same name
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(
existing_id, {"steps": WORKFLOW["steps"]}
)
else:
created = await client.workflows.create(WORKFLOW)
workflow_id = getattr(created, "id", None) or getattr(
getattr(created, "workflow", None), "id", None
)
if not workflow_id:
raise ValueError(
"Could not read created workflow id from response"
)
state["workflowId"] = workflow_id
save_state(state)
print(f"+ created workflow ({workflow_id})")
# Deploy the current draft as a new version (best-effort)
try:
await client.workflows.create_version(state["workflowId"], {})
except Exception:
pass
workflow_id = state["workflowId"]
print("\nDone. Run documents through it with:")
print(
f' POST https://api.extend.ai/workflow_runs {{ "workflow": {{ "id": "{workflow_id}" }}, "file": {{ "url": "https://…" }} }}'
)
print(
"Or open the workflow in the Extend dashboard to review and deploy it."
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())// This Java provisioning script uses Extend's REST API directly (https://api.extend.ai)
// because Extend has no official Java SDK yet. It makes HTTP calls using only
// java.net.http.HttpClient and standard library JSON parsing.
//
// Deploy the "Driver License Template" pipeline to YOUR Extend account.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// javac Provision.java && java Provision
//
// Generated by doc1 (template: kyc-verification-agent).
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class 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("kyc-verification-agent.json");
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
private static Map<String, Object> state = new LinkedHashMap<>();
public static void main(String[] args) throws Exception {
loadState();
Map<String, Object> workflow = buildWorkflow();
run(workflow);
}
private static void loadState() throws IOException {
if (Files.exists(STATE_FILE)) {
String json = Files.readString(STATE_FILE);
state = parseJson(json);
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
String json = toJson(state);
Files.writeString(STATE_FILE, json);
}
private static String api(String method, String pathName, Map<String, Object> body)
throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
String url = API + pathName;
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(toJson(body)))
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
reqBuilder.header("Content-Type", "application/json");
}
HttpRequest req = reqBuilder.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() < 200 || res.statusCode() >= 300) {
String responseBody = res.body().length() > 300 ? res.body().substring(0, 300) : res.body();
throw new RuntimeException(
method + " " + pathName + " failed (" + res.statusCode() + "): " + responseBody);
}
return res.body();
}
private static void run(Map<String, Object> workflow) throws Exception {
String name = (String) workflow.get("name");
System.out.println("Deploying \"" + name + "\"…");
String workflowId = (String) state.get("workflowId");
if (workflowId != null && !workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + workflowId + ") — updating steps");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + workflowId, updateBody);
} else {
// Try to find existing workflow with the same name
try {
String encodedName = URLEncoder.encode(name, StandardCharsets.UTF_8);
String listResponse = api("GET", "/workflows?name=" + encodedName, null);
Map<String, Object> listData = parseJson(listResponse);
List<Map<String, Object>> items = new ArrayList<>();
if (listData.containsKey("data")) {
items = (List<Map<String, Object>>) listData.get("data");
} else if (listData.containsKey("items")) {
items = (List<Map<String, Object>>) listData.get("items");
}
for (Map<String, Object> item : items) {
if (name.equals(item.get("name"))) {
String existingId = (String) item.get("id");
if (existingId != null) {
state.put("workflowId", existingId);
saveState();
System.out.println("✓ workflow \"" + name + "\" found in your account (" + existingId
+ ") — updating steps");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + existingId, updateBody);
workflowId = existingId;
break;
}
}
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (workflowId == null || workflowId.isEmpty()) {
String createResponse = api("POST", "/workflows", workflow);
Map<String, Object> created = parseJson(createResponse);
String wfId = (String) created.get("id");
if (wfId == null) {
Map<String, Object> wfObj = (Map<String, Object>) created.get("workflow");
if (wfObj != null) {
wfId = (String) wfObj.get("id");
}
}
if (wfId == null) {
throw new RuntimeException("Could not read created workflow id from response");
}
state.put("workflowId", wfId);
saveState();
System.out.println("+ created workflow (" + wfId + ")");
workflowId = wfId;
}
}
// Deploy the current draft as a new version
try {
api("POST", "/workflows/" + workflowId + "/versions", new LinkedHashMap<>());
} catch (Exception e) {
// best-effort: some accounts/plans may not require this explicit step
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + workflowId
+ "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Driver License Template Processing Pipeline");
List<Map<String, Object>> steps = new ArrayList<>();
// startTrigger1
Map<String, Object> startTrigger = new LinkedHashMap<>();
startTrigger.put("name", "startTrigger1");
startTrigger.put("type", "TRIGGER");
List<Map<String, String>> nextStart = new ArrayList<>();
Map<String, String> nextStartStep = new LinkedHashMap<>();
nextStartStep.put("step", "parse1");
nextStart.add(nextStartStep);
startTrigger.put("next", nextStart);
steps.add(startTrigger);
// parse1
Map<String, Object> parse = new LinkedHashMap<>();
parse.put("name", "parse1");
parse.put("type", "PARSE");
Map<String, Object> parseConfig = new LinkedHashMap<>();
Map<String, Object> blockOptions = new LinkedHashMap<>();
Map<String, Object> textBlock = new LinkedHashMap<>();
Map<String, Object> agentic = new LinkedHashMap<>();
agentic.put("enabled", true);
textBlock.put("agentic", agentic);
blockOptions.put("text", textBlock);
parseConfig.put("blockOptions", blockOptions);
Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
chunkingStrategy.put("type", "document");
parseConfig.put("chunkingStrategy", chunkingStrategy);
Map<String, Object> parseConfigWrapper = new LinkedHashMap<>();
parseConfigWrapper.put("parseConfig", parseConfig);
parse.put("config", parseConfigWrapper);
List<Map<String, String>> nextParse = new ArrayList<>();
Map<String, String> nextParseStep = new LinkedHashMap<>();
nextParseStep.put("step", "extraction2");
nextParse.add(nextParseStep);
parse.put("next", nextParse);
steps.add(parse);
// extraction2
Map<String, Object> extraction = new LinkedHashMap<>();
extraction.put("name", "extraction2");
extraction.put("type", "EXTRACT");
Map<String, Object> extractConfig = new LinkedHashMap<>();
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("state", prop("State of issuance"));
properties.put("gender", prop("Gender (M/F)"));
properties.put("height", prop("Height in feet and inches format"));
properties.put("address", prop("Full street address including city, state, and zip code"));
properties.put("eye_color", prop("Eye color abbreviation"));
properties.put("last_name", prop("Driver's last name"));
properties.put("first_name", prop("Driver's first name"));
properties.put("issue_date", prop("License issue date in MM/DD/YYYY format"));
properties.put("date_of_birth", prop("Date of birth in MM/DD/YYYY format"));
properties.put("license_class", prop("Driver license class/type"));
properties.put("license_number", prop("Driver license number (DLN)"));
properties.put("expiration_date", prop("License expiration date in MM/DD/YYYY format"));
schema.put("properties", properties);
Map<String, Object> extractorConfig = new LinkedHashMap<>();
extractorConfig.put("schema", schema);
extractorConfig.put("baseProcessor", "extraction_performance");
Map<String, Object> advancedOptions = new LinkedHashMap<>();
Map<String, Object> reviewAgent = new LinkedHashMap<>();
reviewAgent.put("enabled", true);
advancedOptions.put("reviewAgent", reviewAgent);
advancedOptions.put("advancedMultimodalEnabled", true);
extractorConfig.put("advancedOptions", advancedOptions);
Map<String, Object> extractorConfigWrapper = new LinkedHashMap<>();
extractorConfigWrapper.put("extractorConfig", extractorConfig);
extraction.put("config", extractorConfigWrapper);
steps.add(extraction);
workflow.put("steps", steps);
return workflow;
}
private static Map<String, Object> prop(String description) {
Map<String, Object> p = new LinkedHashMap<>();
List<String> types = new ArrayList<>();
types.add("string");
types.add("null");
p.put("type", types);
p.put("description", description);
return p;
}
private static Map<String, Object> parseJson(String json) {
return new JsonParser().parse(json);
}
private static String toJson(Map<String, Object> map) {
return new JsonSerializer().serialize(map);
}
// Minimal JSON parser
private static class JsonParser {
private String input;
private int pos;
Map<String, Object> parse(String json) {
this.input = json;
this.pos = 0;
return (Map<String, Object>) parseValue();
}
private Object parseValue() {
skipWhitespace();
char c = peek();
if (c == '{') {
return parseObject();
} else if (c == '[') {
return parseArray();
} else if (c == '"') {
return parseString();
} else if (c == 't' || c == 'f') {
return parseBoolean();
} else if (c == 'n') {
return parseNull();
} else {
return parseNumber();
}
}
private Map<String, Object> parseObject() {
Map<String, Object> obj = new LinkedHashMap<>();
consume('{');
skipWhitespace();
if (peek() != '}') {
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
consume(':');
skipWhitespace();
Object value = parseValue();
obj.put(key, value);
skipWhitespace();
if (peek() == ',') {
consume(',');
} else {
break;
}
}
}
consume('}');
return obj;
}
private List<Object> parseArray() {
List<Object> arr = new ArrayList<>();
consume('[');
skipWhitespace();
if (peek() != ']') {
while (true) {
skipWhitespace();
arr.add(parseValue());
skipWhitespace();
if (peek() == ',') {
consume(',');
} else {
break;
}
}
}
consume(']');
return arr;
}
private String parseString() {
consume('"');
StringBuilder sb = new StringBuilder();
while (peek() != '"') {
if (peek() == '\\') {
consume('\\');
char esc = consume();
switch (esc) {
case 'n': sb.append('\n'); break;
case 't': sb.append('\t'); break;
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
default: sb.append(esc);
}
} else {
sb.append(consume());
}
}
consume('"');
return sb.toString();
}
private Boolean parseBoolean() {
if (input.startsWith("true", pos)) {
pos += 4;
return true;
} else {
pos += 5;
return false;
}
}
private Object parseNull() {
pos += 4;
return null;
}
private Number parseNumber() {
StringBuilder sb = new StringBuilder();
if (peek() == '-') sb.append(consume());
while (Character.isDigit(peek())) sb.append(consume());
if (peek() == '.') {
sb.append(consume());
while (Character.isDigit(peek())) sb.append(consume());
}
if (peek() == 'e' || peek() == 'E') {
sb.append(consume());
if (peek() == '+' || peek() == '-') sb.append(consume());
while (Character.isDigit(peek())) sb.append(consume());
}
String num = sb.toString();
if (num.contains(".") || num.contains("e") || num.contains("E")) {
return Double.parseDouble(num);
} else {
return Long.parseLong(num);
}
}
private void skipWhitespace() {
while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) {
pos++;
}
}
private char peek() {
if (pos >= input.length()) return '\0';
return input.charAt(pos);
}
private char consume() {
return input.charAt(pos++);
}
private void consume(char expected) {
if (consume() != expected) throw new RuntimeException("Unexpected character");
}
}
// Minimal JSON serializer
private static class JsonSerializer {
String serialize(Object obj) {
if (obj == null) {
return "null";
} else if (obj instanceof String) {
return "\"" + escapeString((String) obj) + "\"";
} else if (obj instanceof Boolean) {
return obj.toString();
} else if (obj instanceof Number) {
return obj.toString();
} else if (obj instanceof Map) {
return serializeMap((Map<String, Object>) obj);
} else if (obj instanceof List) {
return serializeList((List<Object>) obj);
} else {
return "null";
}
}
private String serializeMap(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(escapeString(entry.getKey())).append("\":");
sb.append(serialize(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
private String serializeList(List<Object> list) {
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append(serialize(item));
first = false;
}
sb.append("]");
return sb.toString();
}
private String escapeString(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
.replace("\r", "\\r").replace("\t", "\\t");
}
}
}// This Go code calls the Extend REST API directly (https://api.extend.ai).
// Extend does not publish an official Go SDK, so we use only the standard
// library net/http and encoding/json packages to make API calls.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
type WorkflowResponse struct {
ID string `json:"id,omitempty"`
Workflow struct {
ID string `json:"id,omitempty"`
} `json:"workflow,omitempty"`
}
type WorkflowListResponse struct {
Data []WorkflowItem `json:"data,omitempty"`
Items []WorkflowItem `json:"items,omitempty"`
}
type WorkflowItem struct {
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
}
func init() {
if os.Getenv("EXTEND_API_KEY") == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
}
func loadState(stateFile string) (State, error) {
var s State
data, err := os.ReadFile(stateFile)
if err != nil {
if os.IsNotExist(err) {
return s, nil
}
return s, err
}
err = json.Unmarshal(data, &s)
return s, err
}
func saveState(stateFile string, s State) error {
stateDir := filepath.Dir(stateFile)
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
var bodyReader io.Reader
var contentType string
if body != nil {
jsonBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(jsonBytes)
contentType = "application/json"
}
req, err := http.NewRequest(method, API+pathName, bodyReader)
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{}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var data map[string]interface{}
json.Unmarshal(resBody, &data)
if res.StatusCode < 200 || res.StatusCode >= 300 {
resStr := string(resBody)
if len(resStr) > 300 {
resStr = resStr[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, res.StatusCode, resStr)
}
return data, nil
}
func main() {
stateDir := filepath.Join(".", ".extend")
stateFile := filepath.Join(stateDir, "kyc-verification-agent.json")
state, err := loadState(stateFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err)
os.Exit(1)
}
workflow := map[string]interface{}{
"name": "Driver License Template Processing Pipeline",
"steps": []map[string]interface{}{
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": []map[string]interface{}{
{"step": "parse1"},
},
},
{
"name": "parse1",
"type": "PARSE",
"config": map[string]interface{}{
"parseConfig": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]interface{}{
"type": "document",
},
},
},
"next": []map[string]interface{}{
{"step": "extraction2"},
},
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": map[string]interface{}{
"extractorConfig": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"state": map[string]interface{}{
"type": []string{"string", "null"},
"description": "State of issuance",
},
"gender": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Gender (M/F)",
},
"height": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Height in feet and inches format",
},
"address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Full street address including city, state, and zip code",
},
"eye_color": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Eye color abbreviation",
},
"last_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Driver's last name",
},
"first_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Driver's first name",
},
"issue_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "License issue date in MM/DD/YYYY format",
},
"date_of_birth": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Date of birth in MM/DD/YYYY format",
},
"license_class": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Driver license class/type",
},
"license_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Driver license number (DLN)",
},
"expiration_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "License expiration date in MM/DD/YYYY format",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]interface{}{
"enabled": true,
},
"advancedMultimodalEnabled": true,
},
},
},
},
},
}
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 {
q := url.QueryEscape(workflow["name"].(string))
listResp, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", q), nil)
if err == nil {
var items []WorkflowItem
if data, ok := listResp["data"].([]interface{}); ok {
for _, item := range data {
if m, ok := item.(map[string]interface{}); ok {
items = append(items, WorkflowItem{
Name: m["name"].(string),
ID: m["id"].(string),
})
}
}
} else if data, ok := listResp["items"].([]interface{}); ok {
for _, item := range data {
if m, ok := item.(map[string]interface{}); ok {
items = append(items, WorkflowItem{
Name: m["name"].(string),
ID: m["id"].(string),
})
}
}
}
for _, item := range items {
if item.Name == workflow["name"].(string) && item.ID != "" {
state.WorkflowID = item.ID
if err := saveState(stateFile, state); err != nil {
fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow["name"], item.ID)
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", item.ID), map[string]interface{}{
"steps": workflow["steps"],
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error updating workflow: %v\n", err)
os.Exit(1)
}
break
}
}
}
if state.WorkflowID == "" {
created, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating workflow: %v\n", err)
os.Exit(1)
}
var wfID string
if id, ok := created["id"].(string); ok && id != "" {
wfID = id
} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := wf["id"].(string); ok {
wfID = id
}
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Error: could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
if err := saveState(stateFile, state); err != nil {
fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err)
os.Exit(1)
}
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})
fmt.Println("\nDone. Run documents through it with:")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}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.