Financial & BankingParse → Extract

Check Deposit Extractor

Extracts check details including payee, amount, date, and bank routing information.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document688816579-CHECK.pdf

Step-by-step

A check is a financial instrument issued by a bank account holder that authorizes payment of a specified amount to a designated payee, containing the payer's account information, routing number, check number, and payment authorization. This template takes in Check and outputs markdown (.md) capturing the check's full text and layout, and JSON (.json) with structured payment fields including payee, amounts (numeric and written), dates, bank details, and routing information per the extraction schema by using Extend's Parse, Extract primitives.

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

Parse

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

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

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

Step 2

Extract

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

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

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

Example code

{
  "name": "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 checks
import { 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
from typing import Optional, Dict, Any
from extend_ai import Extend


# Define the check extraction schema as a dictionary
# Each field is nullable (checks may be missing some data)
# Descriptions are detailed to maximize extraction accuracy
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[str, Any]:
    """
    Main processing function
    
    Args:
        file_path: Local path to the check image (PDF, PNG, JPG, etc.)
    
    Returns:
        Extracted check data as JSON
    """
    client = Extend(token=os.environ["EXTEND_API_KEY"])

    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 byte streams for this pattern)
    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={
            "block_options": {
                "text": {
                    "agentic": {
                        "enabled": True,
                    },
                },
            },
            "chunking_strategy": {
                "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(f"✓ Extraction complete.\n")

    # Step 4: Output results
    print("=== Extracted Check Data ===\n")
    print(check_data)

    # 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["amount_numeric"], check_data["amount_written"]
        )
    ):
        print(
            f"⚠ Amount mismatch: numeric='{check_data['amount_numeric']}' vs written='{check_data['amount_written']}'"
        )
    if not check_data.get("date"):
        print("⚠ Date not found")

    print("\n✅ Check processing complete.\n")
    return check_data


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: {err}")
        sys.exit(1)
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.*;
import com.google.gson.*;

/**
 * Check Processing Pipeline
 * 
 * This implementation calls the Extend REST API directly (https://api.extend.ai)
 * because Extend does not publish an official Java SDK.
 * Uses java.net.http.HttpClient with zero external dependencies (except Gson for JSON).
 * 
 * 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 final HttpClient httpClient;
  private final Gson gson;

  public CheckProcessor() {
    this.httpClient = HttpClient.newHttpClient();
    this.gson = new Gson();
  }

  /**
   * Main processing function
   * @param filePath - Local path to the check image (PDF, PNG, JPG, etc.)
   * @return Extracted check data as JSON object
   */
  public JsonObject processCheck(String filePath) throws IOException, InterruptedException {
    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
    // Uses parse_performance + agentic OCR to handle layouts and handwriting
    System.out.println("📄 Step 1: Parsing check with agentic OCR...");
    JsonObject parseResult = createAndPollParseRun(fileUrl);

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

    JsonArray chunks = parseResult.getAsJsonObject("output").getAsJsonArray("chunks");
    StringBuilder parsedText = new StringBuilder();
    for (int i = 0; i < chunks.size(); i++) {
      if (i > 0) parsedText.append("\n\n");
      parsedText.append(chunks.get(i).getAsJsonObject().get("content").getAsString());
    }
    System.out.println("✓ Parse complete. Extracted " + chunks.size() + " chunk(s).\n");

    // Step 3: Extract structured fields
    // Uses extraction_performance + review agent + multimodal for accuracy
    System.out.println("🔍 Step 2: Extracting check fields...");
    JsonObject extractResult = createAndPollExtractRun(fileUrl);

    if (!extractResult.get("status").getAsString().equals("PROCESSED")) {
      throw new RuntimeException("Extraction failed with status: " + extractResult.get("status").getAsString());
    }

    JsonObject checkData = extractResult.getAsJsonObject("output").getAsJsonObject("value");
    System.out.println("✓ Extraction complete.\n");

    // Step 4: Output results
    System.out.println("=== Extracted Check Data ===\n");
    System.out.println(gson.toJson(checkData));

    // Validation: flag potential issues
    System.out.println("\n=== Validation Checks ===");
    if (isNullOrEmpty(checkData, "check_number")) {
      System.out.println("⚠ Check number not found");
    }
    if (isNullOrEmpty(checkData, "routing_number")) {
      System.out.println("⚠ Routing number not found");
    }
    if (isNullOrEmpty(checkData, "account_number")) {
      System.out.println("⚠ Account number not found");
    }
    if (isNullOrEmpty(checkData, "payee")) {
      System.out.println("⚠ Payee not found");
    }
    if (isNullOrEmpty(checkData, "amount_numeric")) {
      System.out.println("⚠ Numeric amount not found");
    }
    if (!isNullOrEmpty(checkData, "amount_numeric") && 
        !isNullOrEmpty(checkData, "amount_written") &&
        !amountsMatch(checkData.get("amount_numeric").getAsString(), 
                     checkData.get("amount_written").getAsString())) {
      System.out.println("⚠ Amount mismatch: numeric='" + checkData.get("amount_numeric").getAsString() + 
                        "' vs written='" + checkData.get("amount_written").getAsString() + "'");
    }
    if (isNullOrEmpty(checkData, "date")) {
      System.out.println("⚠ Date not found");
    }

    System.out.println("\n✅ Check processing complete.\n");
    return checkData;
  }

  /**
   * Create and poll a parse run until completion
   */
  private JsonObject createAndPollParseRun(String fileUrl) throws IOException, InterruptedException {
    JsonObject requestBody = new JsonObject();
    requestBody.add("file", new JsonObject().addProperty("url", fileUrl));
    
    JsonObject config = new JsonObject();
    JsonObject blockOptions = new JsonObject();
    JsonObject textConfig = new JsonObject();
    JsonObject agenticConfig = new JsonObject();
    agenticConfig.addProperty("enabled", true);
    textConfig.add("agentic", agenticConfig);
    blockOptions.add("text", textConfig);
    config.add("blockOptions", blockOptions);
    config.addProperty("chunkingStrategy", "{\"type\":\"document\"}");
    requestBody.add("config", config);

    String runId = createParseRun(requestBody);
    return pollParseRunStatus(runId);
  }

  /**
   * Create a parse run and return the run ID
   */
  private String createParseRun(JsonObject body) throws IOException, InterruptedException {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE + "/v1/parseRuns"))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
        .build();

    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    JsonObject responseBody = JsonParser.parseString(response.body()).getAsJsonObject();
    return responseBody.get("id").getAsString();
  }

  /**
   * Poll parse run status until PROCESSED
   */
  private JsonObject pollParseRunStatus(String runId) throws IOException, InterruptedException {
    int maxAttempts = 120;
    int attempt = 0;
    while (attempt < maxAttempts) {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE + "/v1/parseRuns/" + runId))
          .header("Authorization", "Bearer " + API_KEY)
          .GET()
          .build();

      HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
      JsonObject responseBody = JsonParser.parseString(response.body()).getAsJsonObject();
      String status = responseBody.get("status").getAsString();

      if (status.equals("PROCESSED") || status.equals("FAILED")) {
        return responseBody;
      }

      Thread.sleep(1000);
      attempt++;
    }
    throw new RuntimeException("Parse run polling timed out");
  }

  /**
   * Create and poll an extract run until completion
   */
  private JsonObject createAndPollExtractRun(String fileUrl) throws IOException, InterruptedException {
    JsonObject requestBody = new JsonObject();
    requestBody.add("file", new JsonObject().addProperty("url", fileUrl));
    
    JsonObject config = new JsonObject();
    JsonObject schema = buildCheckSchema();
    config.add("schema", schema);
    requestBody.add("config", config);

    String runId = createExtractRun(requestBody);
    return pollExtractRunStatus(runId);
  }

  /**
   * Create an extract run and return the run ID
   */
  private String createExtractRun(JsonObject body) throws IOException, InterruptedException {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE + "/v1/extractRuns"))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
        .build();

    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    JsonObject responseBody = JsonParser.parseString(response.body()).getAsJsonObject();
    return responseBody.get("id").getAsString();
  }

  /**
   * Poll extract run status until PROCESSED
   */
  private JsonObject pollExtractRunStatus(String runId) throws IOException, InterruptedException {
    int maxAttempts = 120;
    int attempt = 0;
    while (attempt < maxAttempts) {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE + "/v1/extractRuns/" + runId))
          .header("Authorization", "Bearer " + API_KEY)
          .GET()
          .build();

      HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
      JsonObject responseBody = JsonParser.parseString(response.body()).getAsJsonObject();
      String status = responseBody.get("status").getAsString();

      if (status.equals("PROCESSED") || status.equals("FAILED")) {
        return responseBody;
      }

      Thread.sleep(1000);
      attempt++;
    }
    throw new RuntimeException("Extract run polling timed out");
  }

  /**
   * Build the check extraction JSON schema
   */
  private JsonObject buildCheckSchema() {
    JsonObject schema = new JsonObject();
    schema.addProperty("type", "object");
    
    JsonObject properties = new JsonObject();
    
    properties.add("check_number", buildField("The check number printed on the check, typically 6-10 digits in the bottom right corner"));
    properties.add("routing_number", buildField("The bank routing number (ABA number), a 9-digit code at the bottom left of the check"));
    properties.add("account_number", buildField("The bank account number, typically 10-12 digits printed at the bottom center of the check"));
    properties.add("date", buildField("The date the check was issued in MM/DD/YYYY format (e.g., 01/15/2024)"));
    properties.add("payee", buildField("The name of the person or entity the check is payable to, written after 'Pay to the order of'"));
    properties.add("amount_numeric", buildField("The numeric dollar amount of the check, typically in the top right (e.g., '$1,234.56')"));
    properties.add("amount_written", buildField("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')"));
    properties.add("payer_name", buildField("The name of the entity issuing the check, printed at the top left of the check"));
    properties.add("payer_address", buildField("The address of the entity issuing the check, typically below the payer name in the top left"));
    properties.add("memo", buildField("The memo or note field text on the check, typically in the bottom left (may reference invoice numbers or payment purpose)"));
    properties.add("bank_name", buildField("The name of the bank, usually printed in the top center or top left of the check"));
    properties.add("unique_check_id", buildField("The unique identifier for verification purposes, may include MICR encoding or additional security identifiers"));
    
    schema.add("properties", properties);
    return schema;
  }

  /**
   * Build a single nullable string field with description
   */
  private JsonObject buildField(String description) {
    JsonObject field = new JsonObject();
    JsonArray typeArray = new JsonArray();
    typeArray.add("string");
    typeArray.add("null");
    field.add("type", typeArray);
    field.addProperty("description", description);
    return field;
  }

  /**
   * Helper: Check if field is null or empty string
   */
  private boolean isNullOrEmpty(JsonObject obj, String key) {
    JsonElement element = obj.get(key);
    return element == null || element.isJsonNull() || 
           (element.isJsonPrimitive() && element.getAsString().isEmpty());
  }

  /**
   * Helper: Rough check if numeric and written amounts match
   * (Does not validate currency format; just detects obvious mismatches)
   */
  private boolean amountsMatch(String numeric, String written) {
    String numericDigits = numeric.replaceAll("\\D", "");
    String writtenDigits = written.replaceAll("\\D", "");
    return numericDigits.length() > 0 && writtenDigits.length() > 0 &&
           numericDigits.length() == writtenDigits.length();
  }

  /**
   * Main entry point
   */
  public static void main(String[] args) {
    if (args.length == 0) {
      System.err.println("Usage: java CheckProcessor <path-to-check-image>");
      System.exit(1);
    }

    try {
      CheckProcessor processor = new CheckProcessor();
      processor.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 Go net/http and encoding/json.

package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strings"
)

// CheckData represents the extracted check fields
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"`
}

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

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

