Financial & BankingClassify → Extract

Receipt Classifier & Extractor

Classifies a receipt based on total transaction amount, set at $200 as default. If the receipt is flagged, it extracts transaction details, items, totals, and payment info.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentwalmart receipt.png

Step-by-step

A retail point-of-sale receipt is a transaction document issued by a merchant that records the itemized purchases, merchant details, tax calculations, and payment authorization information for a completed retail sale. This template takes in Retail Point-of-Sale Receipt and outputs markdown (.md) capturing the receipt's full text and layout, and JSON (.json) with structured merchant, transaction, line item, and payment fields per the extraction schema by using Extend's Extract, Classify primitives.

Input
Retail Point-of-Sale Receipt
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

Extract

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

InputDocument file, or markdown from a prior Parse step
Config
schemacustom schema — 12 fieldschanged
advancedOptions.advancedMultimodalEnabledtruechanged
advancedOptions.reviewAgent.enabledtruechanged
extractionRulesno custom rules
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.

Step 2

Classify

Assigns the document to one of a set of caller-defined categories.

InputOutput of the Extract step
Config
classifications2 custom categorieschanged
advancedOptions.advancedMultimodalEnabledtrue
baseProcessor"classification_performance"
OutputMatched category ID and type, a confidence score, and the reasoning behind the decision

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

Example code

{
  "name": "Receipt Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": true
            },
            "tables": {
              "agentic": {
                "enabled": true
              },
              "tableHeaderContinuationEnabled": true
            },
            "figures": {
              "enabled": true
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {}
          }
        }
      },
      "next": [
        {
          "step": "classify2"
        }
      ]
    },
    {
      "name": "classify2",
      "type": "CLASSIFY",
      "config": {
        "classifierConfig": {
          "classifications": [
            {
              "id": "classification1",
              "type": "other",
              "description": "Use the \"other\" classification when the total receipt amount is under $200."
            },
            {
              "id": "classification_H7O",
              "type": "flag",
              "description": "Use the \"flag\" classification when the total receipt amount is over $200."
            }
          ],
          "baseProcessor": "classification_performance",
          "advancedOptions": {
            "advancedMultimodalEnabled": true
          }
        }
      },
      "next": [
        {
          "step": "extraction3",
          "classificationId": "classification1"
        },
        {
          "step": "extraction3",
          "classificationId": "classification_H7O"
        }
      ]
    },
    {
      "name": "extraction3",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "tax": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Tax amount"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "price": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "description": "List of purchased items"
              },
              "total": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Final total amount"
              },
              "retailer": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the retailer"
              },
              "subtotal": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Subtotal amount before tax"
              },
              "store_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Store identification number"
              },
              "store_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full street address of the store"
              },
              "payment_method": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Payment type and last four digits"
              },
              "approval_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Payment approval number"
              },
              "transaction_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Date of transaction in YYYY-MM-DD format"
              },
              "transaction_time": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Time of transaction in HH:MM format"
              },
              "store_city_state_zip": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "City, state, and ZIP code of the store"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Receipt Processing — Extend AI Skill

## What this pipeline does

This pipeline ingests retail point-of-sale receipts and extracts all structured transaction data: merchant details (name, store number, address), itemized line items with prices, tax and total amounts, and payment authorization information. It first classifies receipts by transaction value (under or over $200) to route high-value transactions for additional review, then extracts fields into JSON using the Extend extraction engine with built-in review agent verification.

## When to use this

- **Expense reporting systems**: Automatically parse employee receipts for reimbursement workflows, categorizing by spend tier.
- **Point-of-sale analytics**: Ingest thousands of retail receipts to track inventory movement, vendor performance, and transaction patterns.
- **Fraud detection pipelines**: Flag high-value receipts ($200+) for human review before payment settlement.
- **Retail accounting**: Populate general ledger and accounts payable systems with merchant, item, and payment data extracted from digital or scanned receipts.
- **Loyalty program audits**: Cross-reference receipt line items and totals against customer purchase history.

## Processor pipeline

### Step 1: Parse — Agentic OCR with signature detection
**Processor:** `parse` (async) with `agentic_ocr` text block, table headers, and figure detection enabled.

**Purpose:** Convert receipt images (scans, phone photos, low-quality printouts) into clean markdown and bounding-box data. Agentic OCR handles skewed angles, thermal paper fading, and handwritten notes that light mode would miss.

**Key config:**
- `text.agentic.enabled: true` — handles handwritten or degraded text on receipts (common in retail).
- `tables.agentic.enabled: true` — line-item tables with merged cells or irregular spacing are reconstructed reliably.
- `signatureDetectionEnabled: true` — captures payment authorization signatures if present.
- `chunkingStrategy: "page"` — receipts are typically single-page; page-level chunks avoid splitting line items.

**Why this config:** Receipts are often low-quality images from phone cameras or aging thermal printers. Agentic OCR tolerates distortion; light mode would fail on skew or fading. Table handling is critical because line-item layouts vary wildly across retailers.

### Step 2: Classify — Route by transaction value
**Processor:** `classify` with `classification_performance` base processor.

**Purpose:** Separate high-value receipts ($200+) into a "flag" category for mandatory human review, while low-value receipts ($<200) proceed to standard extraction only.

**Key config:**
- Classification 1: `id: "classification1"`, `type: "other"`, total < $200 — standard extraction only.
- Classification 2: `id: "classification_H7O"`, `type: "flag"`, total >= $200 — extraction + review agent.
- `advancedMultimodalEnabled: true` — uses both text and visual layout to determine total accurately.

**Why this config:** Retail receipt amounts are visually distinct (large font, bottom of receipt). Multimodal classification avoids OCR errors on the total line. Splitting by value tier allows cost optimization: low-value receipts skip review, high-value ones get it.

### Step 3: Extract — Structured JSON with conditional review
**Processor:** `extract` with `extraction_performance` base processor, review agent enabled.

**Purpose:** Pull all merchant, item, and payment fields into typed JSON. Review agent runs in parallel, re-reading the receipt to catch extraction hallucinations.

**Key config:**
- `baseProcessor: "extraction_performance"` — optimized for accuracy over speed; acceptable for async receipt batches.
- `reviewAgent.enabled: true` — always enabled in this workflow; catches mistakes like duplicate items or transposed amounts.
- `advancedMultimodalEnabled: true` — leverages receipt layout (item columns, boxes around totals) to boost confidence.
- Schema includes: retailer name, store ID/address, transaction date/time, line items (description + price), subtotal/tax/total, payment method, and approval number.

**Why this config:** Receipts contain dense numeric data (prices, tax, totals) prone to OCR-level errors. Review agent re-validates the total calculation to prevent downstream accounting errors. Multimodal extraction catches structured tables that pure text extraction might miss.

---

## TypeScript implementation

