Financial & BankingParse → Split → Extract

Onboarding Package Splitter

Splits onboarding packets with 1040 Tax Returns, Bank Statements, ID cards, etc.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentSplitter Bank Statement Tax Return ID.pdf

Step-by-step

An onboarding packet includes several relevant documents such as 1040 Tax Returns, ID cards, bank statements, etc, and can be used by a wide variety of industries, from banking to real estate. This template takes in onboarding packets and outputs markdown (.md) capturing the statement's full text and layout, and JSON (.json) with structured fields including account holder details, balance information, and itemized transaction records per the extraction schema. It also splits into sub documents. by using Extend's Parse, Extract primitives.

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

Parse

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

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

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

Step 2

Extract

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

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

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

Example code

{
  "name": "Automated Onboarding Agent Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "split2"
        }
      ]
    },
    {
      "name": "split2",
      "type": "SPLIT",
      "config": {
        "splitterConfig": {
          "splitClassifications": [
            {
              "id": "splitter_classification1",
              "type": "other",
              "description": "Use the `other` document type when the document can not clearly be classified into one of the described classifications."
            },
            {
              "id": "subdocument_type_Bli",
              "type": "bank_statement",
              "description": "Bank statement"
            },
            {
              "id": "subdocument_type_GUh",
              "type": "tax_return",
              "description": "Tax return"
            },
            {
              "id": "subdocument_type_Q_T",
              "type": "identification",
              "description": "Government ID"
            }
          ],
          "baseProcessor": "splitting_performance",
          "advancedOptions": {
            "pageOverlapEnabled": false
          }
        }
      },
      "next": [
        {
          "step": "extraction3",
          "classificationId": "splitter_classification1"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_Bli"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_GUh"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_Q_T"
        }
      ]
    },
    {
      "name": "extraction3",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "required": [
              "ssn",
              "city",
              "state",
              "address",
              "country",
              "tax_owed",
              "tax_paid",
              "tax_year",
              "bank_name",
              "full_name",
              "last_name",
              "first_name",
              "middle_name",
              "postal_code",
              "account_type",
              "total_income",
              "transactions",
              "date_of_birth",
              "document_type",
              "filing_status",
              "refund_amount",
              "account_number",
              "ending_balance",
              "taxable_income",
              "total_deposits",
              "beginning_balance",
              "total_withdrawals",
              "account_holder_name",
              "number_of_dependents",
              "statement_period_end",
              "statement_period_start"
            ],
            "properties": {
              "ssn": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's Social Security Number or equivalent national identification number, as shown on the document. May be labeled as 'SSN', 'Social Security Number', or similar. Format and presence may vary by document type."
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The city or locality portion of the individual's address as presented on the document."
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The state, province, or region portion of the individual's address as shown on the document."
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's primary residential address as shown on the document. This may include street address, apartment or unit number, city, state, and postal code. May be split into multiple fields in some documents."
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The country portion of the individual's address as shown on the document. May be omitted if not present."
              },
              "tax_owed": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total tax amount owed as reported on the tax return. This is the calculated tax liability before payments and credits.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "tax_paid": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of tax payments made, as reported on the tax return. This includes withholding, estimated payments, and credits.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "tax_year": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The tax year or reporting period covered by the tax return or statement. For tax documents, this is the year for which the return is filed. For bank statements, this may be the statement period."
              },
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the financial institution or bank that issued the statement. May appear as a logo, header, or in the footer."
              },
              "full_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The complete legal name of the individual as presented on the document. This may include first, middle, and last names, and is essential for identity verification. In some documents, names may be split into separate fields."
              },
              "last_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's family or surname as shown on the document. May appear as 'Last Name', 'Surname', or similar labels."
              },
              "first_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's given name as shown on the document. May appear as 'First Name', 'Given Name', or similar labels."
              },
              "middle_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's middle name or initial, if present, as shown on the document. May be omitted if not provided."
              },
              "postal_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The postal or ZIP code portion of the individual's address as shown on the document."
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The type of bank account, such as 'Checking', 'Savings', 'Student Checking', etc., as indicated on the statement."
              },
              "total_income": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The individual's total income as reported on the tax return or statement. This is the sum of all income sources before deductions. May be labeled as 'Total Income', 'Adjusted Gross Income', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "transactions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "amount",
                    "description",
                    "currency_code",
                    "reference_number",
                    "transaction_date",
                    "transaction_type"
                  ],
                  "properties": {
                    "amount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The monetary value of the transaction. Positive for credits/deposits, negative for debits/withdrawals."
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A description of the transaction, such as merchant name, payment type, or transaction details."
                    },
                    "currency_code": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The ISO 4217 currency code for the transaction amount, such as 'USD', 'EUR', etc."
                    },
                    "reference_number": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A unique reference or transaction number associated with this transaction, if available."
                    },
                    "transaction_date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The date the transaction was posted or occurred, as shown on the statement.",
                      "extend:type": "date"
                    },
                    "transaction_type": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The type of transaction, such as 'deposit', 'withdrawal', 'purchase', 'fee', etc. May be inferred from context or explicitly labeled."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The list of individual transactions recorded during the statement period. Each transaction may include date, description, reference number, and amount. Transactions may include deposits, withdrawals, purchases, fees, and other account activity."
              },
              "date_of_birth": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's date of birth as presented on the document. Used for identity verification and eligibility checks. May appear in various formats and locations.",
                "extend:type": "date"
              },
              "document_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The type of document this record represents, such as 'bank_statement', 'tax_return', or 'government_id'. This helps categorize the document for onboarding and compliance purposes."
              },
              "filing_status": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's tax filing status as indicated on the tax return, such as 'Single', 'Married filing jointly', 'Head of household', etc. May be represented by a checkbox or selection."
              },
              "refund_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The amount to be refunded to the individual, if any, as reported on the tax return. This is the overpayment after all calculations.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The unique account number associated with the bank statement or financial account. May be labeled as 'Account Number', 'Statement Account', or similar."
              },
              "ending_balance": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The account balance at the end of the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "taxable_income": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The individual's taxable income as reported on the tax return. This is the income amount after deductions and exemptions, used to calculate tax owed.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "total_deposits": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of deposits or credits during the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "beginning_balance": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The account balance at the start of the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "total_withdrawals": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of withdrawals or debits during the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "account_holder_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the primary account holder as shown on the bank statement. May include full name or be split into components."
              },
              "number_of_dependents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The total number of dependents claimed on the tax return, if applicable. May be explicitly stated or inferred from a list of dependents."
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The end date of the statement period for bank statements or financial records. Indicates the end of the covered period.",
                "extend:type": "date"
              },
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The start date of the statement period for bank statements or financial records. Indicates the beginning of the covered period.",
                "extend:type": "date"
              }
            },
            "additionalProperties": false
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Automated Onboarding Agent Processing — Extend AI Skill

## What this pipeline does

This pipeline processes multi-document onboarding packages (IRS Form 1040A, bank statements, government IDs) to extract standardized identity, financial, and tax compliance data. It first parses the bundled PDF into markdown, splits it into sub-documents by type (tax return, bank statement, ID), then extracts 30+ structured fields from each document type into a unified JSON schema. The output feeds directly into KYC/AML checks, account creation, and compliance workflows.

## When to use this

- **Multi-document onboarding flows** where applicants submit batches containing tax returns, ID, and proof of income in a single PDF.
- **Regulated financial services** (lending, brokerage, banking) requiring standardized identity and financial data capture with audit trails.
- **High-volume KYC/AML automation** where manual review is cost-prohibitive and field-level extraction confidence must be logged for compliance.
- **Workflow branching on document type** — when downstream systems handle tax returns, bank statements, and IDs differently (e.g., different validation rules, different required fields).
- **International onboarding** where SSN may not exist but passport/national ID numbers, address components, and income must be captured across varying document layouts.

## Processor pipeline

### Step 1: Parse (`parse_performance` with agentic OCR)
**Purpose:** Convert the bundled PDF to markdown, preserving text structure and handling complex layouts (checkboxes, dependent lists, handwriting).

**Key config:**
- `engine: "parse_performance"` — balanced speed and accuracy for mixed-quality documents (some digital forms, some scanned).
- `blockOptions.text.agentic.enabled: true` — enables smart field detection for structured forms (checkboxes, dependent fields on Form 1040A).
- `chunkingStrategy.type: "document"` — keeps the entire document as one chunk; avoids splitting sections that the splitter will need to reassemble.

**Why this config:** IRS Form 1040A has dependent entries that may span multiple rows and columns. Agentic OCR detects these patterns. Document-level chunking ensures the splitter sees complete dependent blocks.

### Step 2: Split (`splitting_performance` with classification)
**Purpose:** Break the bundled PDF into three sub-documents (tax return, bank statement, government ID), each routed to the same extraction schema.

**Key config:**
- `baseProcessor: "splitting_performance"` — high-accuracy splitter that understands document boundaries and page breaks.
- `splitClassifications`: four types — `bank_statement`, `tax_return`, `identification`, and catch-all `other`.
- `advancedOptions.pageOverlapEnabled: false` — no duplication; each page assigned to exactly one sub-document.

**Why this config:** Onboarding packages are typically 3–5 pages; no overlap needed. Accurate split boundaries prevent fields from one doc type contaminating extraction results for another.

### Step 3: Extract (`extraction_performance` with review agent)
**Purpose:** Pull 30+ fields from each sub-document into a unified JSON schema, handling document-type variation (e.g., `bank_name` only on bank statements, `filing_status` only on tax returns).

**Key config:**
- `baseProcessor: "extraction_performance"` — highest-accuracy extraction for compliance-grade output; trades speed for field-level confidence.
- `advancedOptions.reviewAgent.enabled: true` — async human-in-the-loop review for low-confidence extractions (e.g., ambiguous SSN digits, partially legible amounts).
- `advancedOptions.advancedMultimodalEnabled: true` — uses image + text signals to resolve ambiguities (e.g., checkbox state, signature presence).

**Why this config:** KYC/AML and lending systems cannot accept null fields without review. Review agent flags and routes low-confidence results to human reviewers, maintaining both automation speed and compliance rigor.

## TypeScript implementation

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

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