// ProcessCheck ingests a check image, parses it with agentic OCR,
// then extracts 11 critical payment fields into structured JSON.
func ProcessCheck(filePath string) (*CheckData, error) {
	fmt.Printf("\n=== Check Processing Pipeline ===\n")
	fmt.Printf("File: %s\n\n", filePath)

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

	// Step 1: Convert local file to data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
	fileURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64Data)

	// Step 2: Parse the check with agentic OCR
	fmt.Println("📄 Step 1: Parsing check with agentic OCR...")

	parsePayload := 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",
			},
		},
	}

	parseBody, err := json.Marshal(parsePayload)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal parse payload: %w", err)
	}

	parseReq, err := http.NewRequest("POST", "https://api.extend.ai/v1/parseRuns", bytes.NewReader(parseBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create parse request: %w", err)
	}
	parseReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	parseReq.Header.Set("Content-Type", "application/json")

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

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

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

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

	var parsedTextParts []string
	for _, chunk := range parseRun.Output.Chunks {
		parsedTextParts = append(parsedTextParts, chunk.Content)
	}
	parsedText := strings.Join(parsedTextParts, "\n\n")

	fmt.Printf("✓ Parse complete. Extracted %d chunk(s).\n\n", len(parseRun.Output.Chunks))

	_ = parsedText // parsedText is available for downstream use if needed

	// Step 3: Extract structured fields
	fmt.Println("🔍 Step 2: Extracting check fields...")

	checkSchema := 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 (e.g., 'One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents')",
			},
			"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 (may reference invoice numbers or payment purpose)",
			},
			"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",
			},
		},
	}

	extractPayload := map[string]interface{}{
		"file": map[string]string{
			"url": fileURL,
		},
		"config": map[string]interface{}{
			"schema": checkSchema,
		},
	}

	extractBody, err := json.Marshal(extractPayload)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal extract payload: %w", err)
	}

	extractReq, err := http.NewRequest("POST", "https://api.extend.ai/v1/extractRuns", bytes.NewReader(extractBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create extract request: %w", err)
	}
	extractReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	extractReq.Header.Set("Content-Type", "application/json")

	extractResp, err := client.Do(extractReq)
	if err != nil {
		return nil, fmt.Errorf("failed to execute extract request: %w", err)
	}
	defer extractResp.Body.Close()

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

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

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

	checkData := &extractRun.Output.Value
	fmt.Println("✓ Extraction complete.\n")

	// Step 4: Output results
	fmt.Println("=== Extracted Check Data ===\n")
	checkJSON, err := json.MarshalIndent(checkData, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal check data: %w", err)
	}
	fmt.Println(string(checkJSON))

	// Validation: flag potential issues
	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
}