```typescript
import { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

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

/**
 * Receipt Processing Pipeline
 * 
 * 1. Parse: Agentic OCR with signature detection
 * 2. Classify: Route by transaction value (< $200 vs >= $200)
 * 3. Extract: Structured JSON with review agent verification
 */

const receiptSchema = z.object({
  retailer: z.string().nullable().describe("Name of the retailer (e.g., 'Best Buy', 'Target')"),
  store_number: z.string().nullable().describe("Store identification number or location code"),
  store_address: z.string().nullable().describe("Full street address of the store"),
  store_city_state_zip: z.string().nullable().describe("City, state, and ZIP code of the store"),
  transaction_date: z.string().nullable().describe("Date of transaction in YYYY-MM-DD format"),
  transaction_time: z.string().nullable().describe("Time of transaction in HH:MM format (24-hour)"),
  items: z.array(
    z.object({
      description: z.string().nullable().describe("Item name or SKU description"),
      price: z.string().nullable().describe("Item price as printed (e.g., '$19.99')"),
    })
  ).describe("List of purchased items with individual prices"),
  subtotal: z.string().nullable().describe("Subtotal before tax (e.g., '$99.99')"),
  tax: z.string().nullable().describe("Sales tax amount (e.g., '$8.00')"),
  total: z.string().nullable().describe("Final total including tax (e.g., '$107.99')"),
  payment_method: z.string().nullable().describe("Payment type and last 4 digits (e.g., 'VISA ****1234')"),
  approval_number: z.string().nullable().describe("Payment approval/authorization code"),
});

interface ClassificationResult {
  classification_id: string;
  type: "other" | "flag";
  confidence: number;
}

interface ExtractionResult {
  retailer: string | null;
  store_number: string | null;
  store_address: string | null;
  store_city_state_zip: string | null;
  transaction_date: string | null;
  transaction_time: string | null;
  items: Array<{ description: string | null; price: string | null }>;
  subtotal: string | null;
  tax: string | null;
  total: string | null;
  payment_method: string | null;
  approval_number: string | null;
}

interface ReceiptProcessingOutput {
  classification: ClassificationResult;
  extraction: ExtractionResult;
  flagged_for_review: boolean;
}

async function processReceipt(filePath: string): Promise<ReceiptProcessingOutput> {
  console.log(`\n=== Receipt Processing Pipeline ===`);
  console.log(`Processing: ${path.basename(filePath)}`);

  // Convert local file to base64 data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const base64Data = fileBuffer.toString("base64");
  const dataUrl = `data:application/octet-stream;base64,${base64Data}`;

  // ─────────────────────────────────────────────────────────────
  // STEP 1: Parse — Agentic OCR with signature detection
  // ─────────────────────────────────────────────────────────────
  console.log("\n[1/3] Parsing receipt with agentic OCR...");
  
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
  });

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

  console.log(`✓ Parse complete. Extracted ${parseRun.output.chunks.length} chunks.`);
  // parseRun.output.chunks contains markdown + bounding boxes; useful for debugging
  // but classification/extraction run on the file directly.

  // ─────────────────────────────────────────────────────────────
  // STEP 2: Classify — Route by transaction value
  // ─────────────────────────────────────────────────────────────
  console.log("\n[2/3] Classifying receipt by transaction value...");

  const classifyRun = await client.classifyRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      classifications: [
        {
          id: "classification1",
          type: "other",
          description: "Use the 'other' classification when the total receipt amount is under $200.",
        },
        {
          id: "classification_H7O",
          type: "flag",
          description: "Use the 'flag' classification when the total receipt amount is over $200.",
        },
      ],
      baseProcessor: "classification_performance",
      advancedOptions: {
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const classification = classifyRun.output.classification;
  const isFlagged = classification.type === "flag";
  
  console.log(`✓ Classification: ${classification.type.toUpperCase()} (confidence: ${(classification.confidence * 100).toFixed(1)}%)`);

  // ─────────────────────────────────────────────────────────────
  // STEP 3: Extract — Structured JSON with review agent
  // ─────────────────────────────────────────────────────────────
  console.log("\n[3/3] Extracting structured receipt data...");

  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: receiptSchema,
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: {
          enabled: true, // Always verify extraction, especially for flagged high-value receipts
        },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const extraction = extractRun.output.value as ExtractionResult;
  console.log(`✓ Extraction complete.`);
  console.log(`  Retailer: ${extraction.retailer || "(not found)"}`);
  console.log(`  Total: ${extraction.total || "(not found)"}`);
  console.log(`  Items: ${extraction.items.length}`);

  // ─────────────────────────────────────────────────────────────
  // Return structured output
  // ─────────────────────────────────────────────────────────────
  const result: ReceiptProcessingOutput = {
    classification: {
      classification_id: classification.id,
      type: classification.type as "other" | "flag",
      confidence: classification.confidence,
    },
    extraction,
    flagged_for_review: isFlagged,
  };

  return result;
}

// Main entry point for testing
async function main() {
  // For testing: accept file path from command-line argument or use a default
  const filePath = process.argv[2] || "./test-receipt.pdf";

  if (!fs.existsSync(filePath)) {
    console.error(`Error: File not found: ${filePath}`);
    console.error(`Usage: npx ts-node solution.ts <receipt-file-path>`);
    process.exit(1);
  }

  try {
    const result = await processReceipt(filePath);
    
    console.log("\n=== FINAL OUTPUT ===");
    console.log(JSON.stringify(result, null, 2));
    
    // Example downstream action: route flagged receipts to approval queue
    if (result.flagged_for_review) {
      console.log("\n⚠️  HIGH-VALUE RECEIPT: Routed to approval queue for human review.");
    } else {
      console.log("\n✓ Low-value receipt: Approved for auto-posting.");
    }
  } catch (error) {
    console.error("Pipeline error:", error);
    process.exit(1);
  }
}

main();
```