// Define the extraction schema using Zod.
// This matches the required fields from the onboarding pipeline.
const OnboardingSchema = z.object({
  ssn: z.string().nullable().describe(
    "The individual's Social Security Number or equivalent national identification number, as shown on the document. May be labeled as 'SSN', 'Social Security Number', or similar. Format and presence may vary by document type."
  ),
  city: z.string().nullable().describe(
    "The city or locality portion of the individual's address as presented on the document."
  ),
  state: z.string().nullable().describe(
    "The state, province, or region portion of the individual's address as shown on the document."
  ),
  address: z.string().nullable().describe(
    "The individual's primary residential address as shown on the document. This may include street address, apartment or unit number, city, state, and postal code. May be split into multiple fields in some documents."
  ),
  country: z.string().nullable().describe(
    "The country portion of the individual's address as shown on the document. May be omitted if not present."
  ),
  postal_code: z.string().nullable().describe(
    "The postal or ZIP code portion of the individual's address as shown on the document."
  ),
  full_name: z.string().nullable().describe(
    "The complete legal name of the individual as presented on the document. This may include first, middle, and last names, and is essential for identity verification. In some documents, names may be split into separate fields."
  ),
  first_name: z.string().nullable().describe(
    "The individual's given name as shown on the document. May appear as 'First Name', 'Given Name', or similar labels."
  ),
  middle_name: z.string().nullable().describe(
    "The individual's middle name or initial, if present, as shown on the document. May be omitted if not provided."
  ),
  last_name: z.string().nullable().describe(
    "The individual's family or surname as shown on the document. May appear as 'Last Name', 'Surname', or similar labels."
  ),
  date_of_birth: extendDate().describe(
    "The individual's date of birth as presented on the document. Used for identity verification and eligibility checks. May appear in various formats and locations."
  ),
  tax_year: z.string().nullable().describe(
    "The tax year or reporting period covered by the tax return or statement. For tax documents, this is the year for which the return is filed. For bank statements, this may be the statement period."
  ),
  filing_status: z.string().nullable().describe(
    "The individual's tax filing status as indicated on the tax return, such as 'Single', 'Married filing jointly', 'Head of household', etc. May be represented by a checkbox or selection."
  ),
  number_of_dependents: z.number().nullable().describe(
    "The total number of dependents claimed on the tax return, if applicable. May be explicitly stated or inferred from a list of dependents."
  ),
  total_income: extendCurrency().describe(
    "The individual's total income as reported on the tax return or statement. This is the sum of all income sources before deductions. May be labeled as 'Total Income', 'Adjusted Gross Income', or similar."
  ),
  taxable_income: extendCurrency().describe(
    "The individual's taxable income as reported on the tax return. This is the income amount after deductions and exemptions, used to calculate tax owed."
  ),
  tax_owed: extendCurrency().describe(
    "The total tax amount owed as reported on the tax return. This is the calculated tax liability before payments and credits."
  ),
  tax_paid: extendCurrency().describe(
    "The total amount of tax payments made, as reported on the tax return. This includes withholding, estimated payments, and credits."
  ),
  refund_amount: extendCurrency().describe(
    "The amount to be refunded to the individual, if any, as reported on the tax return. This is the overpayment after all calculations."
  ),
  bank_name: z.string().nullable().describe(
    "The name of the financial institution or bank that issued the statement. May appear as a logo, header, or in the footer."
  ),
  account_holder_name: z.string().nullable().describe(
    "The name of the primary account holder as shown on the bank statement. May include full name or be split into components."
  ),
  account_number: z.string().nullable().describe(
    "The unique account number associated with the bank statement or financial account. May be labeled as 'Account Number', 'Statement Account', or similar."
  ),
  account_type: z.string().nullable().describe(
    "The type of bank account, such as 'Checking', 'Savings', 'Student Checking', etc., as indicated on the statement."
  ),
  statement_period_start: extendDate().describe(
    "The start date of the statement period for bank statements or financial records. Indicates the beginning of the covered period."
  ),
  statement_period_end: extendDate().describe(
    "The end date of the statement period for bank statements or financial records. Indicates the end of the covered period."
  ),
  beginning_balance: extendCurrency().describe(
    "The account balance at the start of the statement period, as shown on the bank statement."
  ),
  ending_balance: extendCurrency().describe(
    "The account balance at the end of the statement period, as shown on the bank statement."
  ),
  total_deposits: extendCurrency().describe(
    "The total amount of deposits or credits during the statement period, as shown on the bank statement."
  ),
  total_withdrawals: extendCurrency().describe(
    "The total amount of withdrawals or debits during the statement period, as shown on the bank statement."
  ),
  transactions: z.array(z.object({
    transaction_date: extendDate().describe(
      "The date the transaction was posted or occurred, as shown on the statement."
    ),
    description: z.string().nullable().describe(
      "A description of the transaction, such as merchant name, payment type, or transaction details."
    ),
    transaction_type: z.string().nullable().describe(
      "The type of transaction, such as 'deposit', 'withdrawal', 'purchase', 'fee', etc. May be inferred from context or explicitly labeled."
    ),
    amount: z.number().nullable().describe(
      "The monetary value of the transaction. Positive for credits/deposits, negative for debits/withdrawals."
    ),
    currency_code: z.string().nullable().describe(
      "The ISO 4217 currency code for the transaction amount, such as 'USD', 'EUR', etc."
    ),
    reference_number: z.string().nullable().describe(
      "A unique reference or transaction number associated with this transaction, if available."
    ),
  })).describe(
    "The list of individual transactions recorded during the statement period. Each transaction may include date, description, reference number, and amount. Transactions may include deposits, withdrawals, purchases, fees, and other account activity."
  ),
  document_type: z.string().nullable().describe(
    "The type of document this record represents, such as 'bank_statement', 'tax_return', or 'government_id'. This helps categorize the document for onboarding and compliance purposes."
  ),
});

/**
 * Process an automated onboarding agent PDF by:
 * 1. Parsing it to markdown with agentic OCR
 * 2. Splitting it into sub-documents (tax return, bank statement, ID)
 * 3. Extracting structured fields from each sub-document
 */
export async function processAutomatedOnboardingAgent(filePath: string) {
  console.log(`\n=== Automated Onboarding Agent Processing ===`);
  console.log(`Input file: ${filePath}\n`);

  // Step 1: Upload the file
  console.log("📁 Uploading file...");
  const fileData = fs.readFileSync(filePath);
  const dataUrl = `data:application/pdf;base64,${fileData.toString("base64")}`;

  // Step 2: Parse with agentic OCR
  console.log("🔍 Parsing document (agentic OCR enabled)...");
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      blockOptions: {
        text: {
          agentic: {
            enabled: true,
          },
        },
      },
      chunkingStrategy: {
        type: "document",
      },
    },
  });

  if (parseRun.status !== "PROCESSED") {
    console.error(`Parse failed: ${parseRun.status}`);
    return;
  }
  console.log(`✓ Parsed into ${parseRun.output.chunks.length} chunk(s)`);
  const parsedMarkdown = parseRun.output.chunks.map(c => c.content).join("\n\n");
  console.log(`Markdown preview (first 500 chars):\n${parsedMarkdown.substring(0, 500)}...\n`);

  // Step 3: Split by document type
  console.log("✂️  Splitting into sub-documents (tax return, bank statement, ID)...");
  const splitRun = await client.splitRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      splitClassifications: [
        { id: "other", type: "other", description: "Use this when the document cannot clearly be classified into one of the other types." },
        { id: "bank_statement", type: "bank_statement", description: "Bank statement" },
        { id: "tax_return", type: "tax_return", description: "Tax return" },
        { id: "identification", type: "identification", description: "Government ID" },
      ],
      baseProcessor: "splitting_performance",
      advancedOptions: {
        pageOverlapEnabled: false,
      },
    },
  });

  if (splitRun.status !== "PROCESSED") {
    console.error(`Split failed: ${splitRun.status}`);
    return;
  }

  const splits = splitRun.output.splits || [];
  console.log(`✓ Split into ${splits.length} sub-document(s):\n`);
  splits.forEach(split => {
    console.log(`  - ${split.type} (pages ${split.startPage}–${split.endPage})`);
  });

  // Step 4: Extract fields from each split
  console.log("\n📊 Extracting structured fields from each sub-document...\n");
  const allExtractions: Array<{
    documentType: string;
    fileId: string;
    data: any;
  }> = [];

  for (const split of splits) {
    console.log(`  Extracting from ${split.type}...`);
    const extractRun = await client.extractRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        schema: OnboardingSchema,
        baseProcessor: "extraction_performance",
        advancedOptions: {
          reviewAgent: {
            enabled: true,
          },
          advancedMultimodalEnabled: true,
        },
      },
    });

    if (extractRun.status === "PROCESSED") {
      allExtractions.push({
        documentType: split.type,
        fileId: split.fileId,
        data: extractRun.output.value,
      });
      console.log(`    ✓ Extracted ${Object.keys(extractRun.output.value || {}).length} fields`);
    } else {
      console.log(`    ✗ Extraction failed: ${extractRun.status}`);
    }
  }

  // Step 5: Aggregate and display results
  console.log("\n=== EXTRACTION RESULTS ===\n");
  allExtractions.forEach((result, idx) => {
    console.log(`Document ${idx + 1}: ${result.documentType}`);
    console.log(`File ID: ${result.fileId}`);
    console.log("Extracted fields:");
    
    // Display a sample of extracted fields (not all 30+)
    const sampleFields = [
      "full_name", "ssn", "date_of_birth", "address", "city", "state", "postal_code", "country",
      "tax_year", "filing_status", "total_income
import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

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

// Define the extraction schema using Zod
const TaxFormSchema = z.object({
  form_type: z.string().nullable().describe(
    "Tax form type (e.g., Form 1040A, Form 1040). Look for the form identifier near the top of the document."
  ),
  tax_year: z.string().nullable().describe(
    "Tax year for the return (e.g., 2007). Usually printed in the upper right or top of form."
  ),
  primary_taxpayer_first_name: z.string().nullable().describe(
    "First name and initial of primary taxpayer. Extract exactly as printed or written in the 'Your first name and initial' field."
  ),
  primary_taxpayer_last_name: z.string().nullable().describe(
    "Last name of primary taxpayer. Extract from 'Last name' field on form."
  ),
  primary_taxpayer_ssn: z.string().nullable().describe(
    "Social security number of primary taxpayer (format: XXX-XX-XXXX or XXXXXXXXX). Extract from 'Your social security number' field."
  ),
  spouse_first_name: z.string().nullable().describe(
    "First name and initial of spouse if filing jointly. Extract from 'If a joint return, spouse's first name and initial' field. Null if single filer."
  ),
  spouse_last_name: z.string().nullable().describe(
    "Last name of spouse if filing jointly. Extract from spouse last name field. Null if single filer."
  ),
  spouse_ssn: z.string().nullable().describe(
    "Social security number of spouse (format: XXX-XX-XXXX or XXXXXXXXX). Extract from 'Spouse's social security number' field. Null if single filer."
  ),
  home_address: z.string().nullable().describe(
    "Full home address including street, city, state, and ZIP code. Extract from 'Home address' field. Include all components even if on separate lines."
  ),
  filing_status: z.string().nullable().describe(
    "Filing status selected (Single, Married filing jointly, Married filing separately, Head of household, Qualifying widow(er)). Look for checked checkbox or marked status on form."
  ),
  dependents: z.array(
    z.object({
      name: z.string().nullable().describe("Full name of dependent as listed on form."),
      ssn: z.string().nullable().describe("Social security number of dependent (format: XXX-XX-XXXX or XXXXXXXXX)."),
      relationship: z.string().nullable().describe("Relationship to taxpayer (e.g., Son, Daughter, Parent, Other relative)."),
    })
  ).describe(
    "List of claimed dependents with name, SSN, and relationship. Extract from the dependents section of the form. Empty array if no dependents claimed."
  ),
  stimulus_payment_indicator: z.string().nullable().describe(
    "Indicates if form is marked for stimulus payment. Look for 'Stimulus Payment' banner or checkbox at the top of form. Values: 'Yes', 'No', or null if not indicated."
  ),
});

export async function processAutomatedOnboardingAgent(filePath: string) {
  console.log(`Processing tax form from: ${filePath}`);

  // Read the file and convert to base64 data URL
  const fileBuffer = fs.readFileSync(filePath);
  const base64 = fileBuffer.toString("base64");
  const mimeType = filePath.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/png";
  const dataUrl = `data:${mimeType};base64,${base64}`;

  try {
    // Step 1: Parse the form to markdown
    console.log("Step 1: Parsing tax form...");
    const parseRun = await client.parseRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        mode: "agentic_ocr",
        outputType: "markdown",
      },
    });

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

    const parsedMarkdown = parseRun.output.chunks
      .map((chunk) => chunk.content)
      .join("\n\n");
    console.log("Parsed markdown (first 500 chars):");
    console.log(parsedMarkdown.substring(0, 500));

    // Step 2: Extract structured data using the schema
    console.log("\nStep 2: Extracting taxpayer data...");
    const extractRun = await client.extractRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        schema: TaxFormSchema,
        baseProcessor: "extraction_performance",
      },
    });

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

    const extracted = extractRun.output.value;
    console.log("Extraction complete. Extracted data:");
    console.log(JSON.stringify(extracted, null, 2));

    // Step 3: Validation and downstream preparation
    console.log("\nStep 3: Validating and preparing for onboarding...");

    // Check for required identity fields
    const hasRequiredIdentity =
      extracted.primary_taxpayer_first_name &&
      extracted.primary_taxpayer_last_name &&
      extracted.primary_taxpayer_ssn;

    if (!hasRequiredIdentity) {
      console.warn("⚠️  Warning: Missing required identity fields (name or SSN)");
    }

    // Check for stimulus payment flag
    if (extracted.stimulus_payment_indicator === "Yes") {
      console.log("✓ Stimulus payment flagged — route to payment processing system.");
    }

    // Build onboarding record
    const onboardingRecord = {
      timestamp: new Date().toISOString(),
      formType: extracted.form_type,
      taxYear: extracted.tax_year,
      primaryTaxpayer: {
        firstName: extracted.primary_taxpayer_first_name,
        lastName: extracted.primary_taxpayer_last_name,
        ssn: extracted.primary_taxpayer_ssn,
      },
      spouse:
        extracted.spouse_first_name || extracted.spouse_last_name
          ? {
              firstName: extracted.spouse_first_name,
              lastName: extracted.spouse_last_name,
              ssn: extracted.spouse_ssn,
            }
          : null,
      homeAddress: extracted.home_address,
      filingStatus: extracted.filing_status,
      dependents: extracted.dependents,
      stimulusPaymentRequested: extracted.stimulus_payment_indicator === "Yes",
      readyForKyc: hasRequiredIdentity,
    };

    console.log("\nOnboarding record ready:");
    console.log(JSON.stringify(onboardingRecord, null, 2));

    return onboardingRecord;
  } catch (error) {
    console.error("Error processing tax form:", error);
    throw error;
  }
}