// amountsMatch performs a rough check if numeric and written amounts match
// (Does not validate currency format; just detects obvious mismatches)
func amountsMatch(numeric, written string) bool {
	// Extract all digits from numeric amount (e.g., "$1,234.56" → "123456")
	numericDigits := regexp.MustCompile(`\D`).ReplaceAllString(numeric, "")
	// Extract all digits from written amount (e.g., "One Thousand..." → "1000")
	writtenDigits := regexp.MustCompile(`\D`).ReplaceAllString(written, "")

	// Simple heuristic: both should contain similar digit patterns
	return len(numericDigits) > 0 && len(writtenDigits) > 0 &&
		len(numericDigits) == len(writtenDigits)
}

func main() {
	flag.Parse()
	args := flag.Args()

	if len(args) == 0 {
		fmt.Fprintf(os.Stderr, "Usage: go run solution.go <path-to-check-image>\n")
		os.Exit(1)
	}

	_, err := ProcessCheck(args[0])
	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-deposit-capture.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-deposit-capture).

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-deposit-capture.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); });
#!/usr/bin/env python3
"""
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-deposit-capture.json,
so re-running updates the existing workflow instead of duplicating it.

Usage:
   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
   python provision.py

Generated by doc1 (template: check-deposit-capture).
"""

import json
import os
import sys
from pathlib import Path
from typing import Any, Optional