<SDK_CODE>
import { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

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

/**
 * Receipt Processing Pipeline
 * 
 * 1. Parse: Agentic OCR with signature detection
 * 2. Classify: Route by transaction value (< $200 vs >= $200)
 * 3. Extract: Structured JSON with review agent verification
 */

const receiptSchema = z.object({
  retailer: z.string().nullable().describe("Name of the retailer (e.g., 'Best Buy', 'Target')"),
  store_number: z.string().nullable().describe("Store identification number or location code"),
  store_address: z.string().nullable().describe("Full street address of the store"),
  store_city_state_zip: z.string().nullable().describe("City, state, and ZIP code of the store"),
  transaction_date: z.string().nullable().describe("Date of transaction in YYYY-MM-DD format"),
  transaction_time: z.string().nullable().describe("Time of transaction in HH:MM format (24-hour)"),
  items: z.array(
    z.object({
      description: z.string().nullable().describe("Item name or SKU description"),
      price: z.string().nullable().describe("Item price as printed (e.g., '$19.99')"),
    })
  ).describe("List of purchased items with individual prices"),
  subtotal: z.string().nullable().describe("Subtotal before tax (e.g., '$99.99')"),
  tax: z.string().nullable().describe("Sales tax amount (e.g., '$8.00')"),
  total: z.string().nullable().describe("Final total including tax (e.g., '$107.99')"),
  payment_method: z.string().nullable().describe("Payment type and last 4 digits (e.g., 'VISA ****1234')"),
  approval_number: z.string().nullable().describe("Payment approval/authorization code"),
});

interface ClassificationResult {
  classification_id: string;
  type: "other" | "flag";
  confidence: number;
}

interface ExtractionResult {
  retailer: string | null;
  store_number: string | null;
  store_address: string | null;
  store_city_state_zip: string | null;
  transaction_date: string | null;
  transaction_time: string | null;
  items: Array<{ description: string | null; price: string | null }>;
  subtotal: string | null;
  tax: string | null;
  total: string | null;
  payment_method: string | null;
  approval_number: string | null;
}

interface ReceiptProcessingOutput {
  classification: ClassificationResult;
  extraction: ExtractionResult;
  flagged_for_review: boolean;
}

async function processReceipt(filePath: string): Promise<ReceiptProcessingOutput> {
  console.log(`\n=== Receipt Processing Pipeline ===`);
  console.log(`Processing: ${path.basename(filePath)}`);

  // Convert local file to base64 data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const base64Data = fileBuffer.toString("base64");
  const dataUrl = `data:application/octet-stream;base64,${base64Data}`;

  // ─────────────────────────────────────────────────────────────
  //
import { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

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

/**
 * Receipt Processing Pipeline
 * 
 * 1. Parse: Agentic OCR with signature detection
 * 2. Classify: Route by transaction value (< $200 vs >= $200)
 * 3. Extract: Structured JSON with review agent verification
 */

const receiptSchema = z.object({
  retailer: z.string().nullable().describe("Name of the retailer (e.g., 'Best Buy', 'Target')"),
  store_number: z.string().nullable().describe("Store identification number or location code"),
  store_address: z.string().nullable().describe("Full street address of the store"),
  store_city_state_zip: z.string().nullable().describe("City, state, and ZIP code of the store"),
  transaction_date: z.string().nullable().describe("Date of transaction in YYYY-MM-DD format"),
  transaction_time: z.string().nullable().describe("Time of transaction in HH:MM format (24-hour)"),
  items: z.array(
    z.object({
      description: z.string().nullable().describe("Item name or SKU description"),
      price: z.string().nullable().describe("Item price as printed (e.g., '$19.99')"),
    })
  ).describe("List of purchased items with individual prices"),
  subtotal: z.string().nullable().describe("Subtotal before tax (e.g., '$99.99')"),
  tax: z.string().nullable().describe("Sales tax amount (e.g., '$8.00')"),
  total: z.string().nullable().describe("Final total including tax (e.g., '$107.99')"),
  payment_method: z.string().nullable().describe("Payment type and last 4 digits (e.g., 'VISA ****1234')"),
  approval_number: z.string().nullable().describe("Payment approval/authorization code"),
});

interface ClassificationResult {
  classification_id: string;
  type: "other" | "flag";
  confidence: number;
}

interface ExtractionResult {
  retailer: string | null;
  store_number: string | null;
  store_address: string | null;
  store_city_state_zip: string | null;
  transaction_date: string | null;
  transaction_time: string | null;
  items: Array<{ description: string | null; price: string | null }>;
  subtotal: string | null;
  tax: string | null;
  total: string | null;
  payment_method: string | null;
  approval_number: string | null;
}

interface ReceiptProcessingOutput {
  classification: ClassificationResult;
  extraction: ExtractionResult;
  flagged_for_review: boolean;
}

async function processReceipt(filePath: string): Promise<ReceiptProcessingOutput> {
  console.log(`\n=== Receipt Processing Pipeline ===`);
  console.log(`Processing: ${path.basename(filePath)}`);

  // Convert local file to base64 data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const base64Data = fileBuffer.toString("base64");
  const dataUrl = `data:application/octet-stream;base64,${base64Data}`;

  // ─────────────────────────────────────────────────────────────
  // STEP 1: Parse — Agentic OCR with signature detection
  // ─────────────────────────────────────────────────────────────
  console.log("\n[1/3] Parsing receipt with agentic OCR...");
  
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
  });

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

  console.log(`✓ Parse complete. Extracted ${parseRun.output.chunks.length} chunks.`);
  // parseRun.output.chunks contains markdown + bounding boxes; useful for debugging
  // but classification/extraction run on the file directly.

  // ─────────────────────────────────────────────────────────────
  // STEP 2: Classify — Route by transaction value
  // ─────────────────────────────────────────────────────────────
  console.log("\n[2/3] Classifying receipt by transaction value...");

  const classifyRun = await client.classifyRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      classifications: [
        {
          id: "classification1",
          type: "other",
          description: "Use the 'other' classification when the total receipt amount is under $200.",
        },
        {
          id: "classification_H7O",
          type: "flag",
          description: "Use the 'flag' classification when the total receipt amount is over $200.",
        },
      ],
      baseProcessor: "classification_performance",
      advancedOptions: {
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const classification = classifyRun.output.classification;
  const isFlagged = classification.type === "flag";
  
  console.log(`✓ Classification: ${classification.type.toUpperCase()} (confidence: ${(classification.confidence * 100).toFixed(1)}%)`);

  // ─────────────────────────────────────────────────────────────
  // STEP 3: Extract — Structured JSON with review agent
  // ─────────────────────────────────────────────────────────────
  console.log("\n[3/3] Extracting structured receipt data...");

  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: receiptSchema,
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: {
          enabled: true, // Always verify extraction, especially for flagged high-value receipts
        },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const extraction = extractRun.output.value as ExtractionResult;
  console.log(`✓ Extraction complete.`);
  console.log(`  Retailer: ${extraction.retailer || "(not found)"}`);
  console.log(`  Total: ${extraction.total || "(not found)"}`);
  console.log(`  Items: ${extraction.items.length}`);

  // ─────────────────────────────────────────────────────────────
  // Return structured output
  // ─────────────────────────────────────────────────────────────
  const result: ReceiptProcessingOutput = {
    classification: {
      classification_id: classification.id,
      type: classification.type as "other" | "flag",
      confidence: classification.confidence,
    },
    extraction,
    flagged_for_review: isFlagged,
  };

  return result;
}

// Main entry point for testing
async function main() {
  // For testing: accept file path from command-line argument or use a default
  const filePath = process.argv[2] || "./test-receipt.pdf";

  if (!fs.existsSync(filePath)) {
    console.error(`Error: File not found: ${filePath}`);
    console.error(`Usage: npx ts-node solution.ts <receipt-file-path>`);
    process.exit(1);
  }

  try {
    const result = await processReceipt(filePath);
    
    console.log("\n=== FINAL OUTPUT ===");
    console.log(JSON.stringify(result, null, 2));
    
    // Example downstream action: route flagged receipts to approval queue
    if (result.flagged_for_review) {
      console.log("\n⚠️  HIGH-VALUE RECEIPT: Routed to approval queue for human review.");
    } else {
      console.log("\n✓ Low-value receipt: Approved for auto-posting.");
    }
  } catch (error) {
    console.error("Pipeline error:", error);
    process.exit(1);
  }
}

main();
import os
import json
import sys
from pathlib import Path
from typing import TypedDict, Optional
from extend_ai import Extend


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


class ItemDict(TypedDict):
    description: Optional[str]
    price: Optional[str]


class ExtractionResult(TypedDict):
    retailer: Optional[str]
    store_number: Optional[str]
    store_address: Optional[str]
    store_city_state_zip: Optional[str]
    transaction_date: Optional[str]
    transaction_time: Optional[str]
    items: list[ItemDict]
    subtotal: Optional[str]
    tax: Optional[str]
    total: Optional[str]
    payment_method: Optional[str]
    approval_number: Optional[str]


class ClassificationResult(TypedDict):
    classification_id: str
    type: str
    confidence: float


class ReceiptProcessingOutput(TypedDict):
    classification: ClassificationResult
    extraction: ExtractionResult
    flagged_for_review: bool


receipt_schema = {
    "type": "object",
    "properties": {
        "retailer": {
            "type": ["string", "null"],
            "description": "Name of the retailer (e.g., 'Best Buy', 'Target')",
        },
        "store_number": {
            "type": ["string", "null"],
            "description": "Store identification number or location code",
        },
        "store_address": {
            "type": ["string", "null"],
            "description": "Full street address of the store",
        },
        "store_city_state_zip": {
            "type": ["string", "null"],
            "description": "City, state, and ZIP code of the store",
        },
        "transaction_date": {
            "type": ["string", "null"],
            "description": "Date of transaction in YYYY-MM-DD format",
        },
        "transaction_time": {
            "type": ["string", "null"],
            "description": "Time of transaction in HH:MM format (24-hour)",
        },
        "items": {
            "type": "array",
            "description": "List of purchased items with individual prices",
            "items": {
                "type": "object",
                "properties": {
                    "description": {
                        "type": ["string", "null"],
                        "description": "Item name or SKU description",
                    },
                    "price": {
                        "type": ["string", "null"],
                        "description": "Item price as printed (e.g., '$19.99')",
                    },
                },
            },
        },
        "subtotal": {
            "type": ["string", "null"],
            "description": "Subtotal before tax (e.g., '$99.99')",
        },
        "tax": {
            "type": ["string", "null"],
            "description": "Sales tax amount (e.g., '$8.00')",
        },
        "total": {
            "type": ["string", "null"],
            "description": "Final total including tax (e.g., '$107.99')",
        },
        "payment_method": {
            "type": ["string", "null"],
            "description": "Payment type and last 4 digits (e.g., 'VISA ****1234')",
        },
        "approval_number": {
            "type": ["string", "null"],
            "description": "Payment approval/authorization code",
        },
    },
}


def process_receipt(file_path: str) -> ReceiptProcessingOutput:
    """
    Receipt Processing Pipeline
    
    1. Parse: Agentic OCR with signature detection
    2. Classify: Route by transaction value (< $200 vs >= $200)
    3. Extract: Structured JSON with review agent verification
    """
    print(f"\n=== Receipt Processing Pipeline ===")
    print(f"Processing: {Path(file_path).name}")

    # Convert local file to base64 data URL for SDK compatibility
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    base64_data = __import__("base64").b64encode(file_buffer).decode("utf-8")
    data_url = f"data:application/octet-stream;base64,{base64_data}"

    # ─────────────────────────────────────────────────────────────
    # STEP 1: Parse — Agentic OCR with signature detection
    # ─────────────────────────────────────────────────────────────
    print("\n[1/3] Parsing receipt with agentic OCR...")

    parse_run = client.parse_runs.create_and_poll(
        file={"url": data_url},
    )

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

    print(f"✓ Parse complete. Extracted {len(parse_run.output.chunks)} chunks.")

    # ─────────────────────────────────────────────────────────────
    # STEP 2: Classify — Route by transaction value
    # ─────────────────────────────────────────────────────────────
    print("\n[2/3] Classifying receipt by transaction value...")

    classify_run = client.classify_runs.create_and_poll(
        file={"url": data_url},
        config={
            "classifications": [
                {
                    "id": "classification1",
                    "type": "other",
                    "description": "Use the 'other' classification when the total receipt amount is under $200.",
                },
                {
                    "id": "classification_H7O",
                    "type": "flag",
                    "description": "Use the 'flag' classification when the total receipt amount is over $200.",
                },
            ],
            "base_processor": "classification_performance",
            "advanced_options": {
                "advanced_multimodal_enabled": True,
            },
        },
    )

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

    classification = classify_run.output.classification
    is_flagged = classification.type == "flag"

    print(
        f"✓ Classification: {classification.type.upper()} (confidence: {classification.confidence * 100:.1f}%)"
    )

    # ─────────────────────────────────────────────────────────────
    # STEP 3: Extract — Structured JSON with review agent
    # ─────────────────────────────────────────────────────────────
    print("\n[3/3] Extracting structured receipt data...")

    extract_run = client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={
            "schema": receipt_schema,
            "base_processor": "extraction_performance",
            "advanced_options": {
                "review_agent": {
                    "enabled": True,
                },
                "advanced_multimodal_enabled": True,
            },
        },
    )

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

    extraction = extract_run.output.value

    print(f"✓ Extraction complete.")
    print(f"  Retailer: {extraction.get('retailer') or '(not found)'}")
    print(f"  Total: {extraction.get('total') or '(not found)'}")
    print(f"  Items: {len(extraction.get('items', []))}")

    # ─────────────────────────────────────────────────────────────
    # Return structured output
    # ─────────────────────────────────────────────────────────────
    result: ReceiptProcessingOutput = {
        "classification": {
            "classification_id": classification.id,
            "type": classification.type,
            "confidence": classification.confidence,
        },
        "extraction": extraction,
        "flagged_for_review": is_flagged,
    }

    return result


def main():
    # For testing: accept file path from command-line argument or use a default
    file_path = sys.argv[1] if len(sys.argv) > 1 else "./test-receipt.pdf"

    if not Path(file_path).exists():
        print(f"Error: File not found: {file_path}")
        print(f"Usage: python solution.py <receipt-file-path>")
        sys.exit(1)

    try:
        result = process_receipt(file_path)

        print("\n=== FINAL OUTPUT ===")
        print(json.dumps(result, indent=2))

        # Example downstream action: route flagged receipts to approval queue
        if result["flagged_for_review"]:
            print(
                "\n⚠️  HIGH-VALUE RECEIPT: Routed to approval queue for human review."
            )
        else:
            print("\n✓ Low-value receipt: Approved for auto-posting.")

    except Exception as error:
        print(f"Pipeline error: {error}")
        sys.exit(1)


if __name__ == "__main__":
    main()
// This code uses the Extend REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with java.net.http.HttpClient (no external dependencies).

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReceiptProcessor {

    private static final String API_BASE = "https://api.extend.ai";
    private static final String API_KEY = System.getenv("EXTEND_API_KEY");
    private static final HttpClient httpClient = HttpClient.newHttpClient();

    static class Item {
        public String description;
        public String price;

        public Item(String description, String price) {
            this.description = description;
            this.price = price;
        }
    }

    static class ExtractionResult {
        public String retailer;
        public String store_number;
        public String store_address;
        public String store_city_state_zip;
        public String transaction_date;
        public String transaction_time;
        public List<Item> items;
        public String subtotal;
        public String tax;
        public String total;
        public String payment_method;
        public String approval_number;

        public ExtractionResult() {
            this.items = new ArrayList<>();
        }
    }

    static class ClassificationResult {
        public String classification_id;
        public String type;
        public double confidence;
    }

    static class ReceiptProcessingOutput {
        public ClassificationResult classification;
        public ExtractionResult extraction;
        public boolean flagged_for_review;
    }

    private static String makeRequest(String method, String endpoint, String jsonBody) throws IOException, InterruptedException {
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + endpoint))
                .header("Authorization", "Bearer " + API_KEY)
                .header("Content-Type", "application/json");

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

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

        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IOException("API error: " + response.statusCode() + " " + response.body());
        }

        return response.body();
    }

    private static String extractJsonField(String json, String fieldName) {
        String pattern = "\"" + fieldName + "\":";
        int startIdx = json.indexOf(pattern);
        if (startIdx == -1) return null;
        startIdx += pattern.length();
        while (startIdx < json.length() && Character.isWhitespace(json.charAt(startIdx))) {
            startIdx++;
        }
        if (startIdx >= json.length()) return null;

        int endIdx = startIdx;
        if (json.charAt(startIdx) == '"') {
            endIdx++;
            while (endIdx < json.length() && json.charAt(endIdx) != '"') {
                if (json.charAt(endIdx) == '\\') endIdx++;
                endIdx++;
            }
            endIdx++;
        } else if (json.charAt(startIdx) == '{' || json.charAt(startIdx) == '[') {
            int depth = 1;
            endIdx = startIdx + 1;
            while (endIdx < json.length() && depth > 0) {
                if (json.charAt(endIdx) == '{' || json.charAt(endIdx) == '[') depth++;
                else if (json.charAt(endIdx) == '}' || json.charAt(endIdx) == ']') depth--;
                endIdx++;
            }
        } else {
            while (endIdx < json.length() && json.charAt(endIdx) != ',' && json.charAt(endIdx) != '}' && json.charAt(endIdx) != ']') {
                endIdx++;
            }
        }

        return json.substring(startIdx, endIdx).trim();
    }

    private static String pollForCompletion(String runId, String runType) throws IOException, InterruptedException {
        long startTime = System.currentTimeMillis();
        long timeout = 300000; // 5 minutes

        while (System.currentTimeMillis() - startTime < timeout) {
            String response = makeRequest("GET", "/" + runType + "/" + runId, null);
            if (response.contains("\"status\":\"PROCESSED\"")) {
                return response;
            }
            Thread.sleep(2000);
        }

        throw new IOException("Polling timeout for " + runType + " " + runId);
    }

    public static ReceiptProcessingOutput processReceipt(String filePath) throws IOException, InterruptedException {
        System.out.println("\n=== Receipt Processing Pipeline ===");
        System.out.println("Processing: " + Paths.get(filePath).getFileName());

        // Convert file to base64 data URL
        byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
        String base64Data = Base64.getEncoder().encodeToString(fileBytes);
        String dataUrl = "data:application/octet-stream;base64," + base64Data;

        // ─────────────────────────────────────────────────────────────
        // STEP 1: Parse — Agentic OCR with signature detection
        // ─────────────────────────────────────────────────────────────
        System.out.println("\n[1/3] Parsing receipt with agentic OCR...");

        String parseBody = "{\"file\":{\"url\":\"" + dataUrl.replace("\"", "\\\"") + "\"}}";
        String parseResponse = makeRequest("POST", "/parse_runs", parseBody);
        String parseRunId = extractJsonField(parseResponse, "id");

        String parseResult = pollForCompletion(parseRunId, "parse_runs");
        if (!parseResult.contains("\"status\":\"PROCESSED\"")) {
            throw new IOException("Parse failed");
        }

        System.out.println("✓ Parse complete.");

        // ─────────────────────────────────────────────────────────────
        // STEP 2: Classify — Route by transaction value
        // ─────────────────────────────────────────────────────────────
        System.out.println("\n[2/3] Classifying receipt by transaction value...");

        String classifyBody = "{"
                + "\"file\":{\"url\":\"" + dataUrl.replace("\"", "\\\"") + "\"},"
                + "\"config\":{"
                + "\"classifications\":["
                + "{\"id\":\"classification1\",\"type\":\"other\",\"description\":\"Use the 'other' classification when the total receipt amount is under $200.\"},"
                + "{\"id\":\"classification_H7O\",\"type\":\"flag\",\"description\":\"Use the 'flag' classification when the total receipt amount is over $200.\"}"
                + "],"
                + "\"baseProcessor\":\"classification_performance\","
                + "\"advancedOptions\":{\"advancedMultimodalEnabled\":true}"
                + "}"
                + "}";

        String classifyResponse = makeRequest("POST", "/classify_runs", classifyBody);
        String classifyRunId = extractJsonField(classifyResponse, "id");

        String classifyResult = pollForCompletion(classifyRunId, "classify_runs");
        if (!classifyResult.contains("\"status\":\"PROCESSED\"")) {
            throw new IOException("Classification failed");
        }

        String classType = extractJsonField(classifyResult, "type");
        String classId = extractJsonField(classifyResult, "id");
        String confidenceStr = extractJsonField(classifyResult, "confidence");
        double confidence = confidenceStr != null ? Double.parseDouble(confidenceStr) : 0.0;
        boolean isFlagged = "flag".equals(classType);

        System.out.println("✓ Classification: " + classType.toUpperCase() + " (confidence: " + String.format("%.1f", confidence * 100) + "%)");

        // ─────────────────────────────────────────────────────────────
        // STEP 3: Extract — Structured JSON with review agent
        // ─────────────────────────────────────────────────────────────
        System.out.println("\n[3/3] Extracting structured receipt data...");

        String receiptSchema = "{\"type\":\"object\",\"properties\":{"
                + "\"retailer\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the retailer\"},"
                + "\"store_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Store identification number\"},"
                + "\"store_address\":{\"type\":[\"string\",\"null\"],\"description\":\"Full street address of the store\"},"
                + "\"store_city_state_zip\":{\"type\":[\"string\",\"null\"],\"description\":\"City, state, and ZIP code of the store\"},"
                + "\"transaction_date\":{\"type\":[\"string\",\"null\"],\"description\":\"Date of transaction in YYYY-MM-DD format\"},"
                + "\"transaction_time\":{\"type\":[\"string\",\"null\"],\"description\":\"Time of transaction in HH:MM format\"},"
                + "\"items\":{\"type\":\"array\",\"description\":\"List of purchased items\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":[\"string\",\"null\"]},\"price\":{\"type\":[\"string\",\"null\"]}}}},"
                + "\"subtotal\":{\"type\":[\"string\",\"null\"],\"description\":\"Subtotal amount before tax\"},"
                + "\"tax\":{\"type\":[\"string\",\"null\"],\"description\":\"Tax amount\"},"
                + "\"total\":{\"type\":[\"string\",\"null\"],\"description\":\"Final total amount\"},"
                + "\"payment_method\":{\"type\":[\"string\",\"null\"],\"description\":\"Payment type and last four digits\"},"
                + "\"approval_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Payment approval number\"}"
                + "}}";

        String extractBody = "{"
                + "\"file\":{\"url\":\"" + dataUrl.replace("\"", "\\\"") + "\"},"
                + "\"config\":{"
                + "\"schema\":" + receiptSchema + ","
                + "\"baseProcessor\":\"extraction_performance\","
                + "\"advancedOptions\":{\"reviewAgent\":{\"enabled\":true},\"advancedMultimodalEnabled\":true}"
                + "}"
                + "}";

        String extractResponse = makeRequest("POST", "/extract_runs", extractBody);
        String extractRunId = extractJsonField(extractResponse, "id");

        String extractResult = pollForCompletion(extractRunId, "extract_runs");
        if (!extractResult.contains("\"status\":\"PROCESSED\"")) {
            throw new IOException("Extraction failed");
        }

        ExtractionResult extraction = new ExtractionResult();
        extraction.retailer = extractJsonField(extractResult, "retailer");
        extraction.store_number = extractJsonField(extractResult, "store_number");
        extraction.store_address = extractJsonField(extractResult, "store_address");
        extraction.store_city_state_zip = extractJsonField(extractResult, "store_city_state_zip");
        extraction.transaction_date = extractJsonField(extractResult, "transaction_date");
        extraction.transaction_time = extractJsonField(extractResult, "transaction_time");
        extraction.subtotal = extractJsonField(extractResult, "subtotal");
        extraction.tax = extractJsonField(extractResult, "tax");
        extraction.total = extractJsonField(extractResult, "total");
        extraction.payment_method = extractJsonField(extractResult, "payment_method");
        extraction.approval_number = extractJsonField(extractResult, "approval_number");

        System.out.println("✓ Extraction complete.");
        System.out.println("  Retailer: " + (extraction.retailer != null ? extraction.retailer : "(not found)"));
        System.out.println("  Total: " + (extraction.total != null ? extraction.total : "(not found)"));
        System.out.println("  Items: " + extraction.items.size());

        // ─────────────────────────────────────────────────────────────
        // Return structured output
        // ─────────────────────────────────────────────────────────────
        ReceiptProcessingOutput result = new ReceiptProcessingOutput();
        result.classification = new ClassificationResult();
        result.classification.classification_id = classId;
        result.classification.type = classType;
        result.classification.confidence = confidence;
        result.extraction = extraction;
        result.flagged_for_review = isFlagged;

        return result;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        String filePath = args.length > 0 ? args[0] : "./test-receipt.pdf";

        if (!Files.exists(Paths.get(filePath))) {
            System.err.println("Error: File not found: " + filePath);
            System.err.println("Usage: java ReceiptProcessor <receipt-file-path>");
            System.exit(1);
        }

        try {
            ReceiptProcessingOutput result = processReceipt(filePath);

            System.out.println("\n=== FINAL OUTPUT ===");
            System.out.println("Classification ID: " + result.classification.classification_id);
            System.out.println("Classification Type: " + result.classification.type);
            System.out.println("Confidence: " + String.format("%.1f", result.classification.confidence * 100) + "%");
            System.out.println("Retailer: " + (result.extraction.retailer != null ? result.extraction.retailer : "(not found)"));
            System.out.println("Total: " + (result.extraction.total != null ? result.extraction.total : "(not found)"));
            System.out.println("Flagged for Review: " + result.flagged_for_review);

            if (result.flagged_for_review) {
                System.out.println("\n⚠️  HIGH-VALUE RECEIPT: Routed to approval queue for human review.");
            } else {
                System.out.println("\n✓ Low-value receipt: Approved for auto-posting.");
            }
        } catch (Exception e) {
            System.err.println("Pipeline error: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.

package main

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

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

type Item struct {
	Description *string `json:"description"`
	Price       *string `json:"price"`
}

type ExtractionResult struct {
	Retailer          *string `json:"retailer"`
	StoreNumber       *string `json:"store_number"`
	StoreAddress      *string `json:"store_address"`
	StoreCityStateZip *string `json:"store_city_state_zip"`
	TransactionDate   *string `json:"transaction_date"`
	TransactionTime   *string `json:"transaction_time"`
	Items             []Item  `json:"items"`
	Subtotal          *string `json:"subtotal"`
	Tax               *string `json:"tax"`
	Total             *string `json:"total"`
	PaymentMethod     *string `json:"payment_method"`
	ApprovalNumber    *string `json:"approval_number"`
}

type ClassificationResult struct {
	ClassificationID string  `json:"classification_id"`
	Type             string  `json:"type"`
	Confidence       float64 `json:"confidence"`
}

type ReceiptProcessingOutput struct {
	Classification ClassificationResult `json:"classification"`
	Extraction     ExtractionResult     `json:"extraction"`
	FlaggedForReview bool                `json:"flagged_for_review"`
}

type ParseRunOutput struct {
	Chunks []interface{} `json:"chunks"`
}

type ParseRunResponse struct {
	Status string         `json:"status"`
	Output ParseRunOutput `json:"output"`
}

type ClassifyRunClassification struct {
	ID         string  `json:"id"`
	Type       string  `json:"type"`
	Confidence float64 `json:"confidence"`
}

type ClassifyRunOutput struct {
	Classification ClassifyRunClassification `json:"classification"`
}

type ClassifyRunResponse struct {
	Status string           `json:"status"`
	Output ClassifyRunOutput `json:"output"`
}

type ExtractRunOutput struct {
	Value ExtractionResult `json:"value"`
}

type ExtractRunResponse struct {
	Status string         `json:"status"`
	Output ExtractRunOutput `json:"output"`
}

func makeRequest(method, endpoint string, body interface{}, apiKey string) ([]byte, error) {
	url := baseURL + endpoint
	var reqBody io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewBuffer(jsonBody)
	}

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

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

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

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

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

	return respBody, nil
}

func pollParseRun(runID string, apiKey string) (*ParseRunResponse, error) {
	for {
		respBody, err := makeRequest("GET", fmt.Sprintf("/parse-runs/%s", runID), nil, apiKey)
		if err != nil {
			return nil, err
		}

		var result ParseRunResponse
		if err := json.Unmarshal(respBody, &result); err != nil {
			return nil, err
		}

		if result.Status == "PROCESSED" || result.Status == "FAILED" {
			return &result, nil
		}

		time.Sleep(2 * time.Second)
	}
}

func pollClassifyRun(runID string, apiKey string) (*ClassifyRunResponse, error) {
	for {
		respBody, err := makeRequest("GET", fmt.Sprintf("/classify-runs/%s", runID), nil, apiKey)
		if err != nil {
			return nil, err
		}

		var result ClassifyRunResponse
		if err := json.Unmarshal(respBody, &result); err != nil {
			return nil, err
		}

		if result.Status == "PROCESSED" || result.Status == "FAILED" {
			return &result, nil
		}

		time.Sleep(2 * time.Second)
	}
}

func pollExtractRun(runID string, apiKey string) (*ExtractRunResponse, error) {
	for {
		respBody, err := makeRequest("GET", fmt.Sprintf("/extract-runs/%s", runID), nil, apiKey)
		if err != nil {
			return nil, err
		}

		var result ExtractRunResponse
		if err := json.Unmarshal(respBody, &result); err != nil {
			return nil, err
		}

		if result.Status == "PROCESSED" || result.Status == "FAILED" {
			return &result, nil
		}

		time.Sleep(2 * time.Second)
	}
}

func processReceipt(filePath string, apiKey string) (*ReceiptProcessingOutput, error) {
	fmt.Printf("\n=== Receipt Processing Pipeline ===\n")
	fmt.Printf("Processing: %s\n", filepath.Base(filePath))

	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

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

	// STEP 1: Parse
	fmt.Println("\n[1/3] Parsing receipt with agentic OCR...")

	parseReqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
	}

	parseRespBody, err := makeRequest("POST", "/parse-runs", parseReqBody, apiKey)
	if err != nil {
		return nil, err
	}

	var parseCreateResp map[string]interface{}
	if err := json.Unmarshal(parseRespBody, &parseCreateResp); err != nil {
		return nil, err
	}

	parseRunID, ok := parseCreateResp["id"].(string)
	if !ok {
		return nil, fmt.Errorf("parse run ID not found in response")
	}

	parseRun, err := pollParseRun(parseRunID, apiKey)
	if err != nil {
		return nil, err
	}

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

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

	// STEP 2: Classify
	fmt.Println("\n[2/3] Classifying receipt by transaction value...")

	classifyReqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"classifications": []map[string]interface{}{
				{
					"id":          "classification1",
					"type":        "other",
					"description": "Use the 'other' classification when the total receipt amount is under $200.",
				},
				{
					"id":          "classification_H7O",
					"type":        "flag",
					"description": "Use the 'flag' classification when the total receipt amount is over $200.",
				},
			},
			"baseProcessor": "classification_performance",
			"advancedOptions": map[string]interface{}{
				"advancedMultimodalEnabled": true,
			},
		},
	}

	classifyRespBody, err := makeRequest("POST", "/classify-runs", classifyReqBody, apiKey)
	if err != nil {
		return nil, err
	}

	var classifyCreateResp map[string]interface{}
	if err := json.Unmarshal(classifyRespBody, &classifyCreateResp); err != nil {
		return nil, err
	}

	classifyRunID, ok := classifyCreateResp["id"].(string)
	if !ok {
		return nil, fmt.Errorf("classify run ID not found in response")
	}

	classifyRun, err := pollClassifyRun(classifyRunID, apiKey)
	if err != nil {
		return nil, err
	}

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

	classification := classifyRun.Output.Classification
	isFlagged := classification.Type == "flag"

	fmt.Printf("✓ Classification: %s (confidence: %.1f%%)\n", 
		classification.Type, classification.Confidence*100)

	// STEP 3: Extract
	fmt.Println("\n[3/3] Extracting structured receipt data...")

	receiptSchema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"retailer": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Name of the retailer (e.g., 'Best Buy', 'Target')",
			},
			"store_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Store identification number or location code",
			},
			"store_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Full street address of the store",
			},
			"store_city_state_zip": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "City, state, and ZIP code of the store",
			},
			"transaction_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Date of transaction in YYYY-MM-DD format",
			},
			"transaction_time": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Time of transaction in HH:MM format (24-hour)",
			},
			"items": map[string]interface{}{
				"type":        "array",
				"description": "List of purchased items with individual prices",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"description": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Item name or SKU description",
						},
						"price": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Item price as printed (e.g., '$19.99')",
						},
					},
				},
			},
			"subtotal": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Subtotal before tax (e.g., '$99.99')",
			},
			"tax": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Sales tax amount (e.g., '$8.00')",
			},
			"total": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Final total including tax (e.g., '$107.99')",
			},
			"payment_method": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Payment type and last 4 digits (e.g., 'VISA ****1234')",
			},
			"approval_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Payment approval/authorization code",
			},
		},
	}

	extractReqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"schema":        receiptSchema,
			"baseProcessor": "extraction_performance",
			"advancedOptions": map[string]interface{}{
				"reviewAgent": map[string]interface{}{
					"enabled": true,
				},
				"advancedMultimodalEnabled": true,
			},
		},
	}

	extractRespBody, err := makeRequest("POST", "/extract-runs", extractReqBody, apiKey)
	if err != nil {
		return nil, err
	}

	var extractCreateResp map[string]interface{}
	if err := json.Unmarshal(extractRespBody, &extractCreateResp); err != nil {
		return nil, err
	}

	extractRunID, ok := extractCreateResp["id"].(string)
	if !ok {
		return nil, fmt.Errorf("extract run ID not found in response")
	}

	extractRun, err := pollExtractRun(extractRunID, apiKey)
	if err != nil {
		return nil, err
	}

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

	extraction := extractRun.Output.Value
	fmt.Println("✓ Extraction complete.")
	if extraction.Retailer != nil {
		fmt.Printf("  Retailer: %s\n", *extraction.Retailer)
	} else {
		fmt.Println("  Retailer: (not found)")
	}
	if extraction.Total != nil {
		fmt.Printf("  Total: %s\n", *extraction.Total)
	} else {
		fmt.Println("  Total: (not found)")
	}
	fmt.Printf("  Items: %d\n", len(extraction.Items))

	result := &ReceiptProcessingOutput{
		Classification: ClassificationResult{
			ClassificationID: classification.ID,
			Type:             classification.Type,
			Confidence:       classification.Confidence,
		},
		Extraction:       extraction,
		FlaggedForReview: isFlagged,
	}

	return result, nil
}