// For CLI testing
const filePath = process.argv[2] || "./sample_1040a.pdf";
processAutomatedOnboardingAgent(filePath).catch(console.error);
import os
import sys
import base64
import json
from typing import Optional
from extend_ai import Extend

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


async def process_bank_statement(file_path: str):
    """
    Bank Statement Processing Pipeline
    
    1. Parse the statement to markdown (captures table structure, dates, amounts)
    2. Extract structured fields using schema with agentic extraction
    
    Handles multi-page statements, scanned originals, and various bank formats.
    """
    print(f"\n[Bank Statement Processing] Starting pipeline for: {file_path}\n")

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

    # Step 1: Parse to markdown
    # Agentic OCR mode recovers table structure from scanned statements
    print("[Step 1/2] Parsing statement to markdown...")
    parse_run = await client.parse_runs.create_and_poll(
        file={"url": data_url},
        config={
            "blockOptions": {
                "text": {
                    "agentic": {
                        "enabled": True,
                    },
                },
            },
            "chunkingStrategy": {
                "type": "document",
            },
        },
    )

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

    parsed_markdown = "\n\n".join([c.get("content", "") for c in parse_run.output.get("chunks", [])])
    print(f"✓ Parsed {len(parse_run.output.get('chunks', []))} chunks\n")

    # Step 2: Extract structured fields
    # Schema enforces types; descriptions guide the extraction agent
    print("[Step 2/2] Extracting bank statement fields...")

    extract_schema = {
        "type": "object",
        "properties": {
            "bank_name": {
                "type": ["string", "null"],
                "description": "Name of the bank issuing the statement, as it appears in the statement header or footer.",
            },
            "account_type": {
                "type": ["string", "null"],
                "description": "Type of account, e.g., 'Checking', 'Student Checking', 'Savings', 'Money Market'.",
            },
            "account_holder_name": {
                "type": ["string", "null"],
                "description": "Full name of the account holder, as shown on the statement header or account section.",
            },
            "account_holder_address": {
                "type": ["string", "null"],
                "description": "Mailing address of the account holder. Include street, city, state, and ZIP.",
            },
            "account_number": {
                "type": ["string", "null"],
                "description": "Bank account number. Often displayed in full or with last 4 digits shown; capture exactly as printed.",
            },
            "statement_period_start": {
                "type": ["string", "null"],
                "description": "Statement start date in format 'MMM DD, YYYY' (e.g., 'Jan 01, 2024'). Do not use ISO format.",
            },
            "statement_period_end": {
                "type": ["string", "null"],
                "description": "Statement end date in format 'MMM DD, YYYY' (e.g., 'Jan 31, 2024'). Do not use ISO format.",
            },
            "beginning_balance": {
                "type": ["number", "null"],
                "description": "Opening balance at the start of the statement period. Extract as numeric value only (e.g., 1234.56), no currency symbol.",
            },
            "ending_balance": {
                "type": ["number", "null"],
                "description": "Closing balance at the end of the statement period. Extract as numeric value only (e.g., 5678.90), no currency symbol.",
            },
            "total_deposits_credits": {
                "type": ["number", "null"],
                "description": "Sum of all deposits and credits during the period. Numeric value only, no currency symbol.",
            },
            "total_withdrawals": {
                "type": ["number", "null"],
                "description": "Sum of all withdrawals and debits during the period. Numeric value only, no currency symbol.",
            },
            "transactions": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "date": {
                            "type": ["string", "null"],
                            "description": "Transaction date in format 'MMM DD, YYYY' or 'MM/DD/YYYY'. Match the statement's format.",
                        },
                        "description": {
                            "type": ["string", "null"],
                            "description": "Transaction description or merchant name as printed on the statement. Capture exact text.",
                        },
                        "amount": {
                            "type": ["number", "null"],
                            "description": "Transaction amount as numeric value (e.g., 123.45). Do not include currency symbol. Positive for deposits, negative for withdrawals (if statement shows direction).",
                        },
                        "reference_number": {
                            "type": ["string", "null"],
                            "description": "Transaction reference, confirmation, or check number if present. Leave null if not provided.",
                        },
                    },
                },
                "description": "List of all transactions on the statement, in order as they appear. Each transaction includes date, description, amount, and optional reference number.",
            },
        },
    }

    extract_run = await client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={
            "schema": extract_schema,
            "baseProcessor": "extraction_performance",
            "advancedOptions": {
                "reviewAgent": {
                    "enabled": True,
                },
                "advancedMultimodalEnabled": True,
            },
        },
    )

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

    result = extract_run.output.get("value", {})
    print("✓ Extraction complete\n")

    # Validation & summary
    print("[Results Summary]")
    print(f"Bank: {result.get('bank_name') or '—'}")
    print(f"Account Holder: {result.get('account_holder_name') or '—'}")
    print(f"Account Type: {result.get('account_type') or '—'}")
    print(f"Account #: {result.get('account_number') or '—'}")
    print(
        f"Statement Period: {result.get('statement_period_start')} to {result.get('statement_period_end')}"
    )
    beginning_balance = result.get("beginning_balance")
    print(
        f"Beginning Balance: ${beginning_balance:.2f if beginning_balance is not None else '—'}"
    )
    ending_balance = result.get("ending_balance")
    print(f"Ending Balance: ${ending_balance:.2f if ending_balance is not None else '—'}")
    total_deposits = result.get("total_deposits_credits")
    print(
        f"Total Deposits/Credits: ${total_deposits:.2f if total_deposits is not None else '—'}"
    )
    total_withdrawals = result.get("total_withdrawals")
    print(
        f"Total Withdrawals: ${total_withdrawals:.2f if total_withdrawals is not None else '—'}"
    )
    transactions = result.get("transactions") or []
    print(f"Transactions Found: {len(transactions)}")

    if transactions:
        print("\n[First 3 Transactions]")
        for i, tx in enumerate(transactions[:3]):
            amount = tx.get("amount")
            amount_str = f"${amount:.2f}" if amount is not None else "$—"
            ref_str = f" (Ref: {tx.get('reference_number')})" if tx.get("reference_number") else ""
            print(
                f"  {i + 1}. {tx.get('date')} | {tx.get('description')} | {amount_str}{ref_str}"
            )

    # Return full typed result
    return {
        "status": "success",
        "parsedMarkdown": parsed_markdown,
        "extractedData": result,
    }


async def main():
    file_path = sys.argv[1] if len(sys.argv) > 1 else "__FILE_PATH__"
    try:
        output = await process_bank_statement(file_path)
        print("\n[Output JSON]")
        print(json.dumps(output["extractedData"], indent=2))
    except Exception as error:
        print(f"[Error] {str(error)}")
        sys.exit(1)


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
// NOTE: This code uses Extend's REST API directly (https://api.extend.ai)
// because Extend does not publish an official Java SDK.
// Uses only java.net.http.HttpClient — no third-party dependencies.

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Scanner;
import com.sun.net.httpserver.HttpServer;
import javax.json.*;
import javax.json.stream.JsonParser;
import java.io.*;

public class BankStatementProcessor {
  private static final String API_BASE_URL = "https://api.extend.ai";
  private static final HttpClient httpClient = HttpClient.newHttpClient();