from extend_ai import Extend

API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
    print("Set EXTEND_API_KEY first.", file=sys.stderr)
    sys.exit(1)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "check-deposit-capture.json"


def load_state() -> dict[str, Optional[str]]:
    """Load workflow state from file."""
    if STATE_FILE.exists():
        with open(STATE_FILE, "r") as f:
            return json.load(f)
    return {}


def save_state(state: dict[str, Optional[str]]) -> None:
    """Save workflow state to file."""
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)


# Workflow definition — extractor/classifier/splitter configs inline
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 def main() -> None:
    """Main provisioning function."""
    client = Extend(token=API_KEY)
    state = load_state()

    print(f'Deploying "{WORKFLOW["name"]}…"')

    if state.get("workflow_id"):
        workflow_id = state["workflow_id"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        await client.workflows.update(
            id=workflow_id,
            body={"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.
        existing_id: Optional[str] = None
        try:
            result = await client.workflows.list(name=WORKFLOW["name"])
            items = result.get("data") or result.get("items") or []
            for item in items:
                if item.get("name") == WORKFLOW["name"]:
                    existing_id = item.get("id")
                    break
            if existing_id:
                state["workflow_id"] = existing_id
                save_state(state)
                print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps')
                await client.workflows.update(
                    id=existing_id,
                    body={"steps": WORKFLOW["steps"]}
                )
        except Exception:
            # Lookup is best-effort; fall through to create
            pass

        if not state.get("workflow_id"):
            created = await client.workflows.create(body=WORKFLOW)
            workflow_id = created.get("id") or created.get("workflow", {}).get("id")
            if not workflow_id:
                raise ValueError("Could not read created workflow id from response")
            state["workflow_id"] = workflow_id
            save_state(state)
            print(f"+ created workflow ({workflow_id})")

    # Deploy the current draft as a new version so the workflow is runnable —
    # best-effort: some accounts/plans may not require this explicit step.
    workflow_id = state["workflow_id"]
    try:
        await client.workflows.create_version(id=workflow_id, body={})
    except Exception:
        pass

    api_endpoint = "https://api.extend.ai"
    print("\nDone. Run documents through it with:")
    print(f'  POST {api_endpoint}/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
    try:
        asyncio.run(main())
    except Exception as e:
        print(str(e) if str(e) else repr(e), file=sys.stderr)
        sys.exit(1)
// NOTE: 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 external dependencies).

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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.util.*;

public class ProvisionCheck {
    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("check-deposit-capture.json");
    
    private static final HttpClient httpClient = HttpClient.newBuilder().build();
    private static Map<String, Object> state = new LinkedHashMap<>();
    
    public static void main(String[] args) {
        try {
            if (API_KEY == null || API_KEY.isEmpty()) {
                System.err.println("Set EXTEND_API_KEY first.");
                System.exit(1);
            }
            
            loadState();
            
            Map<String, Object> workflow = buildWorkflow();
            System.out.println("Deploying \"" + workflow.get("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 {
                    String encodedName = URLEncoder.encode((String) workflow.get("name"), StandardCharsets.UTF_8);
                    Map<String, Object> list = api("GET", "/workflows?name=" + encodedName, null);
                    List<?> items = (List<?>) (list.getOrDefault("data", list.getOrDefault("items", new ArrayList<>())));
                    
                    for (Object item : items) {
                        Map<String, Object> itemMap = (Map<String, Object>) item;
                        if (workflow.get("name").equals(itemMap.get("name"))) {
                            String existingId = (String) itemMap.get("id");
                            if (existingId != null && !existingId.isEmpty()) {
                                state.put("workflowId", existingId);
                                saveState();
                                System.out.println("✓ workflow \"" + workflow.get("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()) {
                    Map<String, Object> created = api("POST", "/workflows", workflow);
                    String wfId = (String) created.get("id");
                    if (wfId == null) {
                        Map<String, Object> workflowMap = (Map<String, Object>) created.get("workflow");
                        if (workflowMap != null) {
                            wfId = (String) workflowMap.get("id");
                        }
                    }
                    if (wfId == null) {
                        throw new Exception("Could not read created workflow id from response");
                    }
                    state.put("workflowId", wfId);
                    saveState();
                    System.out.println("+ created workflow (" + wfId + ")");
                    workflowId = wfId;
                }
            }
            
            try {
                api("POST", "/workflows/" + workflowId + "/versions", new LinkedHashMap<>());
            } 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: \"" + workflowId + "\" }, file: { url: \"https://…\" } }");
            System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
            
        } catch (Exception e) {
            System.err.println(e.getMessage() != null ? e.getMessage() : e);
            e.printStackTrace();
            System.exit(1);
        }
    }
    
    private static void loadState() throws IOException {
        if (Files.exists(STATE_FILE)) {
            String content = Files.readString(STATE_FILE, StandardCharsets.UTF_8);
            state = parseJson(content);
        }
    }
    
    private static void saveState() throws IOException {
        Files.createDirectories(STATE_DIR);
        String json = toJson(state);
        Files.writeString(STATE_FILE, json, StandardCharsets.UTF_8);
    }
    
    private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws Exception {
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
            .uri(new URI(API + pathName))
            .header("Authorization", "Bearer " + API_KEY)
            .header("x-extend-api-version", VERSION);
        
        if (body != null) {
            String bodyJson = toJson(body);
            requestBuilder.header("Content-Type", "application/json")
                .method(method, HttpRequest.BodyPublishers.ofString(bodyJson));
        } else {
            requestBuilder.method(method, HttpRequest.BodyPublishers.noBody());
        }
        
        HttpRequest request = requestBuilder.build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        Map<String, Object> data;
        try {
            data = parseJson(response.body());
        } catch (Exception e) {
            data = new LinkedHashMap<>();
        }
        
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            String errorMsg = toJson(data);
            if (errorMsg.length() > 300) {
                errorMsg = errorMsg.substring(0, 300);
            }
            throw new Exception(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
        }
        
        return data;
    }
    
    private static Map<String, Object> buildWorkflow() {
        Map<String, Object> workflow = new LinkedHashMap<>();
        workflow.put("name", "Check 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>> startNext = new ArrayList<>();
        Map<String, String> startNextStep = new LinkedHashMap<>();
        startNextStep.put("step", "parse1");
        startNext.add(startNextStep);
        startTrigger.put("next", startNext);
        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> text = new LinkedHashMap<>();
        Map<String, Object> agentic = new LinkedHashMap<>();
        agentic.put("enabled", true);
        text.put("agentic", agentic);
        blockOptions.put("text", text);
        Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
        chunkingStrategy.put("type", "document");
        parseConfig.put("blockOptions", blockOptions);
        parseConfig.put("chunkingStrategy", chunkingStrategy);
        Map<String, Object> parseConfigWrapper = new LinkedHashMap<>();
        parseConfigWrapper.put("parseConfig", parseConfig);
        parse.put("config", parseConfigWrapper);
        List<Map<String, String>> parseNext = new ArrayList<>();
        Map<String, String> parseNextStep = new LinkedHashMap<>();
        parseNextStep.put("step", "extraction2");
        parseNext.add(parseNextStep);
        parse.put("next", parseNext);
        steps.add(parse);
        
        // extraction2
        Map<String, Object> extraction = new LinkedHashMap<>();
        extraction.put("name", "extraction2");
        extraction.put("type", "EXTRACT");
        Map<String, Object> extractorConfig = new LinkedHashMap<>();
        
        Map<String, Object> schema = new LinkedHashMap<>();
        schema.put("type", "object");
        Map<String, Object> properties = new LinkedHashMap<>();
        
        String[] fields = {"date", "memo", "payee", "bank_name", "payer_name", "check_number", 
                          "payer_address", "account_number", "amount_numeric", "amount_written", 
                          "routing_number", "unique_check_id"};
        String[] descriptions = {
            "The date the check was issued (MM/DD/YYYY format)",
            "The memo field text on the check",
            "The name of the person or entity the check is payable to",
            "The name of the bank",
            "The name of the entity issuing the check",
            "The check number printed on the check",
            "The address of the entity issuing the check",
            "The bank account number",
            "The numeric dollar amount of the check",
            "The written out dollar amount in words",
            "The bank routing number",
            "The unique identifier for verification purposes"
        };
        
        for (int i = 0; i < fields.length; i++) {
            Map<String, Object> field = new LinkedHashMap<>();
            List<String> typeList = new ArrayList<>();
            typeList.add("string");
            typeList.add("null");
            field.put("type", typeList);
            field.put("description", descriptions[i]);
            properties.put(fields[i], field);
        }
        
        schema.put("properties", properties);
        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> extractionConfigWrapper = new LinkedHashMap<>();
        extractionConfigWrapper.put("extractorConfig", extractorConfig);
        extraction.put("config", extractionConfigWrapper);
        steps.add(extraction);
        
        workflow.put("steps", steps);
        return workflow;
    }
    
    private static Map<String, Object> parseJson(String json) {
        json = json.trim();
        if (json.isEmpty() || !json.startsWith("{")) {
            return new LinkedHashMap<>();
        }
        
        Map<String, Object> result = new LinkedHashMap<>();
        int depth = 0;
        StringBuilder key = new StringBuilder();
        StringBuilder value = new StringBuilder();
        boolean inKey = true;
        boolean inString = false;
        boolean escapeNext = false;
        
        for (int i = 1; i < json.length() - 1; i++) {
            char c = json.charAt(i);
            
            if (escapeNext) {
                if (inKey) key.append(c);
                else value.append(c);
                escapeNext = false;
                continue;
            }
            
            if (c == '\\') {
                escapeNext = true;
                if (inKey) key.append(c);
                else value.append(c);
                continue;
            }
            
            if (c == '"') {
                inString = !inString;
                if (!inKey || key.length() > 0) {
                    if (inKey) key.append(c);
                    else value.append(c);
                }
                continue;
            }
            
            if (inString) {
                if (inKey) key.append(c);
                else value.append(c);
                continue;
            }
            
            if (c == ':' && inKey) {
                inKey = false;
                continue;
            }
            
            if (c == ',' && depth == 0) {
                String k = key.toString().trim().replaceAll("^\"|\"$", "");
                String v = value.toString().trim();
                result.put(k, parseJsonValue(v));
                key = new StringBuilder();
                value = new StringBuilder();
                inKey = true;
                continue;
            }
            
            if (c == '{' || c == '[') depth++;
            if (c == '}' || c == ']') depth--;
            
            if (!inKey) value.append(c);
        }
        
        if (key.length() > 0) {
            String k = key.toString().trim().replaceAll("^\"|\"$", "");
            String v = value.toString().trim();
            result.put(k, parseJsonValue(v));
        }
        
        return result;
    }
    
    private static Object parseJsonValue(String value) {
        value = value.trim();
        if (value.isEmpty()) return null;
        if ("null".equals(value)) return null;
        if ("true".equals(value)) return true;
        if ("false".equals(value)) return false;
        if (value.startsWith("\"") && value.endsWith("\"")) {
            return value.substring(1, value.length() - 1).replace("\\\"", "\"");
        }
        if (value.startsWith("{")) {
            return parseJson(value);
        }
        if (value.startsWith("[")) {
            return parseJsonArray(value);
        }
        try {
            if (value.contains(".")) {
                return Double.parseDouble(value);
            }
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            return value;
        }
    }
    
    private static List<Object> parseJsonArray(String json) {
        List<Object> result = new ArrayList<>();
        json = json.substring(1, json.length() - 1).trim();
        if (json.isEmpty()) return result;
        
        StringBuilder current = new StringBuilder();
        int depth = 0;
        boolean inString = false;
        boolean escapeNext = false;
        
        for (int i = 0; i < json.length(); i++) {
            char c = json.charAt(i);
            
            if (escapeNext) {
                current.append(c);
                escapeNext = false;
                continue;
            }
            
            if (c == '\\') {
                current.append(c);
                escapeNext = true;
                continue;
            }
            
            if (c == '"') {
                inString = !inString;
                current.append(c);
                continue;
            }
            
            if (!inString) {
                if (c == '{' || c == '[') depth++;
                if (c == '}' || c == ']') depth--;
                if (c == ',' && depth == 0) {
                    result.add(parseJsonValue(current.toString()));
                    current = new StringBuilder();
                    continue;
                }
            }
            
            current.append(c);
        }
        
        if (current.length() > 0) {
            result.add(parseJsonValue(current.toString()));
        }
        
        return result;
    }
    
    private static String toJson(Object obj) {
        if (obj == null) return "null";
        if (obj instanceof Boolean) return obj.toString();
        if (obj instanceof Number) return obj.toString();
        if (obj instanceof String) {
            String s = (String) obj;
            return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r") + "\"";
        }
        if (obj instanceof List) {
            List<?> list = (List<?>) obj;
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < list.size(); i++) {
                if (i > 0) sb.append(",");
                sb.append(toJson(list.get(i)));
            }
            sb.append("]");
            return sb.toString();
        }
        if (obj instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) obj;
            StringBuilder sb = new StringBuilder("{");
            boolean first = true;
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                if (!first) sb.append(",");
                sb.append(toJson(entry.getKey().toString())).append(":").append(toJson(entry.getValue()));
                first = false;
            }
            sb.append("}");
            return sb.toString();
        }
        return "\"" + obj.toString() + "\"";
    }
}
package main

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

// Extend does NOT publish an official Go SDK — we call the REST API directly
// using only net/http and encoding/json from the standard library.

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 ListResponse struct {
	Data  []WorkflowItem `json:"data,omitempty"`
	Items []WorkflowItem `json:"items,omitempty"`
}

type WorkflowItem struct {
	ID   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
}

var (
	stateDir  = filepath.Join(os.Getenv("PWD"), ".extend")
	stateFile = filepath.Join(stateDir, "check-deposit-capture.json")
	state     State
	apiKey    string
)

func loadState() error {
	data, err := os.ReadFile(stateFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	return json.Unmarshal(data, &state)
}

func saveState() error {
	if err := os.MkdirAll(stateDir, 0755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(stateFile, data, 0644)
}

func apiCall(method, pathName string, body interface{}) ([]byte, error) {
	var reqBody io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewReader(data)
	}

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

	req.Header.Set("Authorization", "Bearer "+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()

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

	if resp.StatusCode >= 400 {
		respStr := string(respData)
		if len(respStr) > 300 {
			respStr = respStr[:300]
		}
		return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
	}

	return respData, nil
}

func main() {
	flag.Parse()

	apiKey = os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintln(os.Stderr, "Set EXTEND_API_KEY first.")
		os.Exit(1)
	}

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

	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,
						},
					},
				},
			},
		},
	}

	workflowName := workflow["name"].(string)
	fmt.Printf("Deploying \"%s\"…\n", workflowName)

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		stepsPayload := map[string]interface{}{"steps": workflow["steps"]}
		if _, err := apiCall("POST", "/workflows/"+state.WorkflowID, stepsPayload); err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		foundID := ""
		listPath := "/workflows?name=" + strings.ReplaceAll(workflowName, " ", "%20")
		if respData, err := apiCall("GET", listPath, nil); err == nil {
			var listResp ListResponse
			if err := json.Unmarshal(respData, &listResp); err == nil {
				var items []WorkflowItem
				if len(listResp.Data) > 0 {
					items = listResp.Data
				} else {
					items = listResp.Items
				}
				for _, item := range items {
					if item.Name == workflowName {
						foundID = item.ID
						break
					}
				}
			}
		}

		if foundID != "" {
			state.WorkflowID = foundID
			if err := saveState(); 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", workflowName, foundID)
			stepsPayload := map[string]interface{}{"steps": workflow["steps"]}
			if _, err := apiCall("POST", "/workflows/"+foundID, stepsPayload); err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}
		} else {
			respData, err := apiCall("POST", "/workflows", workflow)
			if err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}

			var created WorkflowResponse
			if err := json.Unmarshal(respData, &created); err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
				os.Exit(1)
			}

			wfID := created.ID
			if wfID == "" {
				wfID = created.Workflow.ID
			}
			if wfID == "" {
				fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
				os.Exit(1)
			}

			state.WorkflowID = wfID
			if err := saveState(); err != nil {
				fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err)
				os.Exit(1)
			}
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

	apiCall("POST", "/workflows/"+state.WorkflowID+"/versions", 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.")
}

Frequently Asked Questions (FAQ)

Require `confidence >= 0.85` for fully automated processing; below that, route to human review. For critical fields like `routing_number` and `account_number`, enforce `>= 0.90` since even small extraction errors cause payment failures.
Tags
BankingPaymentsCheck ProcessingFinancial DocumentsVerification
About this template

This template processes digitally-printed 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.

Document formats
  • PDF
  • Images & Scans
Requirements
  • Checkboxes & Strikethroughs
  • Scanned documents

Relevant templates for Financial & Banking

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