Extracts check details including payee, amount, date, and bank routing information.
A check is a financial instrument issued by a bank account holder that instructs their bank to pay a specified amount to a named payee on a given date, containing the payer's details, routing and account numbers, and authorization signature. This template takes in Checks and outputs markdown (.md) capturing the check's full text and layout structure, and JSON (.json) with structured payment fields including check number, routing number, account number, date, payee, numeric and written amounts, payer details, memo, bank name, and unique check identifier 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": "Check 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": {
"date": {
"type": [
"string",
"null"
],
"description": "The date the check was issued (MM/DD/YYYY format)"
},
"memo": {
"type": [
"string",
"null"
],
"description": "The memo field text on the check"
},
"payee": {
"type": [
"string",
"null"
],
"description": "The name of the person or entity the check is payable to"
},
"bank_name": {
"type": [
"string",
"null"
],
"description": "The name of the bank"
},
"payer_name": {
"type": [
"string",
"null"
],
"description": "The name of the entity issuing the check"
},
"check_number": {
"type": [
"string",
"null"
],
"description": "The check number printed on the check"
},
"payer_address": {
"type": [
"string",
"null"
],
"description": "The address of the entity issuing the check"
},
"account_number": {
"type": [
"string",
"null"
],
"description": "The bank account number"
},
"amount_numeric": {
"type": [
"string",
"null"
],
"description": "The numeric dollar amount of the check"
},
"amount_written": {
"type": [
"string",
"null"
],
"description": "The written out dollar amount in words"
},
"routing_number": {
"type": [
"string",
"null"
],
"description": "The bank routing number"
},
"unique_check_id": {
"type": [
"string",
"null"
],
"description": "The unique identifier for verification purposes"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
]
}# Check Processing — Extend AI Skill
## What this pipeline does
This pipeline ingests digitally-printed checks from authorized check writing software and extracts all critical payment information into structured JSON. It parses the check image to markdown using agentic OCR (handling standard and inverted layouts), then extracts 11 fields: check number, routing/account numbers, date, payee, numeric and written amounts, payer details, memo, bank name, and verification ID. The extraction uses `extraction_performance` with review agent enabled for compliance-grade accuracy.
## When to use this
- **Payment processing automation**: Digitizing incoming checks for accounts payable workflows without manual data entry.
- **Check verification workflows**: Validating check authenticity by comparing extracted fields (routing, account, check number) against banking databases.
- **Financial reconciliation**: Matching extracted check amounts and dates to general ledger entries for audit trails.
- **Compliance & retention**: Creating structured, searchable records of check payments for SOX, tax, or legal discovery.
- **Fraud detection**: Flagging mismatches between numeric and written amounts, or unusual payer/payee patterns.
## Processor pipeline
### Step 1: Parse (`parse_performance` + agentic text)
**Purpose**: Convert check image to markdown-structured text preserving layout semantics.
**Config**:
- `engine: "parse_performance"` — optimized for printed documents with reliable text positioning.
- `blockOptions.text.agentic.enabled: true` — enables reasoning over text blocks to resolve ambiguous handwriting or non-standard check layouts (e.g., inverted checks).
- `chunkingStrategy.type: "document"` — keeps the entire check as one coherent chunk rather than fragmenting fields, critical for cross-referencing numeric vs. written amounts.
**Why this config**: Checks are highly structured documents with fixed field positions, but agentic OCR handles edge cases (faded ink, handwritten dates, non-standard layouts) where simple template matching fails. Document-level chunking avoids splitting critical field pairs (e.g., "$1,234.56" and "One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents").
### Step 2: Extract (`extraction_performance` + review agent + advanced multimodal)
**Purpose**: Pull 11 structured fields from the parsed check into JSON.
**Config**:
- `baseProcessor: "extraction_performance"` — accuracy-optimized extractor, slower than `extraction_light` but essential for financial data (routing numbers, account numbers are 9–12 digits and must be 100% correct).
- `advancedOptions.reviewAgent.enabled: true` — a secondary LLM validates extraction output against the parsed markdown, flagging confidence issues. Critical for payment documents where a single digit error blocks reconciliation.
- `advancedOptions.advancedMultimodalEnabled: true` — allows the extractor to reference the original image alongside parsed text, resolving OCR ambiguities (e.g., "l" vs. "1", "O" vs. "0" in routing numbers).
**Why this config**: Financial institutions require >99% accuracy. The review agent catches extraction hallucinations; multimodal grounding prevents digit misreads that would cause payment failures.
---
## TypeScript implementation
---
## CLI equivalent
```bash
#!/bin/bash
# Check Processing Pipeline via CLI
CHECK_IMAGE="$1"
# Step 1: Parse with agentic OCR
echo "📄 Parsing check..."
extend parse "$CHECK_IMAGE" \
--engine "parse_performance" \
--agentic-text-enabled \
--chunking-strategy "document" \
> check_parsed.md
# Step 2: Extract structured fields
echo "🔍 Extracting check fields..."
extend extract "$CHECK_IMAGE" \
--schema check_schema.json \
--base-processor "extraction_performance" \
--review-agent-enabled \
--advanced-multimodal-enabled \
> check_data.json
echo "✅ Check processing complete."
echo "Parsed check: check_parsed.md"
echo "Extracted data: check_data.json"
cat check_data.json | jq .
```
**check_schema.json** (for CLI):
```json
{
"type": "object",
"properties": {
"check_number": {
"type": ["string", "null"],
"description": "The check number printed on the check, typically 6-10 digits in the bottom right corner"
},
"routing_number": {
"type": ["string", "null"],
"description": "The bank routing number (ABA number), a 9-digit code at the bottom left of the check"
},
"account_number": {
"type": ["string", "null"],
"description": "The bank account number, typically 10-12 digits printed at the bottom center of the check"
},
"date": {
"type": ["string", "null"],
"description": "The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)"
},
"payee": {
"type": ["string", "null"],
"description": "The name of the person or entity the check is payable to, written after 'Pay to the order of'"
},
"amount_numeric": {
"type": ["string", "null"],
"description": "The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')"
},
"amount_written": {
"type": ["string", "null"],
"description": "The written out dollar amount in words, usually spanning two lines below the payee"
},
"payer_name": {
"type": ["string", "null"],
"description": "The name of the entity issuing the check, printed at the top left of the check"
},
"payer_address": {
"type": ["string", "null"],
"description": "The address of the entity issuing the check, typically below the payer name in the top left"
},
"memo": {
"type": ["string", "null"],
"description": "The memo or note field text on the check, typically in the bottom left"
},
"bank_name": {
"type": ["string", "null"],
"description": "The name of the bank, usually printed in the top center or top left of the check"
},
"unique_check_id": {
"type": ["string", "null"],
"description": "The unique identifier for verification purposes, may include MICR encoding or security identifiers"
}
}
}
```
---
## Schema
The extraction schema targets 11 fields standard to all printed checks:
```json
{
"type": "object",
"properties": {
"check_number": {
"type": ["string", "null"],
"description": "The check number printed on the check, typically 6-10 digits in the bottom right corner. Critical for matching against bank records and preventing duplicate processing."
},
"routing_number": {
"type": ["string", "null"],
"description": "The bank routing number (ABA number), a 9-digit code at the bottom left of the check, enclosed in special MICR characters. Must be validated against Federal Reserve routing database."
},
"account_number": {
"type": ["string", "null"],
"description": "The bank account number, typically 10-12 digits printed at the bottom center of the check. Used to identify the source account. May contain check digit validation."
},
"date": {
"type": ["string", "null"],
"description": "The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024). Critical for aging analysis and reconciliation windows. May be handwritten."
},
"payee": {
"type": ["string", "null"],
"description": "The name of the person or entity the check is payable to, written after 'Pay to the order of'. May include business suffixes (LLC, Inc., etc.) or 'Bearer' for blank checksimport { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
/**
* Check Processing Pipeline
*
* Ingests a check image, parses it to markdown with agentic OCR,
* then extracts 11 critical payment fields into structured JSON.
*
* This implementation uses:
* - parse_performance + agentic text for layout-aware parsing
* - extraction_performance + review agent + multimodal for compliance-grade accuracy
*/
const client = new ExtendClient({
token: process.env.EXTEND_API_KEY,
});
// Define the check extraction schema using Zod
// Each field is nullable (cards may be missing some data)
// Descriptions are detailed to maximize extraction accuracy
const checkSchema = z.object({
check_number: z
.string()
.nullable()
.describe(
"The check number printed on the check, typically 6-10 digits in the bottom right corner"
),
routing_number: z
.string()
.nullable()
.describe(
"The bank routing number (ABA number), a 9-digit code at the bottom left of the check"
),
account_number: z
.string()
.nullable()
.describe(
"The bank account number, typically 10-12 digits printed at the bottom center of the check"
),
date: z
.string()
.nullable()
.describe(
"The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)"
),
payee: z
.string()
.nullable()
.describe(
"The name of the person or entity the check is payable to, written after 'Pay to the order of'"
),
amount_numeric: z
.string()
.nullable()
.describe(
"The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')"
),
amount_written: z
.string()
.nullable()
.describe(
"The written out dollar amount in words, usually spanning two lines below the payee (e.g., 'One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents')"
),
payer_name: z
.string()
.nullable()
.describe(
"The name of the entity issuing the check, printed at the top left of the check"
),
payer_address: z
.string()
.nullable()
.describe(
"The address of the entity issuing the check, typically below the payer name in the top left"
),
memo: z
.string()
.nullable()
.describe(
"The memo or note field text on the check, typically in the bottom left (may reference invoice numbers or payment purpose)"
),
bank_name: z
.string()
.nullable()
.describe(
"The name of the bank, usually printed in the top center or top left of the check"
),
unique_check_id: z
.string()
.nullable()
.describe(
"The unique identifier for verification purposes, may include MICR encoding or additional security identifiers"
),
});
/**
* Main processing function
* @param filePath - Local path to the check image (PDF, PNG, JPG, etc.)
* @returns Extracted check data as JSON
*/
export async function processCheck(filePath: string) {
console.log(`\n=== Check Processing Pipeline ===`);
console.log(`File: ${filePath}\n`);
// Step 1: Convert local file to data URL
// (The Extend SDK requires file URLs, not ReadStreams)
const fileBuffer = fs.readFileSync(filePath);
const base64Data = fileBuffer.toString("base64");
const fileUrl = `data:application/octet-stream;base64,${base64Data}`;
// Step 2: Parse the check
// Uses parse_performance + agentic OCR to handle layouts and handwriting
console.log("📄 Step 1: Parsing check with agentic OCR...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: fileUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: true,
},
},
},
chunkingStrategy: {
type: "document",
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
const parsedText = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(
`✓ Parse complete. Extracted ${parseRun.output.chunks.length} chunk(s).\n`
);
// Step 3: Extract structured fields
// Uses extraction_performance + review agent + multimodal for accuracy
console.log("🔍 Step 2: Extracting check fields...");
const extractRun = await client.extractRuns.createAndPoll({
file: { url: fileUrl },
config: {
schema: checkSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const checkData = extractRun.output.value;
console.log(`✓ Extraction complete.\n`);
// Step 4: Output results
console.log("=== Extracted Check Data ===\n");
console.log(JSON.stringify(checkData, null, 2));
// Validation: flag potential issues
console.log("\n=== Validation Checks ===");
if (!checkData.check_number) {
console.warn("⚠ Check number not found");
}
if (!checkData.routing_number) {
console.warn("⚠ Routing number not found");
}
if (!checkData.account_number) {
console.warn("⚠ Account number not found");
}
if (!checkData.payee) {
console.warn("⚠ Payee not found");
}
if (!checkData.amount_numeric) {
console.warn("⚠ Numeric amount not found");
}
if (
checkData.amount_numeric &&
checkData.amount_written &&
!amountsMatch(checkData.amount_numeric, checkData.amount_written)
) {
console.warn(
`⚠ Amount mismatch: numeric='${checkData.amount_numeric}' vs written='${checkData.amount_written}'`
);
}
if (!checkData.date) {
console.warn("⚠ Date not found");
}
console.log("\n✅ Check processing complete.\n");
return checkData;
}
/**
* Helper: Rough check if numeric and written amounts match
* (Does not validate currency format; just detects obvious mismatches)
*/
function amountsMatch(numeric: string, written: string): boolean {
// Extract all digits from numeric amount (e.g., "$1,234.56" → "123456")
const numericDigits = numeric.replace(/\D/g, "");
// Extract all digits from written amount (e.g., "One Thousand..." → "1000")
const writtenDigits = written.replace(/\D/g, "");
// Simple heuristic: both should contain similar digit patterns
// In production, use a proper currency parser or banking library
return (
numericDigits.length > 0 &&
writtenDigits.length > 0 &&
numericDigits.length === writtenDigits.length
);
}
// Auto-invoke if run directly
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: npx ts-node solution.ts <path-to-check-image>");
process.exit(1);
}
processCheck(args[0]).catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});import os
import sys
import base64
import json
from typing import Optional
from extend_ai import Extend
# Initialize the Extend client
client = Extend(token=os.environ["EXTEND_API_KEY"])
# Define the check extraction schema as a dictionary
# Each field is nullable (checks may be missing some data)
check_schema = {
"type": "object",
"properties": {
"check_number": {
"type": ["string", "null"],
"description": "The check number printed on the check, typically 6-10 digits in the bottom right corner",
},
"routing_number": {
"type": ["string", "null"],
"description": "The bank routing number (ABA number), a 9-digit code at the bottom left of the check",
},
"account_number": {
"type": ["string", "null"],
"description": "The bank account number, typically 10-12 digits printed at the bottom center of the check",
},
"date": {
"type": ["string", "null"],
"description": "The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)",
},
"payee": {
"type": ["string", "null"],
"description": "The name of the person or entity the check is payable to, written after 'Pay to the order of'",
},
"amount_numeric": {
"type": ["string", "null"],
"description": "The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')",
},
"amount_written": {
"type": ["string", "null"],
"description": "The written out dollar amount in words, usually spanning two lines below the payee (e.g., 'One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents')",
},
"payer_name": {
"type": ["string", "null"],
"description": "The name of the entity issuing the check, printed at the top left of the check",
},
"payer_address": {
"type": ["string", "null"],
"description": "The address of the entity issuing the check, typically below the payer name in the top left",
},
"memo": {
"type": ["string", "null"],
"description": "The memo or note field text on the check, typically in the bottom left (may reference invoice numbers or payment purpose)",
},
"bank_name": {
"type": ["string", "null"],
"description": "The name of the bank, usually printed in the top center or top left of the check",
},
"unique_check_id": {
"type": ["string", "null"],
"description": "The unique identifier for verification purposes, may include MICR encoding or additional security identifiers",
},
},
}
def amounts_match(numeric: str, written: str) -> bool:
"""
Helper: Rough check if numeric and written amounts match.
(Does not validate currency format; just detects obvious mismatches)
"""
# Extract all digits from numeric amount (e.g., "$1,234.56" → "123456")
numeric_digits = "".join(c for c in numeric if c.isdigit())
# Extract all digits from written amount (e.g., "One Thousand..." → "1000")
written_digits = "".join(c for c in written if c.isdigit())
# Simple heuristic: both should contain similar digit patterns
# In production, use a proper currency parser or banking library
return (
len(numeric_digits) > 0
and len(written_digits) > 0
and len(numeric_digits) == len(written_digits)
)
async def process_check(file_path: str) -> dict:
"""
Main processing function.
Args:
file_path: Local path to the check image (PDF, PNG, JPG, etc.)
Returns:
Extracted check data as a dictionary
"""
print(f"\n=== Check Processing Pipeline ===")
print(f"File: {file_path}\n")
# Step 1: Convert local file to data URL
# (The Extend SDK requires file URLs, not file streams)
with open(file_path, "rb") as f:
file_buffer = f.read()
base64_data = base64.b64encode(file_buffer).decode("utf-8")
file_url = f"data:application/octet-stream;base64,{base64_data}"
# Step 2: Parse the check
# Uses parse_performance + agentic OCR to handle layouts and handwriting
print("📄 Step 1: Parsing check with agentic OCR...")
parse_run = await client.parse_runs.create_and_poll(
file={"url": file_url},
config={
"blockOptions": {
"text": {
"agentic": {
"enabled": True,
},
},
},
"chunkingStrategy": {
"type": "document",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
parsed_text = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
print(
f"✓ Parse complete. Extracted {len(parse_run.output.chunks)} chunk(s).\n"
)
# Step 3: Extract structured fields
# Uses extraction_performance + review agent + multimodal for accuracy
print("🔍 Step 2: Extracting check fields...")
extract_run = await client.extract_runs.create_and_poll(
file={"url": file_url},
config={
"schema": check_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
check_data = extract_run.output.value
print("✓ Extraction complete.\n")
# Step 4: Output results
print("=== Extracted Check Data ===\n")
print(json.dumps(check_data, indent=2))
# Validation: flag potential issues
print("\n=== Validation Checks ===")
if not check_data.get("check_number"):
print("⚠ Check number not found")
if not check_data.get("routing_number"):
print("⚠ Routing number not found")
if not check_data.get("account_number"):
print("⚠ Account number not found")
if not check_data.get("payee"):
print("⚠ Payee not found")
if not check_data.get("amount_numeric"):
print("⚠ Numeric amount not found")
if (
check_data.get("amount_numeric")
and check_data.get("amount_written")
and not amounts_match(
check_data.get("amount_numeric"), check_data.get("amount_written")
)
):
print(
f"⚠ Amount mismatch: numeric='{check_data.get('amount_numeric')}' vs written='{check_data.get('amount_written')}'"
)
if not check_data.get("date"):
print("⚠ Date not found")
print("\n✅ Check processing complete.\n")
return check_data
# Auto-invoke if run directly
if __name__ == "__main__":
import asyncio
if len(sys.argv) < 2:
print("Usage: python solution.py <path-to-check-image>")
sys.exit(1)
try:
asyncio.run(process_check(sys.argv[1]))
except Exception as err:
print(f"Error: {str(err)}")
sys.exit(1)import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
/**
* Check Processing Pipeline
*
* This code uses Extend's REST API directly via HttpClient (no official Java SDK exists yet).
* Ingests a check image, parses it to markdown with agentic OCR,
* then extracts 11 critical payment fields into structured JSON.
*
* This implementation uses:
* - parse_performance + agentic text for layout-aware parsing
* - extraction_performance + review agent + multimodal for compliance-grade accuracy
*/
public class CheckProcessor {
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();
/**
* Check extraction schema as a JSON object
*/
private static Map<String, Object> getCheckSchema() {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>();
String[] fields = {
"check_number", "routing_number", "account_number", "date", "payee",
"amount_numeric", "amount_written", "payer_name", "payer_address",
"memo", "bank_name", "unique_check_id"
};
String[] descriptions = {
"The check number printed on the check, typically 6-10 digits in the bottom right corner",
"The bank routing number (ABA number), a 9-digit code at the bottom left of the check",
"The bank account number, typically 10-12 digits printed at the bottom center of the check",
"The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)",
"The name of the person or entity the check is payable to, written after 'Pay to the order of'",
"The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')",
"The written out dollar amount in words, usually spanning two lines below the payee",
"The name of the entity issuing the check, printed at the top left of the check",
"The address of the entity issuing the check, typically below the payer name in the top left",
"The memo or note field text on the check, typically in the bottom left",
"The name of the bank, usually printed in the top center or top left of the check",
"The unique identifier for verification purposes, may include MICR encoding"
};
for (int i = 0; i < fields.length; i++) {
Map<String, Object> field = new HashMap<>();
field.put("type", new String[]{"string", "null"});
field.put("description", descriptions[i]);
properties.put(fields[i], field);
}
schema.put("properties", properties);
return schema;
}
/**
* Helper: Convert Java map to minimal JSON string (no external JSON library)
*/
private static String toJsonString(Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(entry.getKey()).append("\":");
Object value = entry.getValue();
if (value == null) {
sb.append("null");
} else if (value instanceof String) {
sb.append("\"").append(((String) value).replace("\"", "\\\"")).append("\"");
} else if (value instanceof Map) {
sb.append(toJsonString((Map<String, Object>) value));
} else if (value instanceof List) {
sb.append("[");
boolean firstItem = true;
for (Object item : (List<?>) value) {
if (!firstItem) sb.append(",");
if (item instanceof String) {
sb.append("\"").append(item).append("\"");
} else {
sb.append(item);
}
firstItem = false;
}
sb.append("]");
} else {
sb.append(value);
}
first = false;
}
sb.append("}");
return sb.toString();
}
/**
* Parse a check image using agentic OCR
*/
private static String parseCheck(String fileUrl) throws Exception {
System.out.println("📄 Step 1: Parsing check with agentic OCR...");
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);
Map<String, Object> chunkingStrategy = new HashMap<>();
chunkingStrategy.put("type", "document");
Map<String, Object> config = new HashMap<>();
config.put("blockOptions", blockOptions);
config.put("chunkingStrategy", chunkingStrategy);
Map<String, Object> file = new HashMap<>();
file.put("url", fileUrl);
Map<String, Object> body = new HashMap<>();
body.put("file", file);
body.put("config", config);
String requestBody = toJsonString(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(API_BASE + "/v1/parse_runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException("Parse request failed: " + response.statusCode() + " " + response.body());
}
String runId = extractFieldFromJson(response.body(), "id");
// Poll for completion
while (true) {
Thread.sleep(2000);
HttpRequest pollRequest = HttpRequest.newBuilder()
.uri(new URI(API_BASE + "/v1/parse_runs/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> pollResponse = httpClient.send(pollRequest, HttpResponse.BodyHandlers.ofString());
String status = extractFieldFromJson(pollResponse.body(), "status");
if ("PROCESSED".equals(status)) {
System.out.println("✓ Parse complete.\n");
return pollResponse.body();
} else if ("FAILED".equals(status)) {
throw new RuntimeException("Parse run failed");
}
}
}
/**
* Extract check fields using structured extraction
*/
private static Map<String, Object> extractCheck(String fileUrl) throws Exception {
System.out.println("🔍 Step 2: Extracting check fields...");
Map<String, Object> file = new HashMap<>();
file.put("url", fileUrl);
Map<String, Object> config = new HashMap<>();
config.put("schema", getCheckSchema());
Map<String, Object> body = new HashMap<>();
body.put("file", file);
body.put("config", config);
String requestBody = toJsonString(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(API_BASE + "/v1/extract_runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException("Extract request failed: " + response.statusCode() + " " + response.body());
}
String runId = extractFieldFromJson(response.body(), "id");
// Poll for completion
while (true) {
Thread.sleep(2000);
HttpRequest pollRequest = HttpRequest.newBuilder()
.uri(new URI(API_BASE + "/v1/extract_runs/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> pollResponse = httpClient.send(pollRequest, HttpResponse.BodyHandlers.ofString());
String status = extractFieldFromJson(pollResponse.body(), "status");
if ("PROCESSED".equals(status)) {
System.out.println("✓ Extraction complete.\n");
return parseExtractedData(pollResponse.body());
} else if ("FAILED".equals(status)) {
throw new RuntimeException("Extract run failed");
}
}
}
/**
* Helper: Extract a field from JSON response (simple string matching)
*/
private static String extractFieldFromJson(String json, String fieldName) {
String pattern = "\"" + fieldName + "\":\"";
int start = json.indexOf(pattern);
if (start == -1) {
pattern = "\"" + fieldName + "\":";
start = json.indexOf(pattern);
if (start == -1) return null;
start += pattern.length();
int end = json.indexOf(",", start);
if (end == -1) end = json.indexOf("}", start);
return json.substring(start, end).trim();
}
start += pattern.length();
int end = json.indexOf("\"", start);
return json.substring(start, end);
}
/**
* Helper: Parse extracted check data from response
*/
private static Map<String, Object> parseExtractedData(String json) {
Map<String, Object> result = new HashMap<>();
String[] fields = {
"check_number", "routing_number", "account_number", "date", "payee",
"amount_numeric", "amount_written", "payer_name", "payer_address",
"memo", "bank_name", "unique_check_id"
};
for (String field : fields) {
String pattern = "\"" + field + "\":";
int start = json.indexOf(pattern);
if (start != -1) {
start += pattern.length();
if (json.charAt(start) == 'n') {
result.put(field, null);
} else if (json.charAt(start) == '"') {
start++;
int end = json.indexOf("\"", start);
result.put(field, json.substring(start, end));
}
}
}
return result;
}
/**
* Helper: Check if numeric and written amounts match
*/
private static boolean amountsMatch(String numeric, String written) {
if (numeric == null || written == null) return false;
String numericDigits = numeric.replaceAll("[^0-9]", "");
String writtenDigits = written.replaceAll("[^0-9]", "");
return numericDigits.length() > 0 &&
writtenDigits.length() > 0 &&
numericDigits.length() == writtenDigits.length();
}
/**
* Main processing function
*/
public static void processCheck(String filePath) throws Exception {
System.out.println("\n=== Check Processing Pipeline ===");
System.out.println("File: " + filePath + "\n");
// Step 1: Convert local file to data URL
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64Data = Base64.getEncoder().encodeToString(fileBytes);
String fileUrl = "data:application/octet-stream;base64," + base64Data;
// Step 2: Parse the check
parseCheck(fileUrl);
// Step 3: Extract structured fields
Map<String, Object> checkData = extractCheck(fileUrl);
// Step 4: Output results
System.out.println("=== Extracted Check Data ===\n");
for (Map.Entry<String, Object> entry : checkData.entrySet()) {
System.out.println(" \"" + entry.getKey() + "\": " +
(entry.getValue() == null ? "null" : "\"" + entry.getValue() + "\""));
}
// Validation: flag potential issues
System.out.println("\n=== Validation Checks ===");
if (checkData.get("check_number") == null) {
System.out.println("⚠ Check number not found");
}
if (checkData.get("routing_number") == null) {
System.out.println("⚠ Routing number not found");
}
if (checkData.get("account_number") == null) {
System.out.println("⚠ Account number not found");
}
if (checkData.get("payee") == null) {
System.out.println("⚠ Payee not found");
}
if (checkData.get("amount_numeric") == null) {
System.out.println("⚠ Numeric amount not found");
}
if (checkData.get("amount_numeric") != null && checkData.get("amount_written") != null &&
!amountsMatch((String) checkData.get("amount_numeric"),
(String) checkData.get("amount_written"))) {
System.out.println("⚠ Amount mismatch: numeric='" + checkData.get("amount_numeric") +
"' vs written='" + checkData.get("amount_written") + "'");
}
if (checkData.get("date") == null) {
System.out.println("⚠ Date not found");
}
System.out.println("\n✅ Check processing complete.\n");
}
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Usage: java CheckProcessor <path-to-check-image>");
System.exit(1);
}
try {
processCheck(args[0]);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
"time"
)
const extendBaseURL = "https://api.extend.ai"
type CheckData struct {
CheckNumber *string `json:"check_number"`
RoutingNumber *string `json:"routing_number"`
AccountNumber *string `json:"account_number"`
Date *string `json:"date"`
Payee *string `json:"payee"`
AmountNumeric *string `json:"amount_numeric"`
AmountWritten *string `json:"amount_written"`
PayerName *string `json:"payer_name"`
PayerAddress *string `json:"payer_address"`
Memo *string `json:"memo"`
BankName *string `json:"bank_name"`
UniqueCheckID *string `json:"unique_check_id"`
}
type ParseRunResponse struct {
Status string `json:"status"`
Output struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
} `json:"output"`
}
type ExtractRunResponse struct {
Status string `json:"status"`
Output struct {
Value CheckData `json:"value"`
} `json:"output"`
}
func getAPIKey() string {
key := os.Getenv("EXTEND_API_KEY")
if key == "" {
fmt.Fprintf(os.Stderr, "Error: EXTEND_API_KEY environment variable not set\n")
os.Exit(1)
}
return key
}
func fileToDataURL(filePath string) (string, error) {
fileBuffer, err := ioutil.ReadFile(filePath)
if err != nil {
return "", err
}
base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
return fmt.Sprintf("data:application/octet-stream;base64,%s", base64Data), nil
}
func createAndPollParseRun(fileURL string, apiKey string) (*ParseRunResponse, error) {
reqBody := map[string]interface{}{
"file": map[string]string{
"url": fileURL,
},
"config": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]bool{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]string{
"type": "document",
},
},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", extendBaseURL+"/v1/parseRuns", bytes.NewBuffer(bodyBytes))
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, err
}
defer resp.Body.Close()
var createResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&createResp)
runID := createResp["id"].(string)
// Poll for completion
for {
time.Sleep(2 * time.Second)
pollReq, _ := http.NewRequest("GET", extendBaseURL+fmt.Sprintf("/v1/parseRuns/%s", runID), nil)
pollReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
pollResp, _ := client.Do(pollReq)
var pollResult ParseRunResponse
json.NewDecoder(pollResp.Body).Decode(&pollResult)
pollResp.Body.Close()
if pollResult.Status == "PROCESSED" || pollResult.Status == "FAILED" {
return &pollResult, nil
}
}
}
func createAndPollExtractRun(fileURL string, apiKey string) (*ExtractRunResponse, error) {
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"check_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The check number printed on the check, typically 6-10 digits in the bottom right corner",
},
"routing_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The bank routing number (ABA number), a 9-digit code at the bottom left of the check",
},
"account_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The bank account number, typically 10-12 digits printed at the bottom center of the check",
},
"date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)",
},
"payee": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the person or entity the check is payable to, written after 'Pay to the order of'",
},
"amount_numeric": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')",
},
"amount_written": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The written out dollar amount in words, usually spanning two lines below the payee",
},
"payer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the entity issuing the check, printed at the top left of the check",
},
"payer_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The address of the entity issuing the check, typically below the payer name in the top left",
},
"memo": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The memo or note field text on the check, typically in the bottom left",
},
"bank_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the bank, usually printed in the top center or top left of the check",
},
"unique_check_id": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The unique identifier for verification purposes, may include MICR encoding or additional security identifiers",
},
},
}
reqBody := map[string]interface{}{
"file": map[string]string{
"url": fileURL,
},
"config": map[string]interface{}{
"schema": schema,
},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", extendBaseURL+"/v1/extractRuns", bytes.NewBuffer(bodyBytes))
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, err
}
defer resp.Body.Close()
var createResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&createResp)
runID := createResp["id"].(string)
// Poll for completion
for {
time.Sleep(2 * time.Second)
pollReq, _ := http.NewRequest("GET", extendBaseURL+fmt.Sprintf("/v1/extractRuns/%s", runID), nil)
pollReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
pollResp, _ := client.Do(pollReq)
var pollResult ExtractRunResponse
json.NewDecoder(pollResp.Body).Decode(&pollResult)
pollResp.Body.Close()
if pollResult.Status == "PROCESSED" || pollResult.Status == "FAILED" {
return &pollResult, nil
}
}
}
func amountsMatch(numeric, written string) bool {
re := regexp.MustCompile(`\D`)
numericDigits := re.ReplaceAllString(numeric, "")
writtenDigits := re.ReplaceAllString(written, "")
return len(numericDigits) > 0 && len(writtenDigits) > 0 && len(numericDigits) == len(writtenDigits)
}
func processCheck(filePath string) (*CheckData, error) {
fmt.Println("\n=== Check Processing Pipeline ===")
fmt.Printf("File: %s\n\n", filePath)
apiKey := getAPIKey()
// Convert local file to data URL
fileURL, err := fileToDataURL(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
// Parse the check
fmt.Println("📄 Step 1: Parsing check with agentic OCR...")
parseRun, err := createAndPollParseRun(fileURL, apiKey)
if err != nil {
return nil, fmt.Errorf("parse request failed: %w", err)
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
var parsedText strings.Builder
for i, chunk := range parseRun.Output.Chunks {
if i > 0 {
parsedText.WriteString("\n\n")
}
parsedText.WriteString(chunk.Content)
}
fmt.Printf("✓ Parse complete. Extracted %d chunk(s).\n\n", len(parseRun.Output.Chunks))
// Extract structured fields
fmt.Println("🔍 Step 2: Extracting check fields...")
extractRun, err := createAndPollExtractRun(fileURL, apiKey)
if err != nil {
return nil, fmt.Errorf("extract request failed: %w", err)
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
}
checkData := &extractRun.Output.Value
fmt.Println("✓ Extraction complete.\n")
// Output results
fmt.Println("=== Extracted Check Data ===\n")
jsonData, _ := json.MarshalIndent(checkData, "", " ")
fmt.Println(string(jsonData))
// Validation checks
fmt.Println("\n=== Validation Checks ===")
if checkData.CheckNumber == nil {
fmt.Println("⚠ Check number not found")
}
if checkData.RoutingNumber == nil {
fmt.Println("⚠ Routing number not found")
}
if checkData.AccountNumber == nil {
fmt.Println("⚠ Account number not found")
}
if checkData.Payee == nil {
fmt.Println("⚠ Payee not found")
}
if checkData.AmountNumeric == nil {
fmt.Println("⚠ Numeric amount not found")
}
if checkData.AmountNumeric != nil && checkData.AmountWritten != nil &&
!amountsMatch(*checkData.AmountNumeric, *checkData.AmountWritten) {
fmt.Printf("⚠ Amount mismatch: numeric='%s' vs written='%s'\n",
*checkData.AmountNumeric, *checkData.AmountWritten)
}
if checkData.Date == nil {
fmt.Println("⚠ Date not found")
}
fmt.Println("\n✅ Check processing complete.\n")
return checkData, nil
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <path-to-check-image>\n", os.Args[0])
os.Exit(1)
}
_, err := processCheck(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}// Deploy the "Check" 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/check.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: check).
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, "check.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": "Check 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": {
"date": {
"type": [
"string",
"null"
],
"description": "The date the check was issued (MM/DD/YYYY format)"
},
"memo": {
"type": [
"string",
"null"
],
"description": "The memo field text on the check"
},
"payee": {
"type": [
"string",
"null"
],
"description": "The name of the person or entity the check is payable to"
},
"bank_name": {
"type": [
"string",
"null"
],
"description": "The name of the bank"
},
"payer_name": {
"type": [
"string",
"null"
],
"description": "The name of the entity issuing the check"
},
"check_number": {
"type": [
"string",
"null"
],
"description": "The check number printed on the check"
},
"payer_address": {
"type": [
"string",
"null"
],
"description": "The address of the entity issuing the check"
},
"account_number": {
"type": [
"string",
"null"
],
"description": "The bank account number"
},
"amount_numeric": {
"type": [
"string",
"null"
],
"description": "The numeric dollar amount of the check"
},
"amount_written": {
"type": [
"string",
"null"
],
"description": "The written out dollar amount in words"
},
"routing_number": {
"type": [
"string",
"null"
],
"description": "The bank routing number"
},
"unique_check_id": {
"type": [
"string",
"null"
],
"description": "The unique identifier for verification purposes"
}
}
},
"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 json
import os
import sys
from pathlib import Path
import requests
API = "https://api.extend.ai"
VERSION = "2026-02-09"
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 / "check.json"
def load_state() -> dict:
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {}
def save_state(state: dict) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
def api(method: str, path_name: str, body: dict | None = None) -> dict:
url = API + path_name
headers = {
"Authorization": f"Bearer {API_KEY}",
"x-extend-api-version": VERSION,
}
if body is not None:
headers["Content-Type"] = "application/json"
res = requests.request(
method,
url,
headers=headers,
json=body,
)
try:
data = res.json()
except requests.exceptions.JSONDecodeError:
data = {}
if not res.ok:
error_msg = json.dumps(data)[:300]
raise RuntimeError(f"{method} {path_name} failed ({res.status_code}): {error_msg}")
return data
WORKFLOW = {
"name": "Check 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": {
"date": {
"type": ["string", "null"],
"description": "The date the check was issued (MM/DD/YYYY format)",
},
"memo": {
"type": ["string", "null"],
"description": "The memo field text on the check",
},
"payee": {
"type": ["string", "null"],
"description": "The name of the person or entity the check is payable to",
},
"bank_name": {
"type": ["string", "null"],
"description": "The name of the bank",
},
"payer_name": {
"type": ["string", "null"],
"description": "The name of the entity issuing the check",
},
"check_number": {
"type": ["string", "null"],
"description": "The check number printed on the check",
},
"payer_address": {
"type": ["string", "null"],
"description": "The address of the entity issuing the check",
},
"account_number": {
"type": ["string", "null"],
"description": "The bank account number",
},
"amount_numeric": {
"type": ["string", "null"],
"description": "The numeric dollar amount of the check",
},
"amount_written": {
"type": ["string", "null"],
"description": "The written out dollar amount in words",
},
"routing_number": {
"type": ["string", "null"],
"description": "The bank routing number",
},
"unique_check_id": {
"type": ["string", "null"],
"description": "The unique identifier for verification purposes",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {"enabled": True},
"advancedMultimodalEnabled": True,
},
}
},
},
],
}
def main() -> None:
state = load_state()
print(f'Deploying "{WORKFLOW["name"]}"\…')
if state.get("workflowId"):
print(f'✓ workflow already provisioned ({state["workflowId"]}) — updating steps')
api("POST", f'/workflows/{state["workflowId"]}', {"steps": WORKFLOW["steps"]})
else:
try:
list_response = api("GET", f'/workflows?name={requests.utils.quote(WORKFLOW["name"])}')
items = list_response.get("data") or list_response.get("items") or []
existing = next(
(item for item in items if item.get("name") == WORKFLOW["name"]),
None,
)
if existing and existing.get("id"):
state["workflowId"] = existing["id"]
save_state(state)
print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing["id"]}) — updating steps')
api("POST", f'/workflows/{existing["id"]}', {"steps": WORKFLOW["steps"]})
except Exception:
pass
if not state.get("workflowId"):
created = api("POST", "/workflows", WORKFLOW)
wf_id = created.get("id") or created.get("workflow", {}).get("id")
if not wf_id:
raise RuntimeError("Could not read created workflow id from response")
state["workflowId"] = wf_id
save_state(state)
print(f"+ created workflow ({wf_id})")
try:
api("POST", f'/workflows/{state["workflowId"]}/versions', {})
except Exception:
pass
print("\nDone. Run documents through it with:")
print(f' POST {API}/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__":
try:
main()
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)// Extend REST API client — calls Extend's REST API directly since there is no official Java SDK yet.
// Deploy the "Check" 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/check.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: check).
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.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 = Path.of(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("check.json");
private static final HttpClient HTTP = HttpClient.newHttpClient();
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
private static class State {
String workflowId;
}
private static State state = new State();
static {
try {
if (Files.exists(STATE_FILE)) {
String json = Files.readString(STATE_FILE);
state = jsonParseState(json);
}
} catch (IOException e) {
// Ignore; start with empty state
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
String json = jsonStringifyState(state);
Files.writeString(STATE_FILE, json);
}
private static String jsonStringifyState(State s) {
StringBuilder sb = new StringBuilder();
sb.append("{\n");
if (s.workflowId != null) {
sb.append(" \"workflowId\": \"").append(escape(s.workflowId)).append("\"\n");
}
sb.append("}");
return sb.toString();
}
private static State jsonParseState(String json) {
State s = new State();
if (json.contains("\"workflowId\"")) {
int start = json.indexOf("\"workflowId\"") + 13;
while (start < json.length() && json.charAt(start) != '"') start++;
start++;
int end = start;
while (end < json.length() && json.charAt(end) != '"') end++;
s.workflowId = json.substring(start, end);
}
return s;
}
private static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
private static Map<String, Object> api(String method, String pathName, Map<String, Object> body)
throws IOException, InterruptedException {
String url = API + pathName;
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
.method(method, body != null
? HttpRequest.BodyPublishers.ofString(jsonStringify(body))
: HttpRequest.BodyPublishers.noBody())
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
builder.header("Content-Type", "application/json");
}
HttpRequest req = builder.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = new HashMap<>();
try {
data = jsonParse(res.body());
} catch (Exception e) {
// Empty map on parse error
}
if (res.statusCode() < 200 || res.statusCode() >= 300) {
String detail = jsonStringify(data);
if (detail.length() > 300) detail = detail.substring(0, 300);
throw new IOException(method + " " + pathName + " failed (" + res.statusCode() + "): " + detail);
}
return data;
}
private static String jsonStringify(Object obj) {
if (obj == null) return "null";
if (obj instanceof String) return "\"" + escape((String) obj) + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof Boolean) return obj.toString();
if (obj instanceof Map) {
Map<String, Object> m = (Map<String, Object>) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> e : m.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(escape(e.getKey())).append("\":");
sb.append(jsonStringify(e.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
if (obj instanceof List) {
List<Object> l = (List<Object>) obj;
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : l) {
if (!first) sb.append(",");
sb.append(jsonStringify(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return obj.toString();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> jsonParse(String json) {
Map<String, Object> result = new HashMap<>();
json = json.trim();
if (!json.startsWith("{")) return result;
json = json.substring(1, json.length() - 1).trim();
if (json.isEmpty()) return result;
int depth = 0;
StringBuilder key = new StringBuilder();
StringBuilder value = new StringBuilder();
boolean inString = false;
boolean inKey = 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 (inKey) key.append(c);
else value.append(c);
continue;
}
if (!inString) {
if (c == ':' && depth == 0) {
inKey = false;
value = new StringBuilder();
continue;
} else if (c == ',' && depth == 0) {
String k = key.toString().trim().replaceAll("^\"|\"$", "");
String v = value.toString().trim();
result.put(k, parseValue(v));
key = new StringBuilder();
value = new StringBuilder();
inKey = true;
continue;
} else if (c == '{' || c == '[') {
depth++;
} else if (c == '}' || c == ']') {
depth--;
}
}
if (inKey) key.append(c);
else value.append(c);
}
if (key.length() > 0) {
String k = key.toString().trim().replaceAll("^\"|\"$", "");
String v = value.toString().trim();
result.put(k, parseValue(v));
}
return result;
}
private static Object parseValue(String v) {
v = v.trim();
if (v.equals("null")) return null;
if (v.equals("true")) return true;
if (v.equals("false")) return false;
if (v.startsWith("\"") && v.endsWith("\"")) return v.substring(1, v.length() - 1).replace("\\\"", "\"");
if (v.matches("-?\\d+(\\.\\d+)?")) {
try {
if (v.contains(".")) return Double.parseDouble(v);
return Long.parseLong(v);
} catch (NumberFormatException e) {
return v;
}
}
if (v.startsWith("{") && v.endsWith("}")) return jsonParse(v);
if (v.startsWith("[") && v.endsWith("]")) {
List<Object> list = new ArrayList<>();
String inner = v.substring(1, v.length() - 1).trim();
if (!inner.isEmpty()) {
int depth = 0;
StringBuilder item = new StringBuilder();
boolean inString = false;
for (int i = 0; i < inner.length(); i++) {
char c = inner.charAt(i);
if (c == '"' && (i == 0 || inner.charAt(i - 1) != '\\')) {
inString = !inString;
item.append(c);
} else if (!inString && (c == '{' || c == '[')) {
depth++;
item.append(c);
} else if (!inString && (c == '}' || c == ']')) {
depth--;
item.append(c);
} else if (!inString && c == ',' && depth == 0) {
list.add(parseValue(item.toString()));
item = new StringBuilder();
} else {
item.append(c);
}
}
if (item.length() > 0) list.add(parseValue(item.toString()));
}
return list;
}
return v;
}
private static Map<String, Object> createWorkflow() {
Map<String, Object> trigger = new LinkedHashMap<>();
trigger.put("name", "startTrigger1");
trigger.put("type", "TRIGGER");
List<Map<String, String>> triggerNext = new ArrayList<>();
Map<String, String> triggerNextStep = new HashMap<>();
triggerNextStep.put("step", "parse1");
triggerNext.add(triggerNextStep);
trigger.put("next", triggerNext);
Map<String, Object> parseConfig = new LinkedHashMap<>();
Map<String, Object> blockOptions = new LinkedHashMap<>();
Map<String, Object> textBlock = new LinkedHashMap<>();
Map<String, Object> agenticBlock = new LinkedHashMap<>();
agenticBlock.put("enabled", true);
textBlock.put("agentic", agenticBlock);
blockOptions.put("text", textBlock);
Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
chunkingStrategy.put("type", "document");
parseConfig.put("blockOptions", blockOptions);
parseConfig.put("chunkingStrategy", chunkingStrategy);
Map<String, Object> parse = new LinkedHashMap<>();
parse.put("name", "parse1");
parse.put("type", "PARSE");
Map<String, Object> parseConfigWrapper = new HashMap<>();
parseConfigWrapper.put("parseConfig", parseConfig);
parse.put("config", parseConfigWrapper);
List<Map<String, String>> parseNext = new ArrayList<>();
Map<String, String> parseNextStep = new HashMap<>();
parseNextStep.put("step", "extraction2");
parseNext.add(parseNextStep);
parse.put("next", parseNext);
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("date",
createProp("The date the check was issued (MM/DD/YYYY format)"));
properties.put("memo", createProp("The memo field text on the check"));
properties.put("payee", createProp("The name of the person or entity the check is payable to"));
properties.put("bank_name", createProp("The name of the bank"));
properties.put("payer_name", createProp("The name of the entity issuing the check"));
properties.put("check_number", createProp("The check number printed on the check"));
properties.put("payer_address", createProp("The address of the entity issuing the check"));
properties.put("account_number", createProp("The bank account number"));
properties.put("amount_numeric", createProp("The numeric dollar amount of the check"));
properties.put("amount_written", createProp("The written out dollar amount in words"));
properties.put("routing_number", createProp("The bank routing number"));
properties.put("unique_check_id", createProp("The unique identifier for verification purposes"));
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 HashMap<>();
reviewAgent.put("enabled", true);
advancedOptions.put("reviewAgent", reviewAgent);
advancedOptions.put("advancedMultimodalEnabled", true);
extractorConfig.put("advancedOptions", advancedOptions);
Map<String, Object> extraction = new LinkedHashMap<>();
extraction.put("name", "extraction2");
extraction.put("type", "EXTRACT");
Map<String, Object> extractionConfigWrapper = new HashMap<>();
extractionConfigWrapper.put("extractorConfig", extractorConfig);
extraction.put("config", extractionConfigWrapper);
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Check Processing Pipeline");
List<Map<String, Object>> steps = new ArrayList<>();
steps.add(trigger);
steps.add(parse);
steps.add(extraction);
workflow.put("steps", steps);
return workflow;
}
private static Map<String, Object> createProp(String description) {
Map<String, Object> prop = new LinkedHashMap<>();
List<String> typeList = new ArrayList<>();
typeList.add("string");
typeList.add("null");
prop.put("type", typeList);
prop.put("description", description);
return prop;
}
public static void main(String[] args) throws Exception {
Map<String, Object> WORKFLOW = createWorkflow();
String workflowName = (String) WORKFLOW.get("name");
System.out.println("Deploying \"" + workflowName + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) WORKFLOW.get("steps");
Map<String, Object> updatePayload = new HashMap<>();
updatePayload.put("steps", steps);
api("POST", "/workflows/" + state.workflowId, updatePayload);
} else {
boolean found = false;
try {
String queryUrl = "/workflows?name=" + URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
Map<String, Object> list = api("GET", queryUrl, null);
@SuppressWarnings("unchecked")
List<Map<String, Object>> items = (List<Map<String, Object>>) (list.getOrDefault("data",
list.getOrDefault("items", new ArrayList<>())));
for (Map<String, Object> item : items) {
if (workflowName.equals(item.get("name"))) {
String existingId = (String) item.get("id");
if (existingId != null) {
state.workflowId = existingId;
saveState();
System.out.println(
"✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) WORKFLOW.get("steps");
Map<String, Object> updatePayload = new HashMap<>();
updatePayload.put("steps", steps);
api("POST", "/workflows/" + existingId, updatePayload);
found = true;
break;
}
}
}
} catch (Exception e) {
// Lookup is best-effort; fall through to create
}
if (!found) {
Map<String, Object> created = api("POST", "/workflows", WORKFLOW);
String wfId = (String) created.get("id");
if (wfId == null) {
@SuppressWarnings("unchecked")
Map<String, Object> workflow = (Map<String, Object>) created.get("workflow");
if (workflow != null) {
wfId = (String) workflow.get("id");
}
}
if (wfId == null) {
throw new IOException("Could not read created workflow id from response");
}
state.workflowId = wfId;
saveState();
System.out.println("+ created workflow (" + wfId + ")");
}
}
try {
api("POST", "/workflows/" + state.workflowId + "/versions", new HashMap<>());
} catch (Exception e) {
// Best-effort; some accounts/plans may not require this
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId
+ "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
}
}// This code calls Extend's REST API directly using only Go's standard library.
// Extend has no official Go SDK yet; the REST API is the canonical interface.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
var (
apiKey string
stateDir string
stateFile string
state State
)
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
cwd, _ := os.Getwd()
stateDir = filepath.Join(cwd, ".extend")
stateFile = filepath.Join(stateDir, "check.json")
if data, err := os.ReadFile(stateFile); err == nil {
json.Unmarshal(data, &state)
}
}
func saveState() {
os.MkdirAll(stateDir, 0755)
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
var reqBody io.Reader
if body != nil {
b, _ := json.Marshal(body)
reqBody = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, API+pathName, reqBody)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if resp.StatusCode >= 400 {
preview := string(respBody)
if len(preview) > 300 {
preview = preview[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, preview)
}
return data, nil
}
func main() {
workflow := map[string]interface{}{
"name": "Check 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{}{
"date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The date the check was issued (MM/DD/YYYY format)",
},
"memo": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The memo field text on the check",
},
"payee": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the person or entity the check is payable to",
},
"bank_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the bank",
},
"payer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the entity issuing the check",
},
"check_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The check number printed on the check",
},
"payer_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The address of the entity issuing the check",
},
"account_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The bank account number",
},
"amount_numeric": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The numeric dollar amount of the check",
},
"amount_written": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The written out dollar amount in words",
},
"routing_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The bank routing number",
},
"unique_check_id": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The unique identifier for verification purposes",
},
},
},
"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)
apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
"steps": workflow["steps"],
})
} else {
// Try to find an existing workflow with the same name
found := false
if list, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", url.QueryEscape("Check Processing Pipeline")), nil); err == nil {
var items []map[string]interface{}
if data, ok := list["data"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
} else if data, ok := list["items"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
}
for _, item := range items {
if name, ok := item["name"].(string); ok && name == "Check Processing Pipeline" {
if id, ok := item["id"].(string); ok {
state.WorkflowID = id
saveState()
fmt.Printf("✓ workflow \"Check Processing Pipeline\" found in your account (%s) — updating steps\n", id)
apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
"steps": workflow["steps"],
})
found = true
break
}
}
}
}
if !found {
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 {
wfID = id
} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := wf["id"].(string); ok {
wfID = id
}
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
saveState()
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version (best-effort)
apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})
fmt.Printf("\nDone. Run documents through it with:\n")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Printf("Or open the workflow in the Extend dashboard to review and deploy it.\n")
}This template processes checks. It captures critical payment information such as payee name, check amount (numerical and written), date, bank details, routing numbers, and authorization signatures. The template handles both standard and intentionally inverted check layouts.