  static class Transaction {
    String date;
    String description;
    Double amount;
    String referenceNumber;

    Transaction(String date, String description, Double amount, String referenceNumber) {
      this.date = date;
      this.description = description;
      this.amount = amount;
      this.referenceNumber = referenceNumber;
    }
  }

  static class BankStatementData {
    String bankName;
    String accountType;
    String accountHolderName;
    String accountHolderAddress;
    String accountNumber;
    String statementPeriodStart;
    String statementPeriodEnd;
    Double beginningBalance;
    Double endingBalance;
    Double totalDepositsCredits;
    Double totalWithdrawals;
    java.util.List<Transaction> transactions;

    BankStatementData() {
      this.transactions = new java.util.ArrayList<>();
    }
  }

  static class ProcessResult {
    String status;
    String parsedMarkdown;
    BankStatementData extractedData;

    ProcessResult(String status, String parsedMarkdown, BankStatementData extractedData) {
      this.status = status;
      this.parsedMarkdown = parsedMarkdown;
      this.extractedData = extractedData;
    }
  }

  private static String getApiKey() {
    String key = System.getenv("EXTEND_API_KEY");
    if (key == null || key.isEmpty()) {
      throw new IllegalArgumentException("EXTEND_API_KEY environment variable not set");
    }
    return key;
  }

  private static String fileToBase64DataUrl(String filePath) throws IOException {
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64 = Base64.getEncoder().encodeToString(fileBytes);
    return "data:application/octet-stream;base64," + base64;
  }