func main() {
	filePath := flag.String("file", "./test-receipt.pdf", "Path to receipt file")
	flag.Parse()

	if flag.NArg() > 0 {
		*filePath = flag.Arg(0)
	}

	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Error: EXTEND_API_KEY environment variable not set\n")
		os.Exit(1)
	}

	if _, err := os.Stat(*filePath); err != nil {
		fmt.Fprintf(os.Stderr, "Error: File not found: %s\n", *filePath)
		fmt.Fprintf(os.Stderr, "Usage: go run solution.go <receipt-file-path>\n")
		os.Exit(1)
	}

	result, err := processReceipt(*filePath, apiKey)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Pipeline error: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("\n=== FINAL OUTPUT ===")
	jsonOutput, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error marshaling output: %v\n", err)
		os.Exit(1)
	}
	fmt.Println(string(jsonOutput))

	if result.FlaggedForReview {
		fmt.Println("\n⚠️  HIGH-VALUE RECEIPT: Routed to approval queue for human review.")
	} else {
		fmt.Println("\n✓ Low-value receipt: Approved for auto-posting.")
	}
}
// Deploy the "Receipt" 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/receipt-classifier-extractor.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: receipt-classifier-extractor).

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, "receipt-classifier-extractor.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": "Receipt Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": true
            },
            "tables": {
              "agentic": {
                "enabled": true
              },
              "tableHeaderContinuationEnabled": true
            },
            "figures": {
              "enabled": true
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {}
          }
        }
      },
      "next": [
        {
          "step": "classify2"
        }
      ]
    },
    {
      "name": "classify2",
      "type": "CLASSIFY",
      "config": {
        "classifierConfig": {
          "classifications": [
            {
              "id": "classification1",
              "type": "other",
              "description": "Use the \"other\" classification when the total receipt amount is under $200."
            },
            {
              "id": "classification_H7O",
              "type": "flag",
              "description": "Use the \"flag\" classification when the total receipt amount is over $200."
            }
          ],
          "baseProcessor": "classification_performance",
          "advancedOptions": {
            "advancedMultimodalEnabled": true
          }
        }
      },
      "next": [
        {
          "step": "extraction3",
          "classificationId": "classification1"
        },
        {
          "step": "extraction3",
          "classificationId": "classification_H7O"
        }
      ]
    },
    {
      "name": "extraction3",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "tax": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Tax amount"
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "price": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "description": "List of purchased items"
              },
              "total": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Final total amount"
              },
              "retailer": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the retailer"
              },
              "subtotal": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Subtotal amount before tax"
              },
              "store_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Store identification number"
              },
              "store_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Full street address of the store"
              },
              "payment_method": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Payment type and last four digits"
              },
              "approval_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Payment approval number"
              },
              "transaction_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Date of transaction in YYYY-MM-DD format"
              },
              "transaction_time": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Time of transaction in HH:MM format"
              },
              "store_city_state_zip": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "City, state, and ZIP code of the store"
              }
            }
          },
          "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 "Receipt" 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/receipt-classifier-extractor.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: receipt-classifier-extractor).
"""

import json
import os
import sys
from pathlib import Path

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 / "receipt-classifier-extractor.json"

state = {}
if STATE_FILE.exists():
    state = json.loads(STATE_FILE.read_text())


def save_state():
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ── Workflow definition — extractor/classifier/splitter configs inline ──────
WORKFLOW = {
    "name": "Receipt Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {"enabled": True},
                            "signatureDetectionEnabled": True,
                        },
                        "tables": {
                            "agentic": {"enabled": True},
                            "tableHeaderContinuationEnabled": True,
                        },
                        "figures": {"enabled": True},
                    },
                    "chunkingStrategy": {"type": "page", "options": {}},
                }
            },
            "next": [{"step": "classify2"}],
        },
        {
            "name": "classify2",
            "type": "CLASSIFY",
            "config": {
                "classifierConfig": {
                    "classifications": [
                        {
                            "id": "classification1",
                            "type": "other",
                            "description": 'Use the "other" classification when the total receipt amount is under $200.',
                        },
                        {
                            "id": "classification_H7O",
                            "type": "flag",
                            "description": 'Use the "flag" classification when the total receipt amount is over $200.',
                        },
                    ],
                    "baseProcessor": "classification_performance",
                    "advancedOptions": {"advancedMultimodalEnabled": True},
                }
            },
            "next": [
                {"step": "extraction3", "classificationId": "classification1"},
                {"step": "extraction3", "classificationId": "classification_H7O"},
            ],
        },
        {
            "name": "extraction3",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "retailer": {
                                "type": ["string", "null"],
                                "description": "Name of the retailer",
                            },
                            "store_number": {
                                "type": ["string", "null"],
                                "description": "Store identification number",
                            },
                            "store_address": {
                                "type": ["string", "null"],
                                "description": "Full street address of the store",
                            },
                            "store_city_state_zip": {
                                "type": ["string", "null"],
                                "description": "City, state, and ZIP code of the store",
                            },
                            "transaction_date": {
                                "type": ["string", "null"],
                                "description": "Date of transaction in YYYY-MM-DD format",
                            },
                            "transaction_time": {
                                "type": ["string", "null"],
                                "description": "Time of transaction in HH:MM format",
                            },
                            "items": {
                                "type": "array",
                                "description": "List of purchased items",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "description": {
                                            "type": ["string", "null"],
                                        },
                                        "price": {
                                            "type": ["string", "null"],
                                        },
                                    },
                                },
                            },
                            "subtotal": {
                                "type": ["string", "null"],
                                "description": "Subtotal amount before tax",
                            },
                            "tax": {
                                "type": ["string", "null"],
                                "description": "Tax amount",
                            },
                            "total": {
                                "type": ["string", "null"],
                                "description": "Final total amount",
                            },
                            "payment_method": {
                                "type": ["string", "null"],
                                "description": "Payment type and last four digits",
                            },
                            "approval_number": {
                                "type": ["string", "null"],
                                "description": "Payment approval number",
                            },
                        },
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {"enabled": True},
                        "advancedMultimodalEnabled": True,
                    },
                }
            },
        },
    ],
}


def main():
    client = Extend(token=API_KEY)

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

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

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

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

    print("\nDone. Run documents through it with:")
    print(
        f'  POST https://api.extend.ai/workflow_runs  {{ "workflow": {{ "id": "{state["workflowId"]}" }}, "file": {{ "url": "https://…" }} }}'
    )
    print("Or open the workflow in the Extend dashboard to review and deploy it.")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This code calls Extend's REST API directly because Extend has no official Java SDK yet.
// The REST API is the source of truth; the TypeScript SDK is a thin wrapper over it.

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ReceiptProvisioner {
  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("receipt-classifier-extractor.json");
  private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  static {
    if (API_KEY == null || API_KEY.isEmpty()) {
      System.err.println("Set EXTEND_API_KEY first.");
      System.exit(1);
    }
  }

  static class State {
    String workflowId;
  }

  private static State state = new State();

  public static void main(String[] args) throws Exception {
    loadState();
    main();
  }

  private static void loadState() throws IOException {
    if (Files.exists(STATE_FILE)) {
      String json = Files.readString(STATE_FILE);
      state = parseStateJson(json);
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    String json = toJson(state);
    Files.writeString(STATE_FILE, json);
  }

  private static String parseStateJson(String json) {
    // Simple JSON parser for { "workflowId": "..." }
    int idx = json.indexOf("\"workflowId\"");
    if (idx == -1) return null;
    idx = json.indexOf("\"", idx + 12);
    if (idx == -1) return null;
    int end = json.indexOf("\"", idx + 1);
    if (end == -1) return null;
    return json.substring(idx + 1, end);
  }

  private static State parseStateJson(String json) {
    State s = new State();
    s.workflowId = parseStateJson(json);
    return s;
  }

  private static String toJson(State state) {
    if (state.workflowId == null) {
      return "{}";
    }
    return "{\"workflowId\":\"" + escapeJson(state.workflowId) + "\"}";
  }

  private static String escapeJson(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"");
  }

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

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

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

    Map<String, Object> data = parseJsonResponse(response.body());
    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String errorMsg = toJsonString(data);
      if (errorMsg.length() > 300) {
        errorMsg = errorMsg.substring(0, 300);
      }
      throw new RuntimeException(
          method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
    }
    return data;
  }

  private static Map<String, Object> buildWorkflow() {
    Map<String, Object> workflow = new LinkedHashMap<>();
    workflow.put("name", "Receipt 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, Object>> nextStart = new ArrayList<>();
    Map<String, Object> nextStartItem = new LinkedHashMap<>();
    nextStartItem.put("step", "parse1");
    nextStart.add(nextStartItem);
    startTrigger.put("next", nextStart);
    steps.add(startTrigger);

    // parse1
    Map<String, Object> parse = new LinkedHashMap<>();
    parse.put("name", "parse1");
    parse.put("type", "PARSE");
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> parseConfigInner = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> textBlock = new LinkedHashMap<>();
    Map<String, Object> agenticText = new LinkedHashMap<>();
    agenticText.put("enabled", true);
    textBlock.put("agentic", agenticText);
    textBlock.put("signatureDetectionEnabled", true);
    blockOptions.put("text", textBlock);
    Map<String, Object> tablesBlock = new LinkedHashMap<>();
    Map<String, Object> agenticTables = new LinkedHashMap<>();
    agenticTables.put("enabled", true);
    tablesBlock.put("agentic", agenticTables);
    tablesBlock.put("tableHeaderContinuationEnabled", true);
    blockOptions.put("tables", tablesBlock);
    Map<String, Object> figuresBlock = new LinkedHashMap<>();
    figuresBlock.put("enabled", true);
    blockOptions.put("figures", figuresBlock);
    parseConfigInner.put("blockOptions", blockOptions);
    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "page");
    chunkingStrategy.put("options", new LinkedHashMap<>());
    parseConfigInner.put("chunkingStrategy", chunkingStrategy);
    parseConfig.put("parseConfig", parseConfigInner);
    parse.put("config", parseConfig);
    List<Map<String, Object>> nextParse = new ArrayList<>();
    Map<String, Object> nextParseItem = new LinkedHashMap<>();
    nextParseItem.put("step", "classify2");
    nextParse.add(nextParseItem);
    parse.put("next", nextParse);
    steps.add(parse);

    // classify2
    Map<String, Object> classify = new LinkedHashMap<>();
    classify.put("name", "classify2");
    classify.put("type", "CLASSIFY");
    Map<String, Object> classifyConfig = new LinkedHashMap<>();
    Map<String, Object> classifierConfig = new LinkedHashMap<>();
    List<Map<String, Object>> classifications = new ArrayList<>();
    Map<String, Object> class1 = new LinkedHashMap<>();
    class1.put("id", "classification1");
    class1.put("type", "other");
    class1.put("description", "Use the \"other\" classification when the total receipt amount is under $200.");
    classifications.add(class1);
    Map<String, Object> class2 = new LinkedHashMap<>();
    class2.put("id", "classification_H7O");
    class2.put("type", "flag");
    class2.put("description", "Use the \"flag\" classification when the total receipt amount is over $200.");
    classifications.add(class2);
    classifierConfig.put("classifications", classifications);
    classifierConfig.put("baseProcessor", "classification_performance");
    Map<String, Object> advancedOptions = new LinkedHashMap<>();
    advancedOptions.put("advancedMultimodalEnabled", true);
    classifierConfig.put("advancedOptions", advancedOptions);
    classifyConfig.put("classifierConfig", classifierConfig);
    classify.put("config", classifyConfig);
    List<Map<String, Object>> nextClassify = new ArrayList<>();
    Map<String, Object> nextClassify1 = new LinkedHashMap<>();
    nextClassify1.put("step", "extraction3");
    nextClassify1.put("classificationId", "classification1");
    nextClassify.add(nextClassify1);
    Map<String, Object> nextClassify2 = new LinkedHashMap<>();
    nextClassify2.put("step", "extraction3");
    nextClassify2.put("classificationId", "classification_H7O");
    nextClassify.add(nextClassify2);
    classify.put("next", nextClassify);
    steps.add(classify);

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

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

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

    properties.put("retailer", buildStringProperty("Name of the retailer"));
    properties.put("store_number", buildStringProperty("Store identification number"));
    properties.put("store_address", buildStringProperty("Full street address of the store"));
    properties.put("store_city_state_zip", buildStringProperty("City, state, and ZIP code of the store"));
    properties.put("transaction_date", buildStringProperty("Date of transaction in YYYY-MM-DD format"));
    properties.put("transaction_time", buildStringProperty("Time of transaction in HH:MM format"));

    Map<String, Object> items = new LinkedHashMap<>();
    items.put("type", "array");
    items.put("description", "List of purchased items");
    Map<String, Object> itemsSchema = new LinkedHashMap<>();
    itemsSchema.put("type", "object");
    Map<String, Object> itemProps = new LinkedHashMap<>();
    itemProps.put("description", buildStringProperty(null));
    itemProps.put("price", buildStringProperty(null));
    itemsSchema.put("properties", itemProps);
    items.put("items", itemsSchema);
    properties.put("items", items);

    properties.put("subtotal", buildStringProperty("Subtotal amount before tax"));
    properties.put("tax", buildStringProperty("Tax amount"));
    properties.put("total", buildStringProperty("Final total amount"));
    properties.put("payment_method", buildStringProperty("Payment type and last four digits"));
    properties.put("approval_number", buildStringProperty("Payment approval number"));

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

  private static Map<String, Object> buildStringProperty(String description) {
    Map<String, Object> prop = new LinkedHashMap<>();
    List<String> types = new ArrayList<>();
    types.add("string");
    types.add("null");
    prop.put("type", types);
    if (description != null) {
      prop.put("description", description);
    }
    return prop;
  }

  private static void main() throws Exception {
    Map<String, Object> workflow = buildWorkflow();
    String workflowName = (String) workflow.get("name");
    System.out.println("Deploying \"" + workflowName + "\"…");

    if (state.workflowId != null) {
      System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
      Map<String, Object> updateBody = new LinkedHashMap<>();
      updateBody.put("steps", workflow.get("steps"));
      api("POST", "/workflows/" + state.workflowId, updateBody);
    } else {
      try {
        String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
        Map<String, Object> list = api("GET", "/workflows?name=" + encodedName, null);
        List<Map<String, Object>> items = (List<Map<String, Object>>) (list.get("data") != null ? list.get("data")
            : (list.get("items") != null ? list.get("items") : new ArrayList<>()));
        Map<String, Object> existing = null;
        for (Map<String, Object> item : items) {
          if (workflowName.equals(item.get("name"))) {
            existing = item;
            break;
          }
        }
        if (existing != null && existing.get("id") != null) {
          state.workflowId = (String) existing.get("id");
          saveState();
          System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + state.workflowId
              + ") — updating steps");
          Map<String, Object> updateBody = new LinkedHashMap<>();
          updateBody.put("steps", workflow.get("steps"));
          api("POST", "/workflows/" + state.workflowId, updateBody);
        }
      } catch (Exception e) {
        // lookup is best-effort; fall through to create
      }

      if (state.workflowId == null) {
        Map<String, Object> created = api("POST", "/workflows", workflow);
        String wfId = (String) created.get("id");
        if (wfId == null) {
          Map<String, Object> workflowObj = (Map<String, Object>) created.get("workflow");
          if (workflowObj != null) {
            wfId = (String) workflowObj.get("id");
          }
        }
        if (wfId == null) {
          throw new RuntimeException("Could not read created workflow id from response");
        }
        state.workflowId = wfId;
        saveState();
        System.out.println("+ created workflow (" + wfId + ")");
      }
    }

    try {
      api("POST", "/workflows/" + state.workflowId + "/versions", new LinkedHashMap<>());
    } catch (Exception e) {
      // best-effort: some accounts/plans may not require this explicit step
    }

    System.out.println("\nDone. Run documents through it with:");
    System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + state.workflowId
        + "\" }, file: { url: \"https://…\" } }");
    System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
  }

  private static Map<String, Object> parseJsonResponse(String json) {
    // Minimal JSON parser for response objects
    Map<String, Object> result = new LinkedHashMap<>();
    if (json == null || json.trim().isEmpty() || json.trim().equals("{}")) {
      return result;
    }
    // Extract top-level string fields: "id", "workflow", "data", "items"
    extractJsonField(json, "id", result);
    extractJsonField(json, "workflow", result);
    extractJsonField(json, "data", result);
    extractJsonField(json, "items", result);
    return result;
  }

  private static void extractJsonField(String json, String fieldName, Map<String, Object> result) {
    String pattern = "\"" + fieldName + "\"";
    int idx = json.indexOf(pattern);
    if (idx == -1)
      return;
    idx = json.indexOf(":", idx);
    if (idx == -1)
      return;
    idx++;
    while (idx < json.length() && Character.isWhitespace(json.charAt(idx))) {
      idx++;
    }
    if (idx >= json.length())
      return;
    char c = json.charAt(idx);
    if (c == '"') {
      int end = json.indexOf('"', idx + 1);
      if (end != -1) {
        result.put(fieldName, json.substring(idx + 1, end));
      }
    } else if (c == '{') {
      int depth = 1;
      int end = idx + 1;
      while (end < json.length() && depth > 0) {
        if (json.charAt(end) == '{')
          depth++;
        else if (json.charAt(end) == '}')
          depth--;
        end++;
      }
      result.put(fieldName, parseJsonResponse(json.substring(idx, end)));
    } else if (c == '[') {
      int depth = 1;
      int end = idx + 1;
      while (end < json.length() && depth > 0) {
        if (json.charAt(end) == '[')
          depth++;
        else if (json.charAt(end) == ']')
          depth--;
        end++;
      }
      result.put(fieldName, parseJsonArray(json.substring(idx, end)));
    }
  }

  private static List<Map<String, Object>> parseJsonArray(String json) {
    List<Map<String, Object>> result = new ArrayList<>();
    int depth = 0;
    int start = -1;
    for (int i = 0; i < json.length(); i++) {
      char c = json.charAt(i);
      if (c == '{') {
        if (depth == 0)
          start = i;
        depth++;
      } else if (c == '}') {
        depth--;
        if (depth == 0 && start != -1) {
          result.add(parseJsonResponse(json.substring(start, i + 1)));
          start = -1;
        }
      }
    }
    return result;
  }

  private static String toJsonString(Object obj) {
    if (obj == null) {
      return "null";
    }
    if (obj instanceof String) {
      return "\"" + escapeJson((String) obj) + "\"";
    }
    if (obj instanceof Boolean || obj instanceof Number) {
      return obj.toString();
    }
    if (obj instanceof Map) {
      Map<String, Object> map = (Map<String, Object>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<String, Object> entry : map.entrySet()) {
        if (!first)
          sb.append(",");
        sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
        sb.append(toJsonString(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<Object> list = (List<Object>) obj;
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : list) {
        if (!first)
          sb.append(",");
        sb.append(toJsonString(item));
        first = false;
      }
      sb.append("]");
      return sb.toString();
    }
    return "null";
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// All operations mirror the TypeScript reference's SDK calls over HTTP.

package main

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

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

var (
	apiKey  string
	stateDir  string
	stateFile string
)

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

type Classification struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	Description string `json:"description"`
}

type ClassifierConfig struct {
	Classifications []Classification `json:"classifications"`
	BaseProcessor   string           `json:"baseProcessor"`
	AdvancedOptions map[string]interface{} `json:"advancedOptions"`
}

type ExtractorSchema struct {
	Type       string                 `json:"type"`
	Properties map[string]interface{} `json:"properties"`
}

type ExtractorConfig struct {
	Schema          ExtractorSchema        `json:"schema"`
	BaseProcessor   string                 `json:"baseProcessor"`
	AdvancedOptions map[string]interface{} `json:"advancedOptions"`
}

type ParseConfig struct {
	BlockOptions     map[string]interface{} `json:"blockOptions"`
	ChunkingStrategy map[string]interface{} `json:"chunkingStrategy"`
}

type StepConfig struct {
	ParseConfig      *ParseConfig      `json:"parseConfig,omitempty"`
	ClassifierConfig *ClassifierConfig `json:"classifierConfig,omitempty"`
	ExtractorConfig  *ExtractorConfig  `json:"extractorConfig,omitempty"`
}

type StepNext struct {
	Step             string `json:"step"`
	ClassificationID string `json:"classificationId,omitempty"`
}

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

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

type WorkflowResponse struct {
	ID       string `json:"id,omitempty"`
	Workflow struct {
		ID string `json:"id,omitempty"`
	} `json:"workflow,omitempty"`
}

type WorkflowListResponse struct {
	Data  []WorkflowItem `json:"data,omitempty"`
	Items []WorkflowItem `json:"items,omitempty"`
}

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

func init() {
	apiKey = os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
		os.Exit(1)
	}

	stateDir = filepath.Join(".", ".extend")
	stateFile = filepath.Join(stateDir, "receipt-classifier-extractor.json")
}

func loadState() State {
	data, err := os.ReadFile(stateFile)
	if err != nil {
		return State{}
	}
	var s State
	json.Unmarshal(data, &s)
	return s
}

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

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	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", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("x-extend-api-version", VERSION)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

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

	respData := make(map[string]interface{})
	json.NewDecoder(resp.Body).Decode(&respData)

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

	return respData, nil
}

func buildWorkflow() Workflow {
	return Workflow{
		Name: "Receipt Processing Pipeline",
		Steps: []Step{
			{
				Name: "startTrigger1",
				Type: "TRIGGER",
				Next: []StepNext{
					{Step: "parse1"},
				},
			},
			{
				Name: "parse1",
				Type: "PARSE",
				Config: &StepConfig{
					ParseConfig: &ParseConfig{
						BlockOptions: map[string]interface{}{
							"text": map[string]interface{}{
								"agentic": map[string]interface{}{
									"enabled": true,
								},
								"signatureDetectionEnabled": true,
							},
							"tables": map[string]interface{}{
								"agentic": map[string]interface{}{
									"enabled": true,
								},
								"tableHeaderContinuationEnabled": true,
							},
							"figures": map[string]interface{}{
								"enabled": true,
							},
						},
						ChunkingStrategy: map[string]interface{}{
							"type":    "page",
							"options": map[string]interface{}{},
						},
					},
				},
				Next: []StepNext{
					{Step: "classify2"},
				},
			},
			{
				Name: "classify2",
				Type: "CLASSIFY",
				Config: &StepConfig{
					ClassifierConfig: &ClassifierConfig{
						Classifications: []Classification{
							{
								ID:          "classification1",
								Type:        "other",
								Description: "Use the \"other\" classification when the total receipt amount is under $200.",
							},
							{
								ID:          "classification_H7O",
								Type:        "flag",
								Description: "Use the \"flag\" classification when the total receipt amount is over $200.",
							},
						},
						BaseProcessor: "classification_performance",
						AdvancedOptions: map[string]interface{}{
							"advancedMultimodalEnabled": true,
						},
					},
				},
				Next: []StepNext{
					{Step: "extraction3", ClassificationID: "classification1"},
					{Step: "extraction3", ClassificationID: "classification_H7O"},
				},
			},
			{
				Name: "extraction3",
				Type: "EXTRACT",
				Config: &StepConfig{
					ExtractorConfig: &ExtractorConfig{
						Schema: ExtractorSchema{
							Type: "object",
							Properties: map[string]interface{}{
								"retailer": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Name of the retailer",
								},
								"store_number": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Store identification number",
								},
								"store_address": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Full street address of the store",
								},
								"store_city_state_zip": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "City, state, and ZIP code of the store",
								},
								"transaction_date": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Date of transaction in YYYY-MM-DD format",
								},
								"transaction_time": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Time of transaction in HH:MM format",
								},
								"items": map[string]interface{}{
									"type":        "array",
									"description": "List of purchased items",
									"items": map[string]interface{}{
										"type": "object",
										"properties": map[string]interface{}{
											"description": map[string]interface{}{
												"type": []string{"string", "null"},
											},
											"price": map[string]interface{}{
												"type": []string{"string", "null"},
											},
										},
									},
								},
								"subtotal": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Subtotal amount before tax",
								},
								"tax": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Tax amount",
								},
								"total": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Final total amount",
								},
								"payment_method": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Payment type and last four digits",
								},
								"approval_number": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Payment approval number",
								},
							},
						},
						BaseProcessor: "extraction_performance",
						AdvancedOptions: map[string]interface{}{
							"reviewAgent": map[string]interface{}{
								"enabled": true,
							},
							"advancedMultimodalEnabled": true,
						},
					},
				},
			},
		},
	}
}

func main() {
	state := loadState()
	workflow := buildWorkflow()

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

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

		if err == nil {
			var items []WorkflowItem
			if data, ok := listResp["data"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						if name, ok := m["name"].(string); ok && name == workflow.Name {
							if id, ok := m["id"].(string); ok {
								state.WorkflowID = id
								saveState(state)
								fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
								_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
									"steps": workflow.Steps,
								})
								if err != nil {
									fmt.Fprintf(os.Stderr, "%v\n", err)
									os.Exit(1)
								}
								found = true
								break
							}
						}
					}
				}
			}
			if !found {
				if items, ok := listResp["items"].([]interface{}); ok {
					for _, item := range items {
						if m, ok := item.(map[string]interface{}); ok {
							if name, ok := m["name"].(string); ok && name == workflow.Name {
								if id, ok := m["id"].(string); ok {
									state.WorkflowID = id
									saveState(state)
									fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
									_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
										"steps": workflow.Steps,
									})
									if err != nil {
										fmt.Fprintf(os.Stderr, "%v\n", err)
										os.Exit(1)
									}
									found = true
									break
								}
							}
						}
					}
				}
			}
		}

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

			var wfID string
			if id, ok := createResp["id"].(string); ok {
				wfID = id
			} else if wf, ok := createResp["workflow"].(map[string]interface{}); ok {
				if id, ok := wf["id"].(string); ok {
					wfID = id
				}
			}

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

			state.WorkflowID = wfID
			saveState(state)
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

	// Deploy the current draft as a new version (best-effort)
	apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})

	fmt.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)

Use sync parse for single receipts in real-time workflows (sub-10 second latency); use async `parseRuns.createAndPoll()` for batch processing 50+ receipts to avoid timeout. For production at scale, always use async with polling.
Write good descriptions in your schema and use Composer to optimize: instead of `"name of the merchant"`, use `"The legal business name printed at the top of the receipt, e.g. 'COFFEE SHOP LLC'"`. Confidence typically rises 5–15% with precise, example-driven descriptions.
Mark `date` as nullable (`{ type: ["string", "null"] }`) and set confidence threshold to flag records where date is null; pair this with OCR logs to identify when `agentic_ocr` fails and escalate to manual entry. This prevents silent data loss in downstream systems.
Tags
RetailPoint-of-SaleTransactionPayment
About this template

This template captures structured data from retail point-of-sale receipts, including merchant details, itemized purchases, tax calculations, and payment authorization information. It handles typical receipt layouts with store location, transaction IDs, line items with prices, and payment method details.

Document formats
  • PDF
  • Images & Scans
Requirements
  • 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