  private static String postRequest(String endpoint, String jsonBody) throws Exception {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(new URI(API_BASE_URL + endpoint))
        .header("Authorization", "Bearer " + getApiKey())
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
        .build();

    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 400) {
      throw new RuntimeException("API error: " + response.statusCode() + " " + response.body());
    }
    return response.body();
  }

  private static String pollParseRun(String runId) throws Exception {
    Thread.sleep(2000); // Initial delay before polling
    while (true) {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(new URI(API_BASE_URL + "/parse-runs/" + runId))
          .header("Authorization", "Bearer " + getApiKey())
          .GET()
          .build();

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

      JsonReader jsonReader = Json.createReader(new StringReader(body));
      JsonObject jsonObject = jsonReader.readObject();

      String status = jsonObject.getString("status");
      if ("PROCESSED".equals(status)) {
        return body;
      }
      if ("FAILED".equals(status) || "ERROR".equals(status)) {
        throw new RuntimeException("Parse run failed with status: " + status);
      }

      Thread.sleep(2000);
    }
  }

  private static String pollExtractRun(String runId) throws Exception {
    Thread.sleep(2000);
    while (true) {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(new URI(API_BASE_URL + "/extract-runs/" + runId))
          .header("Authorization", "Bearer " + getApiKey())
          .GET()
          .build();

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

      JsonReader jsonReader = Json.createReader(new StringReader(body));
      JsonObject jsonObject = jsonReader.readObject();

      String status = jsonObject.getString("status");
      if ("PROCESSED".equals(status)) {
        return body;
      }
      if ("FAILED".equals(status) || "ERROR".equals(status)) {
        throw new RuntimeException("Extract run failed with status: " + status);
      }

      Thread.sleep(2000);
    }
  }

  private static String buildExtractSchema() {
    JsonObjectBuilder schema = Json.createObjectBuilder()
        .add("type", "object")
        .add("properties", Json.createObjectBuilder()
            .add("bank_name", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Name of the bank issuing the statement"))
            .add("account_type", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Type of account, e.g., 'Checking', 'Student Checking', 'Savings'"))
            .add("account_holder_name", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Full name of the account holder"))
            .add("account_holder_address", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Mailing address including street, city, state, ZIP"))
            .add("account_number", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Bank account number"))
            .add("statement_period_start", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Statement start date in format 'MMM DD, YYYY'"))
            .add("statement_period_end", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("string").add("null").build())
                .add("description", "Statement end date in format 'MMM DD, YYYY'"))
            .add("beginning_balance", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("number").add("null").build())
                .add("description", "Opening balance at the start of the statement period"))
            .add("ending_balance", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("number").add("null").build())
                .add("description", "Closing balance at the end of the statement period"))
            .add("total_deposits_credits", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("number").add("null").build())
                .add("description", "Sum of all deposits and credits during the period"))
            .add("total_withdrawals", Json.createObjectBuilder()
                .add("type", Json.createArrayBuilder().add("number").add("null").build())
                .add("description", "Sum of all withdrawals and debits during the period"))
            .add("transactions", Json.createObjectBuilder()
                .add("type", "array")
                .add("items", Json.createObjectBuilder()
                    .add("type", "object")
                    .add("properties", Json.createObjectBuilder()
                        .add("date", Json.createObjectBuilder()
                            .add("type", Json.createArrayBuilder().add("string").add("null").build())
                            .add("description", "Transaction date in format 'MMM DD, YYYY' or 'MM/DD/YYYY'"))
                        .add("description", Json.createObjectBuilder()
                            .add("type", Json.createArrayBuilder().add("string").add("null").build())
                            .add("description", "Transaction description or merchant name"))
                        .add("amount", Json.createObjectBuilder()
                            .add("type", Json.createArrayBuilder().add("number").add("null").build())
                            .add("description", "Transaction amount as numeric value"))
                        .add("reference_number", Json.createObjectBuilder()
                            .add("type", Json.createArrayBuilder().add("string").add("null").build())
                            .add("description", "Transaction reference, confirmation, or check number"))
                        .build())
                    .build())
                .add("description", "List of all transactions on the statement"))
            .build());

    return schema.build().toString();
  }

  private static BankStatementData parseExtractResponse(String responseJson) throws Exception {
    JsonReader jsonReader = Json.createReader(new StringReader(responseJson));
    JsonObject jsonObject = jsonReader.readObject();

    JsonObject output = jsonObject.getJsonObject("output");
    JsonObject value = output.getJsonObject("value");

    BankStatementData data = new BankStatementData();
    data.bankName = value.isNull("bank_name") ? null : value.getString("bank_name");
    data.accountType = value.isNull("account_type") ? null : value.getString("account_type");
    data.accountHolderName = value.isNull("account_holder_name") ? null : value.getString("account_holder_name");
    data.accountHolderAddress = value.isNull("account_holder_address") ? null : value.getString("account_holder_address");
    data.accountNumber = value.isNull("account_number") ? null : value.getString("account_number");
    data.statementPeriodStart = value.isNull("statement_period_start") ? null : value.getString("statement_period_start");
    data.statementPeriodEnd = value.isNull("statement_period_end") ? null : value.getString("statement_period_end");
    data.beginningBalance = value.isNull("beginning_balance") ? null : value.getJsonNumber("beginning_balance").doubleValue();
    data.endingBalance = value.isNull("ending_balance") ? null : value.getJsonNumber("ending_balance").doubleValue();
    data.totalDepositsCredits = value.isNull("total_deposits_credits") ? null : value.getJsonNumber("total_deposits_credits").doubleValue();
    data.totalWithdrawals = value.isNull("total_withdrawals") ? null : value.getJsonNumber("total_withdrawals").doubleValue();

    if (!value.isNull("transactions")) {
      JsonArray transactions = value.getJsonArray("transactions");
      for (int i = 0; i < transactions.size(); i++) {
        JsonObject txObj = transactions.getJsonObject(i);
        String txDate = txObj.isNull("date") ? null : txObj.getString("date");
        String txDesc = txObj.isNull("description") ? null : txObj.getString("description");
        Double txAmount = txObj.isNull("amount") ? null : txObj.getJsonNumber("amount").doubleValue();
        String txRef = txObj.isNull("reference_number") ? null : txObj.getString("reference_number");
        data.transactions.add(new Transaction(txDate, txDesc, txAmount, txRef));
      }
    }

    return data;
  }

  private static String extractMarkdownFromParseResponse(String responseJson) throws Exception {
    JsonReader jsonReader = Json.createReader(new StringReader(responseJson));
    JsonObject jsonObject = jsonReader.readObject();

    JsonObject output = jsonObject.getJsonObject("output");
    JsonArray chunks = output.getJsonArray("chunks");

    StringBuilder markdown = new StringBuilder();
    for (int i = 0; i < chunks.size(); i++) {
      JsonObject chunk = chunks.getJsonObject(i);
      String content = chunk.getString("content");
      if (i > 0) markdown.append("\n\n");
      markdown.append(content);
    }

    return markdown.toString();
  }

  public static ProcessResult processBankStatement(String filePath) throws Exception {
    System.out.println("\n[Bank Statement Processing] Starting pipeline for: " + filePath + "\n");

    String dataUrl = fileToBase64DataUrl(filePath);

    // Step 1: Parse to markdown
    System.out.println("[Step 1/2] Parsing statement to markdown...");
    String parseRequestBody = Json.createObjectBuilder()
        .add("file", Json.createObjectBuilder().add("url", dataUrl).build())
        .add("config", Json.createObjectBuilder()
            .add("blockOptions", Json.createObjectBuilder()
                .add("text", Json.createObjectBuilder()
                    .add("agentic", Json.createObjectBuilder()
                        .add("enabled", true).build()).build()).build())
            .add("chunkingStrategy", Json.createObjectBuilder()
                .add("type", "document").build()).build())
        .build().toString();

    String parseCreateResponse = postRequest("/parse-runs", parseRequestBody);
    JsonReader parseCreateReader = Json.createReader(new StringReader(parseCreateResponse));
    JsonObject parseCreateObj = parseCreateReader.readObject();
    String parseRunId = parseCreateObj.getString("id");

    String parseRunResponse = pollParseRun(parseRunId);
    String parsedMarkdown = extractMarkdownFromParseResponse(parseRunResponse);

    JsonReader parseRunReader = Json.createReader(new StringReader(parseRunResponse));
    JsonObject parseRunObj = parseRunReader.readObject();
    JsonArray chunks = parseRunObj.getJsonObject("output").getJsonArray("chunks");
    System.out.println("✓ Parsed " + chunks.size() + " chunks\n");

    // Step 2: Extract structured fields
    System.out.println("[Step 2/2] Extracting bank statement fields...");
    String schemaJson = buildExtractSchema();

    String extractRequestBody = Json.createObjectBuilder()
        .add("file", Json.createObjectBuilder().add("url", dataUrl).build())
        .add("config", Json.createObjectBuilder()
            .add("schema", Json.createReader(new StringReader(schemaJson)).readObject())
            .add("baseProcessor", "extraction_performance")
            .add("advancedOptions", Json.createObjectBuilder()
                .add("reviewAgent", Json.createObjectBuilder()
                    .add("enabled", true).build())
                .add("advancedMultimodalEnabled", true).build()).build())
        .build().toString();

    String extractCreateResponse = postRequest("/extract-runs", extractRequestBody);
    JsonReader extractCreateReader = Json.createReader(new StringReader(extractCreateResponse));
    JsonObject extractCreateObj = extractCreateReader.readObject();
    String extractRunId = extractCreateObj.getString("id");

    String extractRunResponse = pollExtractRun(extractRunId);
    BankStatementData extractedData = parseExtractResponse(extractRunResponse);

    System.out.println("✓ Extraction complete\n");

    // Validation & summary
    System.out.println("[Results Summary]");
    System.out.println("Bank: " + (extractedData.bankName != null ? extractedData.bankName : "—"));
    System.out.println("Account Holder: " + (extractedData.accountHolderName != null ? extractedData.accountHolderName : "—"));
    System.out.println("Account Type: " + (extractedData.accountType != null ? extractedData.accountType : "—"));
    System.out.println("Account #: " + (extractedData.accountNumber != null ? extractedData.accountNumber : "—"));
    System.out.println("Statement Period: " + extractedData.statementPeriodStart + " to " + extractedData.statementPeriodEnd);
    System.out.println("Beginning Balance: $" + (extractedData.beginningBalance != null ? String.format("%.2f", extractedData.beginningBalance) : "—"));
    System.out.println("Ending Balance: $" + (extractedData.endingBalance != null ? String.format("%.2f", extractedData.endingBalance) : "—"));
    System.out.println("Total Deposits/Credits: $" + (extractedData.totalDepositsCredits != null ? String.format("%.2f", extractedData.totalDepositsCredits) : "—"));
    System.out.println("Total Withdrawals: $" + (extractedData.totalWithdrawals != null ? String.format("%.2f", extractedData.totalWithdrawals) : "—"));
    System.out.println("Transactions Found: " + extractedData.transactions.size());

    if (!extractedData.transactions.isEmpty()) {
      System.out.println("\n[First 3 Transactions]");
      for (int i = 0; i < Math.min(3, extractedData.transactions.size()); i++) {
        Transaction tx = extractedData.transactions.get(i);
        String amountStr = tx.amount != null ? String.format("%.2f", tx.amount) : "—";
        String refStr = tx.referenceNumber != null ? " (Ref: " + tx.referenceNumber + ")" : "";
        System.out.println("  " + (i + 1) + ". " + tx.date + " | " + tx.description + " | $" + amountStr + refStr);
      }
    }

    return new ProcessResult("success", parsedMarkdown, extractedData);
  }

  public static void main(String[] args) {
    String filePath = args.length > 0 ? args[0] : "__FILE_PATH__";
    try {
      ProcessResult output = processBankStatement(filePath);
      System.out.println("\n[Output JSON]");
      System.out.println(jsonPrettyPrint(output.extractedData));
    } catch (Exception error) {
      System.err.println("[Error] " + error.getMessage());
      error.printStackTrace();
      System.exit(1);
    }
  }

  private static String jsonPrettyPrint(BankStatementData data) {
    JsonObjectBuilder objBuilder = Json.createObjectBuilder();
    if (data.bankName != null) objBuilder.add("bank_name", data.bankName);
    else objBuilder.addNull("bank_name");
    if (data.accountType != null) objBuilder.add("account_type", data.accountType);
    else objBuilder.addNull("account_type");
    if (data.accountHolderName != null) objBuilder.add("account_holder_name", data.accountHolderName);
    else objBuilder.addNull("account_holder_name");
    if (data.accountHolderAddress != null) objBuilder.add("account_holder_address", data.accountHolderAddress);
    else objBuilder.addNull("account_holder_address");
    if (data.accountNumber != null) objBuilder.add("account_number", data.accountNumber);
    else objBuilder.addNull("account_number");
    if (data.statementPeriodStart != null) objBuilder.add("statement_period_start", data.statementPeriodStart);
    else objBuilder.addNull("statement_period_start");
    if (data.statementPeriodEnd != null) objBuilder.add("statement_period_end", data.statementPeriodEnd);
    else objBuilder.addNull("statement_period_end");
    if (data.beginningBalance != null) objBuilder.add("beginning_balance", data.beginningBalance);
    else objBuilder.addNull("beginning_balance");
    if (data.endingBalance != null) objBuilder.add("ending_balance", data.endingBalance);
    else objBuilder.addNull("ending_balance");
    if (data.totalDepositsCredits != null) objBuilder.add("total_deposits_credits", data.totalDepositsCredits);
    else objBuilder.addNull("total_deposits_credits");
    if (data.totalWithdrawals != null) objBuilder.add("total_withdrawals", data.totalWithdrawals);
    else objBuilder.addNull("total_withdrawals");

    JsonArrayBuilder txBuilder = Json.createArrayBuilder();
    for (Transaction tx : data.transactions) {
      JsonObjectBuilder txObj = Json.createObjectBuilder();
      if (tx.date != null) txObj.add("date", tx.date);
      else txObj.addNull("date");
      if (tx.description != null) txObj.add("description", tx.description);
      else txObj.addNull("description");
      if (tx.amount != null) txObj.add("amount", tx.amount);
      else txObj.addNull("amount");
      if (tx.referenceNumber != null) txObj.add("reference_number", tx.referenceNumber);
      else txObj.addNull("reference_number");
      txBuilder.add(txObj.build());
    }
    objBuilder.add("transactions", txBuilder.build());

    JsonObject result = objBuilder.build();
    StringWriter sw = new StringWriter();
    JsonWriter jsonWriter = Json.createWriter(sw);
    jsonWriter.writeObject(result);
    jsonWriter.close();
    return sw.toString();
  }
}
```
// This code uses the Extend REST API directly since Extend does not publish an official Go SDK.
// It calls https://api.extend.ai endpoints using only Go's standard library (net/http, encoding/json).

package main

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

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

var apiKey = os.Getenv("EXTEND_API_KEY")

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

// Transaction represents a single transaction.
type Transaction struct {
	Date            *string  `json:"date"`
	Description     *string  `json:"description"`
	Amount          *float64 `json:"amount"`
	ReferenceNumber *string  `json:"reference_number"`
}

// ExtractedData represents the extracted bank statement fields.
type ExtractedData struct {
	BankName             *string        `json:"bank_name"`
	AccountType          *string        `json:"account_type"`
	AccountHolderName    *string        `json:"account_holder_name"`
	AccountHolderAddress *string        `json:"account_holder_address"`
	AccountNumber        *string        `json:"account_number"`
	StatementPeriodStart *string        `json:"statement_period_start"`
	StatementPeriodEnd   *string        `json:"statement_period_end"`
	BeginningBalance     *float64       `json:"beginning_balance"`
	EndingBalance        *float64       `json:"ending_balance"`
	TotalDepositsCredits *float64       `json:"total_deposits_credits"`
	TotalWithdrawals     *float64       `json:"total_withdrawals"`
	Transactions         []Transaction  `json:"transactions"`
}

// ExtractRunResponse represents the response from an extract run.
type ExtractRunResponse struct {
	Status string `json:"status"`
	Output struct {
		Value ExtractedData `json:"value"`
	} `json:"output"`
	ID string `json:"id"`
}

// createAndPollParseRun initiates and polls a parse run.
func createAndPollParseRun(fileDataURL string) (*ParseRunResponse, error) {
	requestBody := map[string]interface{}{
		"file": map[string]string{
			"url": fileDataURL,
		},
		"config": map[string]interface{}{
			"blockOptions": map[string]interface{}{
				"text": map[string]interface{}{
					"agentic": map[string]bool{
						"enabled": true,
					},
				},
			},
			"chunkingStrategy": map[string]string{
				"type": "document",
			},
		},
	}

	bodyBytes, _ := json.Marshal(requestBody)
	req, _ := http.NewRequest("POST", baseURL+"/parseruns", bytes.NewReader(bodyBytes))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

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

	var runResponse ParseRunResponse
	json.NewDecoder(resp.Body).Decode(&runResponse)
	runID := runResponse.ID

	// Poll until status is PROCESSED
	for {
		req, _ := http.NewRequest("GET", baseURL+"/parseruns/"+runID, nil)
		req.Header.Set("Authorization", "Bearer "+apiKey)

		resp, _ := client.Do(req)
		var pollResponse ParseRunResponse
		json.NewDecoder(resp.Body).Decode(&pollResponse)
		resp.Body.Close()

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

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

// createAndPollExtractRun initiates and polls an extract run.
func createAndPollExtractRun(fileDataURL string) (*ExtractRunResponse, error) {
	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"bank_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Name of the bank issuing the statement, as it appears in the statement header or footer.",
			},
			"account_type": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Type of account, e.g., 'Checking', 'Student Checking', 'Savings', 'Money Market'.",
			},
			"account_holder_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Full name of the account holder, as shown on the statement header or account section.",
			},
			"account_holder_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Mailing address of the account holder. Include street, city, state, and ZIP.",
			},
			"account_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Bank account number. Often displayed in full or with last 4 digits shown; capture exactly as printed.",
			},
			"statement_period_start": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Statement start date in format 'MMM DD, YYYY' (e.g., 'Jan 01, 2024'). Do not use ISO format.",
			},
			"statement_period_end": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Statement end date in format 'MMM DD, YYYY' (e.g., 'Jan 31, 2024'). Do not use ISO format.",
			},
			"beginning_balance": map[string]interface{}{
				"type":        []string{"number", "null"},
				"description": "Opening balance at the start of the statement period. Extract as numeric value only (e.g., 1234.56), no currency symbol.",
			},
			"ending_balance": map[string]interface{}{
				"type":        []string{"number", "null"},
				"description": "Closing balance at the end of the statement period. Extract as numeric value only (e.g., 5678.90), no currency symbol.",
			},
			"total_deposits_credits": map[string]interface{}{
				"type":        []string{"number", "null"},
				"description": "Sum of all deposits and credits during the period. Numeric value only, no currency symbol.",
			},
			"total_withdrawals": map[string]interface{}{
				"type":        []string{"number", "null"},
				"description": "Sum of all withdrawals and debits during the period. Numeric value only, no currency symbol.",
			},
			"transactions": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"date": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Transaction date in format 'MMM DD, YYYY' or 'MM/DD/YYYY'. Match the statement's format.",
						},
						"description": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Transaction description or merchant name as printed on the statement. Capture exact text.",
						},
						"amount": map[string]interface{}{
							"type":        []string{"number", "null"},
							"description": "Transaction amount as numeric value (e.g., 123.45). Do not include currency symbol. Positive for deposits, negative for withdrawals (if statement shows direction).",
						},
						"reference_number": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Transaction reference, confirmation, or check number if present. Leave null if not provided.",
						},
					},
				},
				"description": "List of all transactions on the statement, in order as they appear. Each transaction includes date, description, amount, and optional reference number.",
			},
		},
	}

	requestBody := map[string]interface{}{
		"file": map[string]string{
			"url": fileDataURL,
		},
		"config": map[string]interface{}{
			"schema":          schema,
			"baseProcessor":   "extraction_performance",
			"advancedOptions": map[string]interface{}{
				"reviewAgent": map[string]bool{
					"enabled": true,
				},
				"advancedMultimodalEnabled": true,
			},
		},
	}

	bodyBytes, _ := json.Marshal(requestBody)
	req, _ := http.NewRequest("POST", baseURL+"/extractruns", bytes.NewReader(bodyBytes))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

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

	var runResponse ExtractRunResponse
	json.NewDecoder(resp.Body).Decode(&runResponse)
	runID := runResponse.ID

	// Poll until status is PROCESSED
	for {
		req, _ := http.NewRequest("GET", baseURL+"/extractruns/"+runID, nil)
		req.Header.Set("Authorization", "Bearer "+apiKey)

		resp, _ := client.Do(req)
		var pollResponse ExtractRunResponse
		json.NewDecoder(resp.Body).Decode(&pollResponse)
		resp.Body.Close()

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

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

// processBankStatement runs the full pipeline.
func processBankStatement(filePath string) (map[string]interface{}, error) {
	fmt.Printf("\n[Bank Statement Processing] Starting pipeline for: %s\n\n", filePath)

	// Read file and convert to base64 data URL
	fileBytes, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}
	dataURL := "data:application/octet-stream;base64," + base64.StdEncoding.EncodeToString(fileBytes)

	// Step 1: Parse to markdown
	fmt.Println("[Step 1/2] Parsing statement to markdown...")
	parseRun, err := createAndPollParseRun(dataURL)
	if err != nil {
		return nil, err
	}

	parsedMarkdown := ""
	for _, chunk := range parseRun.Output.Chunks {
		parsedMarkdown += chunk.Content + "\n\n"
	}
	fmt.Printf("✓ Parsed %d chunks\n\n", len(parseRun.Output.Chunks))

	// Step 2: Extract structured fields
	fmt.Println("[Step 2/2] Extracting bank statement fields...")
	extractRun, err := createAndPollExtractRun(dataURL)
	if err != nil {
		return nil, err
	}

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

	// Print summary
	fmt.Println("[Results Summary]")
	if result.BankName != nil {
		fmt.Printf("Bank: %s\n", *result.BankName)
	} else {
		fmt.Println("Bank: —")
	}
	if result.AccountHolderName != nil {
		fmt.Printf("Account Holder: %s\n", *result.AccountHolderName)
	} else {
		fmt.Println("Account Holder: —")
	}
	if result.AccountType != nil {
		fmt.Printf("Account Type: %s\n", *result.AccountType)
	} else {
		fmt.Println("Account Type: —")
	}
	if result.AccountNumber != nil {
		fmt.Printf("Account #: %s\n", *result.AccountNumber)
	} else {
		fmt.Println("Account #: —")
	}

	var startDate, endDate string
	if result.StatementPeriodStart != nil {
		startDate = *result.StatementPeriodStart
	} else {
		startDate = "—"
	}
	if result.StatementPeriodEnd != nil {
		endDate = *result.StatementPeriodEnd
	} else {
		endDate = "—"
	}
	fmt.Printf("Statement Period: %s to %s\n", startDate, endDate)

	if result.BeginningBalance != nil {
		fmt.Printf("Beginning Balance: $%.2f\n", *result.BeginningBalance)
	} else {
		fmt.Println("Beginning Balance: —")
	}
	if result.EndingBalance != nil {
		fmt.Printf("Ending Balance: $%.2f\n", *result.EndingBalance)
	} else {
		fmt.Println("Ending Balance: —")
	}
	if result.TotalDepositsCredits != nil {
		fmt.Printf("Total Deposits/Credits: $%.2f\n", *result.TotalDepositsCredits)
	} else {
		fmt.Println("Total Deposits/Credits: —")
	}
	if result.TotalWithdrawals != nil {
		fmt.Printf("Total Withdrawals: $%.2f\n", *result.TotalWithdrawals)
	} else {
		fmt.Println("Total Withdrawals: —")
	}
	fmt.Printf("Transactions Found: %d\n", len(result.Transactions))

	if len(result.Transactions) > 0 {
		fmt.Println("\n[First 3 Transactions]")
		limit := 3
		if len(result.Transactions) < 3 {
			limit = len(result.Transactions)
		}
		for i := 0; i < limit; i++ {
			tx := result.Transactions[i]
			date := "—"
			if tx.Date != nil {
				date = *tx.Date
			}
			desc := "—"
			if tx.Description != nil {
				desc = *tx.Description
			}
			amt := "—"
			if tx.Amount != nil {
				amt = fmt.Sprintf("$%.2f", *tx.Amount)
			}
			refStr := ""
			if tx.ReferenceNumber != nil {
				refStr = fmt.Sprintf(" (Ref: %s)", *tx.ReferenceNumber)
			}
			fmt.Printf("  %d. %s | %s | %s%s\n", i+1, date, desc, amt, refStr)
		}
	}

	return map[string]interface{}{
		"status":         "success",
		"parsedMarkdown": parsedMarkdown,
		"extractedData":  result,
	}, nil
}

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

	filePath := "__FILE_PATH__"
	if len(args) > 0 {
		filePath = args[0]
	}

	output, err := processBankStatement(filePath)
	if err != nil {
		log.Fatalf("[Error] %v\n", err)
	}

	fmt.Println("\n[Output JSON]")
	jsonBytes, _ := json.MarshalIndent(output["extractedData"], "", "  ")
	fmt.Println(string(jsonBytes))
}
// Deploy the "Automated Onboarding Agent" 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/automated-onboarding-agent.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: automated-onboarding-agent).

import fs from "node:fs";
import path from "node:path";

const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }

const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "automated-onboarding-agent.json");

type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
  ? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
  : {};
function saveState() {
  fs.mkdirSync(STATE_DIR, { recursive: true });
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

async function api(method: string, pathName: string, body?: unknown) {
  const res = await fetch(API + pathName, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "x-extend-api-version": VERSION,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
  return data;
}

// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
  "name": "Automated Onboarding Agent Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "split2"
        }
      ]
    },
    {
      "name": "split2",
      "type": "SPLIT",
      "config": {
        "splitterConfig": {
          "splitClassifications": [
            {
              "id": "splitter_classification1",
              "type": "other",
              "description": "Use the `other` document type when the document can not clearly be classified into one of the described classifications."
            },
            {
              "id": "subdocument_type_Bli",
              "type": "bank_statement",
              "description": "Bank statement"
            },
            {
              "id": "subdocument_type_GUh",
              "type": "tax_return",
              "description": "Tax return"
            },
            {
              "id": "subdocument_type_Q_T",
              "type": "identification",
              "description": "Government ID"
            }
          ],
          "baseProcessor": "splitting_performance",
          "advancedOptions": {
            "pageOverlapEnabled": false
          }
        }
      },
      "next": [
        {
          "step": "extraction3",
          "classificationId": "splitter_classification1"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_Bli"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_GUh"
        },
        {
          "step": "extraction3",
          "classificationId": "subdocument_type_Q_T"
        }
      ]
    },
    {
      "name": "extraction3",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "required": [
              "ssn",
              "city",
              "state",
              "address",
              "country",
              "tax_owed",
              "tax_paid",
              "tax_year",
              "bank_name",
              "full_name",
              "last_name",
              "first_name",
              "middle_name",
              "postal_code",
              "account_type",
              "total_income",
              "transactions",
              "date_of_birth",
              "document_type",
              "filing_status",
              "refund_amount",
              "account_number",
              "ending_balance",
              "taxable_income",
              "total_deposits",
              "beginning_balance",
              "total_withdrawals",
              "account_holder_name",
              "number_of_dependents",
              "statement_period_end",
              "statement_period_start"
            ],
            "properties": {
              "ssn": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's Social Security Number or equivalent national identification number, as shown on the document. May be labeled as 'SSN', 'Social Security Number', or similar. Format and presence may vary by document type."
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The city or locality portion of the individual's address as presented on the document."
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The state, province, or region portion of the individual's address as shown on the document."
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's primary residential address as shown on the document. This may include street address, apartment or unit number, city, state, and postal code. May be split into multiple fields in some documents."
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The country portion of the individual's address as shown on the document. May be omitted if not present."
              },
              "tax_owed": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total tax amount owed as reported on the tax return. This is the calculated tax liability before payments and credits.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "tax_paid": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of tax payments made, as reported on the tax return. This includes withholding, estimated payments, and credits.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "tax_year": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The tax year or reporting period covered by the tax return or statement. For tax documents, this is the year for which the return is filed. For bank statements, this may be the statement period."
              },
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the financial institution or bank that issued the statement. May appear as a logo, header, or in the footer."
              },
              "full_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The complete legal name of the individual as presented on the document. This may include first, middle, and last names, and is essential for identity verification. In some documents, names may be split into separate fields."
              },
              "last_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's family or surname as shown on the document. May appear as 'Last Name', 'Surname', or similar labels."
              },
              "first_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's given name as shown on the document. May appear as 'First Name', 'Given Name', or similar labels."
              },
              "middle_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's middle name or initial, if present, as shown on the document. May be omitted if not provided."
              },
              "postal_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The postal or ZIP code portion of the individual's address as shown on the document."
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The type of bank account, such as 'Checking', 'Savings', 'Student Checking', etc., as indicated on the statement."
              },
              "total_income": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The individual's total income as reported on the tax return or statement. This is the sum of all income sources before deductions. May be labeled as 'Total Income', 'Adjusted Gross Income', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "transactions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "amount",
                    "description",
                    "currency_code",
                    "reference_number",
                    "transaction_date",
                    "transaction_type"
                  ],
                  "properties": {
                    "amount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The monetary value of the transaction. Positive for credits/deposits, negative for debits/withdrawals."
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A description of the transaction, such as merchant name, payment type, or transaction details."
                    },
                    "currency_code": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The ISO 4217 currency code for the transaction amount, such as 'USD', 'EUR', etc."
                    },
                    "reference_number": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A unique reference or transaction number associated with this transaction, if available."
                    },
                    "transaction_date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The date the transaction was posted or occurred, as shown on the statement.",
                      "extend:type": "date"
                    },
                    "transaction_type": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The type of transaction, such as 'deposit', 'withdrawal', 'purchase', 'fee', etc. May be inferred from context or explicitly labeled."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The list of individual transactions recorded during the statement period. Each transaction may include date, description, reference number, and amount. Transactions may include deposits, withdrawals, purchases, fees, and other account activity."
              },
              "date_of_birth": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's date of birth as presented on the document. Used for identity verification and eligibility checks. May appear in various formats and locations.",
                "extend:type": "date"
              },
              "document_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The type of document this record represents, such as 'bank_statement', 'tax_return', or 'government_id'. This helps categorize the document for onboarding and compliance purposes."
              },
              "filing_status": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The individual's tax filing status as indicated on the tax return, such as 'Single', 'Married filing jointly', 'Head of household', etc. May be represented by a checkbox or selection."
              },
              "refund_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The amount to be refunded to the individual, if any, as reported on the tax return. This is the overpayment after all calculations.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The unique account number associated with the bank statement or financial account. May be labeled as 'Account Number', 'Statement Account', or similar."
              },
              "ending_balance": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The account balance at the end of the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "taxable_income": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The individual's taxable income as reported on the tax return. This is the income amount after deductions and exemptions, used to calculate tax owed.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "total_deposits": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of deposits or credits during the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "beginning_balance": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The account balance at the start of the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "total_withdrawals": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of withdrawals or debits during the statement period, as shown on the bank statement.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "account_holder_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the primary account holder as shown on the bank statement. May include full name or be split into components."
              },
              "number_of_dependents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The total number of dependents claimed on the tax return, if applicable. May be explicitly stated or inferred from a list of dependents."
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The end date of the statement period for bank statements or financial records. Indicates the end of the covered period.",
                "extend:type": "date"
              },
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The start date of the statement period for bank statements or financial records. Indicates the beginning of the covered period.",
                "extend:type": "date"
              }
            },
            "additionalProperties": false
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

async function main() {
  console.log(`Deploying "${WORKFLOW.name}"…`);

  if (state.workflowId) {
    console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
    await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
  } else {
    // Reuse an existing workflow with the same name if one exists (e.g. a
    // previous run's state file was lost) instead of creating a duplicate.
    try {
      const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
      const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
      const existing = items.find((x) => x.name === WORKFLOW.name);
      if (existing?.id) {
        state.workflowId = existing.id; saveState();
        console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
        await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
      }
    } catch { /* lookup is best-effort; fall through to create */ }

    if (!state.workflowId) {
      const created = await api("POST", "/workflows", WORKFLOW);
      const wfId = created.id ?? created.workflow?.id;
      if (!wfId) throw new Error("Could not read created workflow id from response");
      state.workflowId = wfId; saveState();
      console.log(`+ created workflow (${wfId})`);
    }
  }

  // Deploy the current draft as a new version so the workflow is runnable —
  // best-effort: some accounts/plans may not require this explicit step.
  await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});

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

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from typing import TypedDict, Optional, Any

from extend_ai import Extend

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

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "automated-onboarding-agent.json"

class State(TypedDict):
    workflowId: Optional[str]

def load_state() -> State:
    if STATE_FILE.exists():
        with open(STATE_FILE, "r") as f:
            return json.load(f)
    return {}

def save_state(state: State) -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)

WORKFLOW = {
    "name": "Bank Statement Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [
                {
                    "step": "parse1"
                }
            ]
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {
                                "enabled": True
                            }
                        }
                    },
                    "chunkingStrategy": {
                        "type": "document"
                    }
                }
            },
            "next": [
                {
                    "step": "extraction2"
                }
            ]
        },
        {
            "name": "extraction2",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "bank_name": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Name of the bank issuing the statement"
                            },
                            "account_type": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Type of account (e.g., Student Checking)"
                            },
                            "transactions": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "date": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "Transaction date"
                                        },
                                        "amount": {
                                            "type": [
                                                "number",
                                                "null"
                                            ],
                                            "description": "Transaction amount"
                                        },
                                        "description": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "Transaction description"
                                        },
                                        "reference_number": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "Reference or confirmation number"
                                        }
                                    }
                                },
                                "description": "List of transactions"
                            },
                            "account_number": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Bank account number"
                            },
                            "ending_balance": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Account balance at the end of the statement period"
                            },
                            "beginning_balance": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Account balance at the start of the statement period"
                            },
                            "total_withdrawals": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Total amount of all withdrawals during the period"
                            },
                            "account_holder_name": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Name of the account holder"
                            },
                            "statement_period_end": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "End date of statement period in format MMM DD, YYYY"
                            },
                            "account_holder_address": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Mailing address of the account holder"
                            },
                            "statement_period_start": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Start date of statement period in format MMM DD, YYYY"
                            },
                            "total_deposits_credits": {
                                "type": [
                                    "number",
                                    "null"
                                ],
                                "description": "Total amount of deposits and credits during the period"
                            }
                        }
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {
                            "enabled": True
                        },
                        "advancedMultimodalEnabled": True
                    }
                }
            }
        }
    ]
}

async def main() -> None:
    client = Extend(token=API_KEY)
    state = load_state()

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

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        await client.workflows.update(
            id=workflow_id,
            steps=WORKFLOW["steps"]
        )
    else:
        try:
            workflows_list = await client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, 'data') else (workflows_list.items if hasattr(workflows_list, 'items') else [])
            existing = next((x for x in items if x.name == WORKFLOW["name"]), None)
            if existing and existing.id:
                state["workflowId"] = existing.id
                save_state(state)
                print(f"✓ workflow \"{WORKFLOW['name']}\" found in your account ({existing.id}) — updating steps")
                await client.workflows.update(
                    id=existing.id,
                    steps=WORKFLOW["steps"]
                )
        except Exception:
            pass

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

    try:
        await client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass

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

if __name__ == "__main__":
    import asyncio
    try:
        asyncio.run(main())
    except Exception as e:
        print(f"{str(e)}", file=sys.stderr)
        sys.exit(1)
/*
 * Bank Statement Pipeline Provisioning Script
 * 
 * This uses the Extend REST API directly (base URL https://api.extend.ai)
 * because Extend does not publish an official Java SDK yet.
 * 
 * Usage:
 *   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
 *   javac Provision.java && java Provision
 */

import java.io.*;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

public class Provision {
    private static final String API = "https://api.extend.ai";
    private static final String VERSION = "2026-02-09";
    private static final String API_KEY = System.getenv("EXTEND_API_KEY");
    private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
    private static final Path STATE_FILE = STATE_DIR.resolve("automated-onboarding-agent.json");

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

    private static final HttpClient httpClient = HttpClient.newHttpClient();
    private static Map<String, Object> state = new HashMap<>();

    static {
        if (Files.exists(STATE_FILE)) {
            try {
                String content = Files.readString(STATE_FILE);
                state = parseJson(content);
            } catch (IOException e) {
                state = new HashMap<>();
            }
        }
    }

    private static void saveState() throws IOException {
        Files.createDirectories(STATE_DIR);
        Files.writeString(STATE_FILE, toJsonString(state));
    }

    private static String toJsonString(Object obj) {
        if (obj == null) return "null";
        if (obj instanceof String) return "\"" + ((String) obj).replace("\"", "\\\"") + "\"";
        if (obj instanceof Number || obj instanceof Boolean) 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> e : map.entrySet()) {
                if (!first) sb.append(",");
                sb.append("\"").append(e.getKey()).append("\":");
                sb.append(toJsonString(e.getValue()));
                first = false;
            }
            sb.append("}");
            return sb.toString();
        }
        if (obj instanceof List) {
            List<?> list = (List<?>) obj;
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < list.size(); i++) {
                if (i > 0) sb.append(",");
                sb.append(toJsonString(list.get(i)));
            }
            sb.append("]");
            return sb.toString();
        }
        return obj.toString();
    }

    private static Map<String, Object> parseJson(String json) {
        json = json.trim();
        if (!json.startsWith("{")) return new HashMap<>();
        Map<String, Object> map = new HashMap<>();
        json = json.substring(1, json.length() - 1);
        int depth = 0;
        StringBuilder currentKey = new StringBuilder();
        StringBuilder currentValue = new StringBuilder();
        boolean inString = false;
        boolean readingKey = true;

        for (int i = 0; i < json.length(); i++) {
            char c = json.charAt(i);
            if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
                inString = !inString;
            }
            if (!inString && (c == '{' || c == '[')) depth++;
            if (!inString && (c == '}' || c == ']')) depth--;
            if (!inString && c == ':' && depth == 0 && readingKey) {
                readingKey = false;
                currentKey = new StringBuilder(currentKey.toString().trim().replaceAll("^\"|\"$", ""));
                currentValue = new StringBuilder();
                i++;
                while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
                i--;
            } else if (!inString && c == ',' && depth == 0) {
                String valStr = currentValue.toString().trim();
                Object val = valStr;
                if (valStr.equals("null")) val = null;
                else if (valStr.equals("true")) val = true;
                else if (valStr.equals("false")) val = false;
                else if (valStr.matches("-?\\d+")) val = Long.parseLong(valStr);
                else if (valStr.matches("-?\\d+\\.\\d+")) val = Double.parseDouble(valStr);
                else val = valStr.replaceAll("^\"|\"$", "");
                map.put(currentKey.toString(), val);
                currentKey = new StringBuilder();
                currentValue = new StringBuilder();
                readingKey = true;
            } else {
                if (readingKey) currentKey.append(c);
                else currentValue.append(c);
            }
        }
        if (currentKey.length() > 0 && !readingKey) {
            String valStr = currentValue.toString().trim();
            Object val = valStr;
            if (valStr.equals("null")) val = null;
            else if (valStr.equals("true")) val = true;
            else if (valStr.equals("false")) val = false;
            else if (valStr.matches("-?\\d+")) val = Long.parseLong(valStr);
            else if (valStr.matches("-?\\d+\\.\\d+")) val = Double.parseDouble(valStr);
            else val = valStr.replaceAll("^\"|\"$", "");
            map.put(currentKey.toString(), val);
        }
        return map;
    }

    private static Map<String, Object> apiCall(String method, String pathName, Object body)
            throws Exception {
        URI uri = URI.create(API + pathName);
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(uri)
                .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 = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        Map<String, Object> data = new HashMap<>();
        if (!response.body().isEmpty()) {
            data = parseJson(response.body());
        }

        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            String errMsg = toJsonString(data);
            if (errMsg.length() > 300) errMsg = errMsg.substring(0, 300);
            throw new Exception(method + " " + pathName + " failed (" + response.statusCode() + "): " + errMsg);
        }
        return data;
    }

    private static Map<String, Object> buildWorkflow() {
        Map<String, Object> workflow = new LinkedHashMap<>();
        workflow.put("name", "Bank Statement Processing Pipeline");

        List<Map<String, Object>> steps = new ArrayList<>();

        // Step 1: Trigger
        Map<String, Object> triggerStep = new LinkedHashMap<>();
        triggerStep.put("name", "startTrigger1");
        triggerStep.put("type", "TRIGGER");
        List<Map<String, Object>> triggerNext = new ArrayList<>();
        Map<String, Object> triggerTransition = new LinkedHashMap<>();
        triggerTransition.put("step", "parse1");
        triggerNext.add(triggerTransition);
        triggerStep.put("next", triggerNext);
        steps.add(triggerStep);

        // Step 2: Parse
        Map<String, Object> parseStep = new LinkedHashMap<>();
        parseStep.put("name", "parse1");
        parseStep.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> agentic = new LinkedHashMap<>();
        agentic.put("enabled", true);
        textBlock.put("agentic", agentic);
        blockOptions.put("text", textBlock);
        parseConfigInner.put("blockOptions", blockOptions);
        Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
        chunkingStrategy.put("type", "document");
        parseConfigInner.put("chunkingStrategy", chunkingStrategy);
        parseConfig.put("parseConfig", parseConfigInner);
        parseStep.put("config", parseConfig);
        List<Map<String, Object>> parseNext = new ArrayList<>();
        Map<String, Object> parseTransition = new LinkedHashMap<>();
        parseTransition.put("step", "extraction2");
        parseNext.add(parseTransition);
        parseStep.put("next", parseNext);
        steps.add(parseStep);

        // Step 3: Extract
        Map<String, Object> extractStep = new LinkedHashMap<>();
        extractStep.put("name", "extraction2");
        extractStep.put("type", "EXTRACT");
        Map<String, Object> extractConfig = new LinkedHashMap<>();
        Map<String, Object> extractorConfig = new LinkedHashMap<>();

        // Schema
        Map<String, Object> schema = buildSchema();
        extractorConfig.put("schema", schema);
        extractorConfig.put("baseProcessor", "extraction_performance");

        Map<String, Object> advancedOptions = new LinkedHashMap<>();
        Map<String, Object> reviewAgent = new LinkedHashMap<>();
        reviewAgent.put("enabled", true);
        advancedOptions.put("reviewAgent", reviewAgent);
        advancedOptions.put("advancedMultimodalEnabled", true);
        extractorConfig.put("advancedOptions", advancedOptions);

        extractConfig.put("extractorConfig", extractorConfig);
        extractStep.put("config", extractConfig);
        steps.add(extractStep);

        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("bank_name", buildStringProperty("Name of the bank issuing the statement"));
        properties.put("account_type", buildStringProperty("Type of account (e.g., Student Checking)"));

        Map<String, Object> transactionsProperty = new LinkedHashMap<>();
        transactionsProperty.put("type", "array");
        Map<String, Object> itemsSchema = new LinkedHashMap<>();
        itemsSchema.put("type", "object");
        Map<String, Object> transactionProps = new LinkedHashMap<>();
        transactionProps.put("date", buildStringProperty("Transaction date"));
        transactionProps.put("amount", buildNumberProperty("Transaction amount"));
        transactionProps.put("description", buildStringProperty("Transaction description"));
        transactionProps.put("reference_number", buildStringProperty("Reference or confirmation number"));
        itemsSchema.put("properties", transactionProps);
        transactionsProperty.put("items", itemsSchema);
        transactionsProperty.put("description", "List of transactions");
        properties.put("transactions", transactionsProperty);

        properties.put("account_number", buildStringProperty("Bank account number"));
        properties.put("ending_balance", buildNumberProperty("Account balance at the end of the statement period"));
        properties.put("beginning_balance", buildNumberProperty("Account balance at the start of the statement period"));
        properties.put("total_withdrawals", buildNumberProperty("Total amount of all withdrawals during the period"));
        properties.put("account_holder_name", buildStringProperty("Name of the account holder"));
        properties.put("statement_period_end", buildStringProperty("End date of statement period in format MMM DD, YYYY"));
        properties.put("account_holder_address", buildStringProperty("Mailing address of the account holder"));
        properties.put("statement_period_start", buildStringProperty("Start date of statement period in format MMM DD, YYYY"));
        properties.put("total_deposits_credits", buildNumberProperty("Total amount of deposits and credits during the period"));

        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);
        prop.put("description", description);
        return prop;
    }

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

    public static void main(String[] args) {
        try {
            Map<String, Object> workflow = buildWorkflow();
            String workflowName = (String) workflow.get("name");
            System.out.println("Deploying \"" + workflowName + "\"…");

            String workflowId = (String) state.get("workflowId");

            if (workflowId != null && !workflowId.isEmpty()) {
                System.out.println("✓ workflow already provisioned (" + workflowId + ") — updating steps");
                apiCall("POST", "/workflows/" + workflowId, Map.of("steps", workflow.get("steps")));
            } else {
                try {
                    String encoded = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
                    Map<String, Object> list = apiCall("GET", "/workflows?name=" + encoded, null);
                    List<?> items = (List<?>) (list.getOrDefault("data", list.getOrDefault("items", new ArrayList<>())));

                    String existingId = null;
                    for (Object item : items) {
                        if (item instanceof Map) {
                            Map<String, Object> itemMap = (Map<String, Object>) item;
                            if (workflowName.equals(itemMap.get("name"))) {
                                existingId = (String) itemMap.get("id");
                                break;
                            }
                        }
                    }

                    if (existingId != null && !existingId.isEmpty()) {
                        state.put("workflowId", existingId);
                        saveState();
                        System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
                        apiCall("POST", "/workflows/" + existingId, Map.of("steps", workflow.get("steps")));
                    }
                } catch (Exception e) {
                    // Lookup is best-effort
                }

                if (!state.containsKey("workflowId") || state.get("workflowId") == null) {
                    Map<String, Object> created = apiCall("POST", "/workflows", workflow);
                    String wfId = (String) (created.getOrDefault("id", 
                        ((Map<?, ?>) created.getOrDefault("workflow", new HashMap<>())).get("id")));
                    if (wfId == null || wfId.isEmpty()) {
                        throw new Exception("Could not read created workflow id from response");
                    }
                    state.put("workflowId", wfId);
                    saveState();
                    System.out.println("+ created workflow (" + wfId + ")");
                }
            }

            workflowId = (String) state.get("workflowId");
            try {
                apiCall("POST", "/workflows/" + workflowId + "/versions", new HashMap<>());
            } catch (Exception e) {
                // Best-effort
            }

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

        } catch (Exception e) {
            System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
            System.exit(1);
        }
    }
}
// Extend Bank Statement provisioning script using the REST API directly.
// Extend does not publish an official Go SDK; this uses net/http and encoding/json (stdlib only).
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   go run provision.go
//
// Generated by doc1 (template: automated-onboarding-agent).

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  = os.Getenv("EXTEND_API_KEY")
	stateDir = filepath.Join(".", ".extend")
	stateFile = filepath.Join(stateDir, "automated-onboarding-agent.json")
)

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

var state State

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

	data, err := os.ReadFile(stateFile)
	if err == nil {
		json.Unmarshal(data, &state)
	}
}

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

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

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

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("x-extend-api-version", VERSION)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

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

	data := make(map[string]interface{})
	respBody, _ := io.ReadAll(resp.Body)
	json.Unmarshal(respBody, &data)

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

	return data, nil
}

var workflow = map[string]interface{}{
	"name": "Bank Statement Processing Pipeline",
	"steps": []map[string]interface{}{
		{
			"name": "startTrigger1",
			"type": "TRIGGER",
			"next": []map[string]interface{}{
				{"step": "parse1"},
			},
		},
		{
			"name": "parse1",
			"type": "PARSE",
			"config": map[string]interface{}{
				"parseConfig": map[string]interface{}{
					"blockOptions": map[string]interface{}{
						"text": map[string]interface{}{
							"agentic": map[string]interface{}{
								"enabled": true,
							},
						},
					},
					"chunkingStrategy": map[string]interface{}{
						"type": "document",
					},
				},
			},
			"next": []map[string]interface{}{
				{"step": "extraction2"},
			},
		},
		{
			"name": "extraction2",
			"type": "EXTRACT",
			"config": map[string]interface{}{
				"extractorConfig": map[string]interface{}{
					"schema": map[string]interface{}{
						"type": "object",
						"properties": map[string]interface{}{
							"bank_name": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Name of the bank issuing the statement",
							},
							"account_type": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Type of account (e.g., Student Checking)",
							},
							"transactions": map[string]interface{}{
								"type": "array",
								"items": map[string]interface{}{
									"type": "object",
									"properties": map[string]interface{}{
										"date": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "Transaction date",
										},
										"amount": map[string]interface{}{
											"type":        []string{"number", "null"},
											"description": "Transaction amount",
										},
										"description": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "Transaction description",
										},
										"reference_number": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "Reference or confirmation number",
										},
									},
								},
								"description": "List of transactions",
							},
							"account_number": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Bank account number",
							},
							"ending_balance": map[string]interface{}{
								"type":        []string{"number", "null"},
								"description": "Account balance at the end of the statement period",
							},
							"beginning_balance": map[string]interface{}{
								"type":        []string{"number", "null"},
								"description": "Account balance at the start of the statement period",
							},
							"total_withdrawals": map[string]interface{}{
								"type":        []string{"number", "null"},
								"description": "Total amount of all withdrawals during the period",
							},
							"account_holder_name": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Name of the account holder",
							},
							"statement_period_end": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "End date of statement period in format MMM DD, YYYY",
							},
							"account_holder_address": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Mailing address of the account holder",
							},
							"statement_period_start": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Start date of statement period in format MMM DD, YYYY",
							},
							"total_deposits_credits": map[string]interface{}{
								"type":        []string{"number", "null"},
								"description": "Total amount of deposits and credits during the period",
							},
						},
					},
					"baseProcessor": "extraction_performance",
					"advancedOptions": map[string]interface{}{
						"reviewAgent": map[string]interface{}{
							"enabled": true,
						},
						"advancedMultimodalEnabled": true,
					},
				},
			},
		},
	},
}

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

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", "/workflows/"+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 by name
		query := url.QueryEscape(workflowName)
		list, err := apiCall("GET", "/workflows?name="+query, nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := list["data"].([]interface{}); ok {
				for _, v := range data {
					items = append(items, v.(map[string]interface{}))
				}
			} else if data, ok := list["items"].([]interface{}); ok {
				for _, v := range data {
					items = append(items, v.(map[string]interface{}))
				}
			}

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

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

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

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

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

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

	fmt.Println("\nDone. Run documents through it with:")
	fmt.Printf("  POST %s/workflow_runs  { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
	fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}

Frequently Asked Questions (FAQ)

Use async (`parseRuns.createAndPoll`) for production: it handles documents >10 pages reliably and scales to thousands without timeout risk. Reserve sync parsing for <5 documents in interactive workflows where sub-second latency is critical.
For critical fields (salary, start date, tax ID), flag extractions below 0.85 confidence for manual verification; for lower-stakes fields (middle name, phone), 0.70 is acceptable. Always inspect the first 10 onboarded candidates' extracted data against source documents to calibrate your threshold.
Tags
Tax FormIRSIncome TaxPersonal Tax Return
About this template

This template processes onboarding packets that include 1040s, bank statements, ID cards, etc. It first splits the packet into the different documents, and then extracts information from them such as names, SSNs, addresses, and tax-related declarations with checkboxes and multiple dependent entries.

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

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