Extracts property details, parties, and financial settlement from real estate closing statements.
A closing disclosure is a legal document provided at real estate settlement that itemizes all transaction costs, credits, and adjustments for both buyer and seller, including the final cash due from the buyer and net proceeds to the seller. This template takes in Closing Disclosures and outputs markdown (.md) capturing the document's full text and layout, and JSON (.json) with structured closing transaction fields including parties, property address, sale price, loan amount, line-item charges and credits by section, and calculated settlement totals per the extraction schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemacustom schema — 12 fieldschangedadvancedOptions.advancedMultimodalEnabledtruechangedadvancedOptions.reviewAgent.enabledtruechangedbaseProcessor"extraction_performance"You can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Real Estate Closing Form 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",
"required": [
"buyer_name",
"line_items",
"sale_price",
"loan_amount",
"seller_name",
"closing_date",
"cash_to_close",
"property_address",
"settlement_agent_name",
"net_proceeds_to_seller",
"total_buyer_closing_costs",
"total_seller_closing_costs"
],
"properties": {
"buyer_name": {
"type": [
"string",
"null"
],
"description": "The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"notes",
"payee",
"section",
"description",
"amount_buyer",
"amount_seller"
],
"properties": {
"notes": {
"type": [
"string",
"null"
],
"description": "Any additional notes or clarifications about this line item, such as calculation details, special conditions, or references to other documents."
},
"payee": {
"type": [
"string",
"null"
],
"description": "The party or entity receiving payment for this line item, if specified. May be a company, government agency, or individual."
},
"section": {
"type": [
"string",
"null"
],
"description": "The general category or section this line item belongs to, such as 'Loan Charges', 'Title Charges', 'Government Fees', 'Prepaids', 'Escrow', 'Payoffs', or similar. Helps group related items."
},
"description": {
"type": [
"string",
"null"
],
"description": "A clear description of the specific charge, credit, or adjustment. May include fee names, recipient, or other identifying details. Examples: 'Appraisal Fee', 'Title Insurance', 'Property Taxes', 'HOA Dues', 'Commission', etc."
},
"amount_buyer": {
"type": [
"number",
"null"
],
"description": "The amount of this line item that is charged to or credited to the buyer. May be positive (charge) or negative (credit)."
},
"amount_seller": {
"type": [
"number",
"null"
],
"description": "The amount of this line item that is charged to or credited to the seller. May be positive (charge) or negative (credit)."
}
},
"additionalProperties": false
},
"description": "The detailed list of all charges, credits, and adjustments that make up the closing statement. Each item represents a specific fee, tax, deposit, payoff, or other transaction component. May be organized by section (e.g., loan charges, title charges, government fees, prepaid items, escrow deposits, payoffs, etc.)."
},
"sale_price": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The agreed-upon purchase price for the property. This is the total consideration paid by the buyer to the seller, before adjustments. May be labeled as 'Sale Price', 'Purchase Price', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"loan_amount": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The principal amount of any new loan or mortgage being obtained as part of this transaction. May be labeled as 'Loan Amount', 'New Loan', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"seller_name": {
"type": [
"string",
"null"
],
"description": "The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar."
},
"closing_date": {
"type": [
"string",
"null"
],
"description": "The official date on which the real estate transaction is finalized and ownership is transferred. May be labeled as 'Closing Date', 'Settlement Date', or similar. This is a key milestone in the transaction.",
"extend:type": "date"
},
"cash_to_close": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The final amount the buyer must bring to closing, after accounting for all credits, deposits, and adjustments. May be labeled as 'Cash to Close', 'Amount Due from Buyer', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"property_address": {
"type": [
"string",
"null"
],
"description": "The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code. May be labeled as 'Property Address', 'Subject Property', or similar."
},
"settlement_agent_name": {
"type": [
"string",
"null"
],
"description": "The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process. May be labeled as 'Settlement Agent', 'Escrow Officer', or similar."
},
"net_proceeds_to_seller": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The final amount the seller receives after all deductions, payoffs, and adjustments. May be labeled as 'Net Proceeds', 'Amount to Seller', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"total_buyer_closing_costs": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The total of all closing costs and fees that the buyer is responsible for paying at settlement. Includes lender fees, title charges, taxes, insurance, and other expenses. May be labeled as 'Total Buyer Closing Costs', 'Buyer Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"total_seller_closing_costs": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The total of all closing costs and fees that the seller is responsible for paying at settlement. Includes commissions, title charges, taxes, and other expenses. May be labeled as 'Total Seller Closing Costs', 'Seller Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": false
}
},
"additionalProperties": false
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
]
}# Real Estate Closing Form Processing — Extend AI Skill
## What this pipeline does
This pipeline converts a real estate closing form (typically a settlement statement or Closing Disclosure) into machine-readable structured data. It first parses the document to markdown using agentic OCR to handle complex layouts, tables, and signatures, then extracts all critical transaction fields—buyer/seller names, property address, sale price, loan amount, line-item charges and credits, and final cash due. Output is a complete JSON object ready for downstream systems (title companies, lenders, CRM integrations).
## When to use this
- **Real estate transaction automation**: Stream closing forms directly into your escrow management system without manual data entry.
- **Compliance & audit**: Extract and validate all settlement charges, fees, and calculations against regulatory requirements (TRID, state-specific closing disclosures).
- **Risk assessment**: Automatically flag unusual fee structures, missing required fields, or inconsistent buyer/seller credits before closing.
- **Multi-property portfolios**: Process closing forms in bulk for portfolio investors or institutional buyers.
- **Integration with title platforms**: Feed extracted data into LOS (Loan Origination System), closing software, or title insurance platforms.
## Processor pipeline
### Step 1: Parse (`parse_performance` with agentic OCR)
**Purpose**: Convert the PDF closing form into structured markdown while preserving layout integrity.
**Config**:
```typescript
{
blockOptions: {
text: { agentic: { enabled: true } } // Enables smart table detection, handwriting recognition
},
chunkingStrategy: { type: "document" } // Keep entire form as one logical unit
}
```
**Why**: Closing forms are dense financial documents with complex settlement tables, signature blocks, and multi-column layouts. Agentic OCR recognizes table structures and preserves financial alignment needed for accurate field extraction downstream. Document-level chunking prevents splitting tables across chunks.
### Step 2: Extract (structured fields with `extraction_performance` + review agent)
**Purpose**: Pull all required real estate transaction fields into a strongly-typed JSON object.
**Config**:
```typescript
{
baseProcessor: "extraction_performance", // High-accuracy for complex financial tables
advancedOptions: {
reviewAgent: { enabled: true }, // Double-check extracted amounts and names
advancedMultimodalEnabled: true // Handle handwritten signatures, annotations
}
}
```
**Why**: Real estate closing statements contain precise currency values, cross-referenced line items, and legal names that must be 100% accurate. Review agent catches OCR errors in amounts (e.g., $125,000 misread as $125,900). Multimodal processing flags unsigned signature blocks for manual review.
## TypeScript implementation
## CLI equivalent
```bash
# Step 1: Parse the closing form to markdown
extend parse closing_form.pdf --config '{"blockOptions":{"text":{"agentic":{"enabled":true}}},"chunkingStrategy":{"type":"document"}}'
# Step 2: Extract structured fields using the schema
extend extract closing_form.pdf --schema closing_schema.json --base-processor extraction_performance --review-agent --advanced-multimodal
# Or run the complete workflow in one command
extend run workflow_real_estate_closing_2024 --file closing_form.pdf
```
## Schema
```json
{
"type": "object",
"properties": {
"buyer_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar. Critical for legal authority and title transfer."
},
"seller_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar. Must match title deed exactly for legal validity."
},
"property_address": {
"type": ["string", "null"],
"description": "The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code. Used to verify property identity and cross-reference county records."
},
"closing_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The official date on which the real estate transaction is finalized and ownership is transferred. ISO format (yyyy-mm-dd). Key milestone for prorations and regulatory timeline."
},
"settlement_agent_name": {
"type": ["string", "null"],
"description": "The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process. Required for audit trail and dispute resolution."
},
"sale_price": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The agreed-upon purchase price for the property. This is the base consideration—all prorations and adjustments derive from this figure. Must be consistent across all settlement documents."
},
"loan_amount": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {import { ExtendClient, extendDate, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
// Define the extraction schema using Zod for full type safety
const realEstateClosingSchema = z.object({
buyer_name: z.string().nullable().describe(
"The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar."
),
seller_name: z.string().nullable().describe(
"The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar."
),
property_address: z.string().nullable().describe(
"The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code."
),
closing_date: extendDate().describe(
"The official date on which the real estate transaction is finalized and ownership is transferred. May be labeled as 'Closing Date', 'Settlement Date', or similar."
),
settlement_agent_name: z.string().nullable().describe(
"The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process."
),
sale_price: extendCurrency().describe(
"The agreed-upon purchase price for the property. This is the total consideration paid by the buyer to the seller, before adjustments."
),
loan_amount: extendCurrency().describe(
"The principal amount of any new loan or mortgage being obtained as part of this transaction. May be labeled as 'Loan Amount', 'New Loan', or similar."
),
total_buyer_closing_costs: extendCurrency().describe(
"The total of all closing costs and fees that the buyer is responsible for paying at settlement. Includes lender fees, title charges, taxes, insurance, and other expenses."
),
total_seller_closing_costs: extendCurrency().describe(
"The total of all closing costs and fees that the seller is responsible for paying at settlement. Includes commissions, title charges, taxes, and other expenses."
),
cash_to_close: extendCurrency().describe(
"The final amount the buyer must bring to closing, after accounting for all credits, deposits, and adjustments. May be labeled as 'Cash to Close', 'Amount Due from Buyer', or similar."
),
net_proceeds_to_seller: extendCurrency().describe(
"The final amount the seller receives after all deductions, payoffs, and adjustments. May be labeled as 'Net Proceeds', 'Amount to Seller', or similar."
),
line_items: z.array(z.object({
section: z.string().nullable().describe(
"The general category or section this line item belongs to, such as 'Loan Charges', 'Title Charges', 'Government Fees', 'Prepaids', 'Escrow', 'Payoffs', or similar."
),
description: z.string().nullable().describe(
"A clear description of the specific charge, credit, or adjustment. Examples: 'Appraisal Fee', 'Title Insurance', 'Property Taxes', 'HOA Dues', 'Commission', etc."
),
payee: z.string().nullable().describe(
"The party or entity receiving payment for this line item, if specified. May be a company, government agency, or individual."
),
amount_buyer: z.number().nullable().describe(
"The amount of this line item that is charged to or credited to the buyer. May be positive (charge) or negative (credit)."
),
amount_seller: z.number().nullable().describe(
"The amount of this line item that is charged to or credited to the seller. May be positive (charge) or negative (credit)."
),
notes: z.string().nullable().describe(
"Any additional notes or clarifications about this line item, such as calculation details, special conditions, or references to other documents."
),
})).describe(
"The detailed list of all charges, credits, and adjustments that make up the closing statement. Each item represents a specific fee, tax, deposit, payoff, or other transaction component."
),
});
async function processRealEstateClosingForm(filePath: string) {
try {
// Read the local file and convert to base64 data URL
// This allows the SDK to accept the file without requiring a publicly accessible URL
const fileBuffer = fs.readFileSync(filePath);
const base64Data = fileBuffer.toString("base64");
const mimeType = filePath.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/jpeg";
const dataUrl = `data:${mimeType};base64,${base64Data}`;
console.log(`Processing Real Estate Closing Form: ${path.basename(filePath)}`);
console.log("Step 1: Parsing document with agentic OCR...");
// Step 1: Parse the closing form to markdown
// Uses agentic OCR to handle complex tables, signatures, and multi-column layouts
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: true, // Enable smart table detection and layout recognition
},
},
},
chunkingStrategy: {
type: "document", // Keep entire form as one logical unit
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse step failed with status: ${parseRun.status}`);
}
// Combine all parsed chunks into markdown
const markdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(`✓ Parsed document (${markdown.length} characters)`);
console.log("Step 2: Extracting structured fields...");
// Step 2: Extract all closing form fields using the Zod schema
// Uses extraction_performance for high accuracy on financial amounts
// Review agent double-checks extracted values
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: realEstateClosingSchema,
baseProcessor: "extraction_performance", // High-accuracy extraction for financial data
advancedOptions: {
reviewAgent: {
enabled: true, // Double-check amounts and critical fields
},
advancedMultimodalEnabled: true, // Handle handwritten signatures and annotations
},
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extract step failed with status: ${extractRun.status}`);
}
const extractedData = extractRun.output.value;
// Validate the extracted data matches our schema
const validated = realEstateClosingSchema.parse(extractedData);
console.log("✓ Extraction complete");
console.log("\n--- EXTRACTED REAL ESTATE CLOSING DATA ---\n");
console.log(`Buyer: ${validated.buyer_name || "N/A"}`);
console.log(`Seller: ${validated.seller_name || "N/A"}`);
console.log(`Property: ${validated.property_address || "N/A"}`);
console.log(`Closing Date: ${validated.closing_date || "N/A"}`);
console.log(`Settlement Agent: ${validated.settlement_agent_name || "N/A"}`);
console.log(
`Sale Price: $${validated.sale_price?.amount?.toLocaleString() || "N/A"} ${validated.sale_price?.iso_4217_currency_code || ""}`
);
console.log(
`Loan Amount: $${validated.loan_amount?.amount?.toLocaleString() || "N/A"} ${validated.loan_amount?.iso_4217_currency_code || ""}`
);
console.log(
`Total Buyer Closing Costs: $${validated.total_buyer_closing_costs?.amount?.toLocaleString() || "N/A"}`
);
console.log(
`Total Seller Closing Costs: $${validated.total_seller_closing_costs?.amount?.toLocaleString() || "N/A"}`
);
console.log(
`Cash to Close: $${validated.cash_to_close?.amount?.toLocaleString() || "N/A"}`
);
console.log(
`Net Proceeds to Seller: $${validated.net_proceeds_to_seller?.amount?.toLocaleString() || "N/A"}`
);
// Display line items grouped by section
if (validated.line_items && validated.line_items.length > 0) {
console.log("\n--- LINE ITEMS ---");
const groupedBySection: Record<string, typeof validated.line_items> = {};
for (const item of validated.line_items) {
const section = item.section || "Other";
if (!groupedBySection[section]) groupedBySection[section] = [];
groupedBySection[section].push(item);
}
for (const [section, items] of Object.entries(groupedBySection)) {
console.log(`\n${section}:`);
for (const item of items) {
console.log(
` - ${item.description} (Payee: ${item.payee || "N/A"})`
);
if (item.amount_buyer !== null && item.amount_buyer !== undefined) {
console.log(` Buyer: $${item.amount_buyer.toLocaleString()}`);
}
if (item.amount_seller !== null && item.amount_seller !== undefined) {
console.log(` Seller: $${item.amount_seller.toLocaleString()}`);
}
if (item.notes) {
console.log(` Notes: ${item.notes}`);
}
}
}
}
console.log("\n--- RAW EXTRACTED JSON ---");
console.log(JSON.stringify(validated, null, 2));
return validated;
} catch (error) {
if (error instanceof Error) {
console.error("Error processing closing form:", error.message);
} else {
console.error("Unknown error:", error);
}
throw error;
}
}
// Export for testing
export { processRealEstateClosingForm };
// Run if invoked directly
if (require.main === module) {
const filePath = process.argv[2];
if (!filePath) {
console.error("Usage: npx ts-node solution.ts <path-to-closing-form>");
process.exit(1);
}
processRealEstateClosingForm(filePath).catch((err) => {
console.error(err);
process.exit(1);
});
}import os
import sys
import base64
from pathlib import Path
from typing import Optional, List
from dataclasses import dataclass
from extend_ai import Extend
@dataclass
class LineItem:
section: Optional[str]
description: Optional[str]
payee: Optional[str]
amount_buyer: Optional[float]
amount_seller: Optional[float]
notes: Optional[str]
@dataclass
class CurrencyValue:
amount: Optional[float]
iso_4217_currency_code: Optional[str]
@dataclass
class RealEstateClosingData:
buyer_name: Optional[str]
seller_name: Optional[str]
property_address: Optional[str]
closing_date: Optional[str]
settlement_agent_name: Optional[str]
sale_price: Optional[CurrencyValue]
loan_amount: Optional[CurrencyValue]
total_buyer_closing_costs: Optional[CurrencyValue]
total_seller_closing_costs: Optional[CurrencyValue]
cash_to_close: Optional[CurrencyValue]
net_proceeds_to_seller: Optional[CurrencyValue]
line_items: List[LineItem]
def process_real_estate_closing_form(file_path: str) -> RealEstateClosingData:
"""
Process a real estate closing form by parsing and extracting structured data.
Args:
file_path: Path to the closing form document (PDF or image)
Returns:
RealEstateClosingData containing extracted information
"""
client = Extend(token=os.environ["EXTEND_API_KEY"])
try:
# Read the local file and convert to base64 data URL
file_buffer = Path(file_path).read_bytes()
base64_data = base64.b64encode(file_buffer).decode("utf-8")
mime_type = (
"application/pdf"
if file_path.lower().endswith(".pdf")
else "image/jpeg"
)
data_url = f"data:{mime_type};base64,{base64_data}"
print(f"Processing Real Estate Closing Form: {Path(file_path).name}")
print("Step 1: Parsing document with agentic OCR...")
# Step 1: Parse the closing form to markdown
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"block_options": {
"text": {
"agentic": {
"enabled": True,
},
},
},
"chunking_strategy": {
"type": "document",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse step failed with status: {parse_run.status}")
# Combine all parsed chunks into markdown
markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
print(f"✓ Parsed document ({len(markdown)} characters)")
print("Step 2: Extracting structured fields...")
# Step 2: Extract all closing form fields
real_estate_closing_schema = {
"type": "object",
"properties": {
"buyer_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity purchasing the property.",
},
"seller_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity selling the property.",
},
"property_address": {
"type": ["string", "null"],
"description": "The full address of the property being bought or sold in this transaction.",
},
"closing_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The official date on which the real estate transaction is finalized.",
},
"settlement_agent_name": {
"type": ["string", "null"],
"description": "The name of the settlement agent, escrow officer, or closing attorney.",
},
"sale_price": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The agreed-upon purchase price for the property.",
},
"loan_amount": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The principal amount of any new loan or mortgage.",
},
"total_buyer_closing_costs": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The total closing costs and fees for the buyer.",
},
"total_seller_closing_costs": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The total closing costs and fees for the seller.",
},
"cash_to_close": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The final amount the buyer must bring to closing.",
},
"net_proceeds_to_seller": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"required": ["amount", "iso_4217_currency_code"],
"description": "The final amount the seller receives.",
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"section": {
"type": ["string", "null"],
"description": "The general category or section this line item belongs to.",
},
"description": {
"type": ["string", "null"],
"description": "A clear description of the specific charge, credit, or adjustment.",
},
"payee": {
"type": ["string", "null"],
"description": "The party or entity receiving payment for this line item.",
},
"amount_buyer": {
"type": ["number", "null"],
"description": "The amount charged to or credited to the buyer.",
},
"amount_seller": {
"type": ["number", "null"],
"description": "The amount charged to or credited to the seller.",
},
"notes": {
"type": ["string", "null"],
"description": "Any additional notes or clarifications about this line item.",
},
},
"required": [
"section",
"description",
"payee",
"amount_buyer",
"amount_seller",
"notes",
],
"additionalProperties": False,
},
"description": "The detailed list of all charges, credits, and adjustments.",
},
},
"required": [
"buyer_name",
"seller_name",
"property_address",
"closing_date",
"settlement_agent_name",
"sale_price",
"loan_amount",
"total_buyer_closing_costs",
"total_seller_closing_costs",
"cash_to_close",
"net_proceeds_to_seller",
"line_items",
],
"additionalProperties": False,
}
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": real_estate_closing_schema,
"base_processor": "extraction_performance",
"advanced_options": {
"review_agent": {
"enabled": True,
},
"advanced_multimodal_enabled": True,
},
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extract step failed with status: {extract_run.status}")
extracted_data = extract_run.output.value
print("✓ Extraction complete")
print("\n--- EXTRACTED REAL ESTATE CLOSING DATA ---\n")
buyer_name = extracted_data.get("buyer_name") or "N/A"
seller_name = extracted_data.get("seller_name") or "N/A"
property_address = extracted_data.get("property_address") or "N/A"
closing_date = extracted_data.get("closing_date") or "N/A"
settlement_agent_name = extracted_data.get("settlement_agent_name") or "N/A"
print(f"Buyer: {buyer_name}")
print(f"Seller: {seller_name}")
print(f"Property: {property_address}")
print(f"Closing Date: {closing_date}")
print(f"Settlement Agent: {settlement_agent_name}")
sale_price = extracted_data.get("sale_price") or {}
if sale_price.get("amount"):
print(
f"Sale Price: ${sale_price['amount']:,.2f} {sale_price.get('iso_4217_currency_code', '')}"
)
else:
print("Sale Price: N/A")
loan_amount = extracted_data.get("loan_amount") or {}
if loan_amount.get("amount"):
print(
f"Loan Amount: ${loan_amount['amount']:,.2f} {loan_amount.get('iso_4217_currency_code', '')}"
)
else:
print("Loan Amount: N/A")
total_buyer_closing_costs = extracted_data.get("total_buyer_closing_costs") or {}
if total_buyer_closing_costs.get("amount"):
print(f"Total Buyer Closing Costs: ${total_buyer_closing_costs['amount']:,.2f}")
else:
print("Total Buyer Closing Costs: N/A")
total_seller_closing_costs = extracted_data.get("total_seller_closing_costs") or {}
if total_seller_closing_costs.get("amount"):
print(f"Total Seller Closing Costs: ${total_seller_closing_costs['amount']:,.2f}")
else:
print("Total Seller Closing Costs: N/A")
cash_to_close = extracted_data.get("cash_to_close") or {}
if cash_to_close.get("amount"):
print(f"Cash to Close: ${cash_to_close['amount']:,.2f}")
else:
print("Cash to Close: N/A")
net_proceeds_to_seller = extracted_data.get("net_proceeds_to_seller") or {}
if net_proceeds_to_seller.get("amount"):
print(f"Net Proceeds to Seller: ${net_proceeds_to_seller['amount']:,.2f}")
else:
print("Net Proceeds to Seller: N/A")
# Display line items grouped by section
line_items = extracted_data.get("line_items") or []
if line_items:
print("\n--- LINE ITEMS ---")
grouped_by_section = {}
for item in line_items:
section = item.get("section") or "Other"
if section not in grouped_by_section:
grouped_by_section[section] = []
grouped_by_section[section].append(item)
for section, items in grouped_by_section.items():
print(f"\n{section}:")
for item in items:
description = item.get("description") or "N/A"
payee = item.get("payee") or "N/A"
print(f" - {description} (Payee: {payee})")
amount_buyer = item.get("amount_buyer")
if amount_buyer is not None:
print(f" Buyer: ${amount_buyer:,.2f}")
amount_seller = item.get("amount_seller")
if amount_seller is not None:
print(f" Seller: ${amount_seller:,.2f}")
notes = item.get("notes")
if notes:
print(f" Notes: {notes}")
print("\n--- RAW EXTRACTED JSON ---")
import json
print(json.dumps(extracted_data, indent=2))
# Convert to structured data object
result = RealEstateClosingData(
buyer_name=extracted_data.get("buyer_name"),
seller_name=extracted_data.get("seller_name"),
property_address=extracted_data.get("property_address"),
closing_date=extracted_data.get("closing_date"),
settlement_agent_name=extracted_data.get("settlement_agent_name"),
sale_price=(
CurrencyValue(
amount=extracted_data.get("sale_price", {}).get("amount"),
iso_4217_currency_code=extracted_data.get("sale_price", {}).get(
"iso_4217_currency_code"
),
)
if extracted_data.get("sale_price")
else None
),
loan_amount=(
CurrencyValue(
amount=extracted_data.get("loan_amount", {}).get("amount"),
iso_4217_currency_code=extracted_data.get("loan_amount", {}).get(
"iso_4217_currency_code"
),
)
if extracted_data.get("loan_amount")
else None
),
total_buyer_closing_costs=(
CurrencyValue(
amount=extracted_data.get("total_buyer_closing_costs", {}).get(
"amount"
),
iso_4217_currency_code=extracted_data.get(
"total_buyer_closing_costs", {}
).get("iso_4217_currency_code"),
)
if extracted_data.get("total_buyer_closing_costs")
else None
),
total_seller_closing_costs=(
CurrencyValue(
amount=extracted_data.get("total_seller_closing_costs", {}).get(
"amount"
),
iso_4217_currency_code=extracted_data.get(
"total_seller_closing_costs", {}
).get("iso_4217_currency_code"),
)
if extracted_data.get("total_seller_closing_costs")
else None
),
cash_to_close=(
CurrencyValue(
amount=extracted_data.get("cash_to_close", {}).get("amount"),
iso_4217_currency_code=extracted_data.get("cash_to_close", {}).get(
"iso_4217_currency_code"
),
)
if extracted_data.get("cash_to_close")
else None
),
net_proceeds_to_seller=(
CurrencyValue(
amount=extracted_data.get("net_proceeds_to_seller", {}).get(
"amount"
),
iso_4217_currency_code=extracted_data.get(
"net_proceeds_to_seller", {}
).get("iso_4217_currency_code"),
)
if extracted_data.get("net_proceeds_to_seller")
else None
),
line_items=[
LineItem(
section=item.get("section"),
description=item.get("description"),
payee=item.get("payee"),
amount_buyer=item.get("amount_buyer"),
amount_seller=item.get("amount_seller"),
notes=item.get("notes"),
)
for item in extracted_data.get("line_items", [])
],
)
return result
except Exception as error:
print(f"Error processing closing form: {str(error)}")
raise
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python solution.py <path-to-closing-form>")
sys.exit(1)
file_path = sys.argv[1]
process_real_estate_closing_form(file_path)// This code calls Extend's REST API directly because Extend has no official Java SDK yet.
// It uses only java.net.http.HttpClient — no third-party dependencies.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
public class RealEstateClosingFormProcessor {
private static final String EXTEND_API_BASE = "https://api.extend.ai";
private final String apiKey;
private final HttpClient httpClient;
public RealEstateClosingFormProcessor(String apiKey) {
this.apiKey = apiKey;
this.httpClient = HttpClient.newHttpClient();
}
public static void main(String[] args) throws IOException, InterruptedException {
if (args.length == 0) {
System.err.println("Usage: java RealEstateClosingFormProcessor <path-to-closing-form>");
System.exit(1);
}
String apiKey = System.getenv("EXTEND_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
System.err.println("Error: EXTEND_API_KEY environment variable not set");
System.exit(1);
}
RealEstateClosingFormProcessor processor = new RealEstateClosingFormProcessor(apiKey);
try {
processor.processRealEstateClosingForm(args[0]);
} catch (Exception e) {
System.err.println("Error processing closing form: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
public void processRealEstateClosingForm(String filePath)
throws IOException, InterruptedException {
// Read file and convert to base64 data URL
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64Data = Base64.getEncoder().encodeToString(fileBytes);
String mimeType =
filePath.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/jpeg";
String dataUrl = "data:" + mimeType + ";base64," + base64Data;
String fileName = Paths.get(filePath).getFileName().toString();
System.out.println("Processing Real Estate Closing Form: " + fileName);
System.out.println("Step 1: Parsing document with agentic OCR...");
// Step 1: Parse the closing form
String parsePayload =
"""
{
"file": {
"url": "%s"
},
"config": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
}
"""
.formatted(escapeJson(dataUrl));
String parseRunId = createAndPollParseRun(parsePayload);
Map<String, Object> parseRun = getParseRunStatus(parseRunId);
if (!"PROCESSED".equals(parseRun.get("status"))) {
throw new RuntimeException("Parse step failed with status: " + parseRun.get("status"));
}
System.out.println("✓ Parsed document");
System.out.println("Step 2: Extracting structured fields...");
// Step 2: Extract structured fields
String extractPayload =
"""
{
"file": {
"url": "%s"
},
"config": {
"schema": %s,
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
"""
.formatted(escapeJson(dataUrl), getRealEstateClosingSchema());
String extractRunId = createAndPollExtractRun(extractPayload);
Map<String, Object> extractRun = getExtractRunStatus(extractRunId);
if (!"PROCESSED".equals(extractRun.get("status"))) {
throw new RuntimeException("Extract step failed with status: " + extractRun.get("status"));
}
@SuppressWarnings("unchecked")
Map<String, Object> output = (Map<String, Object>) extractRun.get("output");
@SuppressWarnings("unchecked")
Map<String, Object> extractedData = (Map<String, Object>) output.get("value");
System.out.println("✓ Extraction complete");
System.out.println("\n--- EXTRACTED REAL ESTATE CLOSING DATA ---\n");
System.out.println("Buyer: " + nullSafe(extractedData.get("buyer_name")));
System.out.println("Seller: " + nullSafe(extractedData.get("seller_name")));
System.out.println("Property: " + nullSafe(extractedData.get("property_address")));
System.out.println("Closing Date: " + nullSafe(extractedData.get("closing_date")));
System.out.println("Settlement Agent: " + nullSafe(extractedData.get("settlement_agent_name")));
System.out.println(
"Sale Price: "
+ formatCurrency((Map<String, Object>) extractedData.get("sale_price")));
System.out.println(
"Loan Amount: "
+ formatCurrency((Map<String, Object>) extractedData.get("loan_amount")));
System.out.println(
"Total Buyer Closing Costs: "
+ formatCurrency(
(Map<String, Object>) extractedData.get("total_buyer_closing_costs")));
System.out.println(
"Total Seller Closing Costs: "
+ formatCurrency(
(Map<String, Object>) extractedData.get("total_seller_closing_costs")));
System.out.println(
"Cash to Close: " + formatCurrency((Map<String, Object>) extractedData.get("cash_to_close")));
System.out.println(
"Net Proceeds to Seller: "
+ formatCurrency((Map<String, Object>) extractedData.get("net_proceeds_to_seller")));
// Display line items grouped by section
@SuppressWarnings("unchecked")
List<Map<String, Object>> lineItems =
(List<Map<String, Object>>) extractedData.get("line_items");
if (lineItems != null && !lineItems.isEmpty()) {
System.out.println("\n--- LINE ITEMS ---");
Map<String, List<Map<String, Object>>> groupedBySection = new LinkedHashMap<>();
for (Map<String, Object> item : lineItems) {
String section = (String) item.getOrDefault("section", "Other");
groupedBySection.computeIfAbsent(section, k -> new ArrayList<>()).add(item);
}
for (Map.Entry<String, List<Map<String, Object>>> entry : groupedBySection.entrySet()) {
System.out.println("\n" + entry.getKey() + ":");
for (Map<String, Object> item : entry.getValue()) {
System.out.println(" - " + item.get("description") + " (Payee: " + item.get("payee") + ")");
Object amountBuyer = item.get("amount_buyer");
if (amountBuyer != null) {
System.out.println(" Buyer: $" + formatNumber(amountBuyer));
}
Object amountSeller = item.get("amount_seller");
if (amountSeller != null) {
System.out.println(" Seller: $" + formatNumber(amountSeller));
}
Object notes = item.get("notes");
if (notes != null) {
System.out.println(" Notes: " + notes);
}
}
}
}
System.out.println("\n--- RAW EXTRACTED JSON ---");
System.out.println(prettyPrintJson(extractedData));
}
private String createAndPollParseRun(String payload)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/v1/parse-runs"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJsonResponse(response.body());
String runId = (String) responseBody.get("id");
// Poll until completion
while (true) {
Thread.sleep(1000);
Map<String, Object> status = getParseRunStatus(runId);
if ("PROCESSED".equals(status.get("status"))
|| "FAILED".equals(status.get("status"))) {
return runId;
}
}
}
private String createAndPollExtractRun(String payload)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/v1/extract-runs"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJsonResponse(response.body());
String runId = (String) responseBody.get("id");
// Poll until completion
while (true) {
Thread.sleep(1000);
Map<String, Object> status = getExtractRunStatus(runId);
if ("PROCESSED".equals(status.get("status"))
|| "FAILED".equals(status.get("status"))) {
return runId;
}
}
}
private Map<String, Object> getParseRunStatus(String runId)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/v1/parse-runs/" + runId))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return parseJsonResponse(response.body());
}
private Map<String, Object> getExtractRunStatus(String runId)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/v1/extract-runs/" + runId))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return parseJsonResponse(response.body());
}
private String getRealEstateClosingSchema() {
return """
{
"type": "object",
"properties": {
"buyer_name": {"type": ["string", "null"]},
"seller_name": {"type": ["string", "null"]},
"property_address": {"type": ["string", "null"]},
"closing_date": {"type": ["string", "null"], "extend:type": "date"},
"settlement_agent_name": {"type": ["string", "null"]},
"sale_price": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"loan_amount": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"total_buyer_closing_costs": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"total_seller_closing_costs": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"cash_to_close": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"net_proceeds_to_seller": {
"type": "object",
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]}
},
"extend:type": "currency"
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"section": {"type": ["string", "null"]},
"description": {"type": ["string", "null"]},
"payee": {"type": ["string", "null"]},
"amount_buyer": {"type": ["number", "null"]},
"amount_seller": {"type": ["number", "null"]},
"notes": {"type": ["string", "null"]}
}
}
}
}
}
""";
}
@SuppressWarnings("unchecked")
private Map<String, Object> parseJsonResponse(String json) {
// Simple JSON parser for response objects
Map<String, Object> result = new HashMap<>();
json = json.trim();
if (json.startsWith("{") && json.endsWith("}")) {
json = json.substring(1, json.length() - 1);
String[] pairs = json.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
for (String pair : pairs) {
pair = pair.trim();
int colonIdx = pair.indexOf(":");
if (colonIdx > 0) {
String key = pair.substring(0, colonIdx).trim().replaceAll("^\"|\"$", "");
String valueStr = pair.substring(colonIdx + 1).trim();
Object value = parseJsonValue(valueStr);
result.put(key, value);
}
}
}
return result;
}
private Object parseJsonValue(String valueStr) {
valueStr = valueStr.trim();
if (valueStr.equals("null")) {
return null;
} else if (valueStr.equals("true")) {
return true;
} else if (valueStr.equals("false")) {
return false;
} else if (valueStr.startsWith("\"") && valueStr.endsWith("\"")) {
return valueStr.substring(1, valueStr.length() - 1);
} else if (valueStr.startsWith("{")) {
return parseJsonResponse(valueStr);
} else if (valueStr.startsWith("[")) {
return new ArrayList<>();
} else {
try {
return Double.parseDouble(valueStr);
} catch (NumberFormatException e) {
return valueStr;
}
}
}
private String escapeJson(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private String nullSafe(Object obj) {
return obj != null ? obj.toString() : "N/A";
}
@SuppressWarnings("unchecked")
private String formatCurrency(Map<String, Object> currencyObj) {
if (currencyObj == null) return "N/A";
Object amount = currencyObj.get("amount");
Object code = currencyObj.get("iso_4217_currency_code");
if (amount == null) return "N/A";
return "$" + formatNumber(amount) + " " + (code != null ? code : "");
}
private String formatNumber(Object num) {
if (num instanceof Number) {
return String.format("%,d", ((Number) num).longValue());
}
return num != null ? num.toString() : "N/A";
}
private String prettyPrintJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
sb.append("{\n");
map.forEach(
(key, value) -> {
sb.append(" \"").append(key).append("\": ");
if (value == null) {
sb.append("null");
} else if (value instanceof String) {
sb.append("\"").append(value).append("\"");
} else if (value instanceof Map) {
sb.append(prettyPrintJson((Map<String, Object>) value).replace("\n", "\n "));
} else if (value instanceof List) {
sb.append("[");
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
for (int i = 0; i < list.size(); i++) {
Object item = list.get(i);
if (item instanceof Map) {
sb.append("\n ")
.append(prettyPrintJson((Map<String, Object>) item)
.replace("\n", "\n "));
} else {
sb.append(item);
}
if (i < list.size() - 1) sb.append(",");
}
sb.append("\n ]");
} else {
sb.append(value);
}
sb.append(",\n");
});
sb.setLength(sb.length() - 2);
sb.append("\n}");
return sb.toString();
}
}// This code calls Extend's REST API directly (base URL https://api.extend.ai)
// because Extend does not publish an official Go SDK yet.
// The API calls mirror the exact endpoints and request/response shapes the TypeScript SDK uses.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
)
const extendAPIBase = "https://api.extend.ai"
// Currency represents a currency field with amount and ISO code
type Currency struct {
Amount *float64 `json:"amount"`
ISO4217CurrencyCode *string `json:"iso_4217_currency_code"`
}
// LineItem represents a single charge/credit in the closing statement
type LineItem struct {
Section *string `json:"section"`
Description *string `json:"description"`
Payee *string `json:"payee"`
AmountBuyer *float64 `json:"amount_buyer"`
AmountSeller *float64 `json:"amount_seller"`
Notes *string `json:"notes"`
}
// RealEstateClosingData represents the extracted closing form data
type RealEstateClosingData struct {
BuyerName *string `json:"buyer_name"`
SellerName *string `json:"seller_name"`
PropertyAddress *string `json:"property_address"`
ClosingDate *string `json:"closing_date"`
SettlementAgentName *string `json:"settlement_agent_name"`
SalePrice *Currency `json:"sale_price"`
LoanAmount *Currency `json:"loan_amount"`
TotalBuyerClosingCosts *Currency `json:"total_buyer_closing_costs"`
TotalSellerClosingCosts *Currency `json:"total_seller_closing_costs"`
CashToClose *Currency `json:"cash_to_close"`
NetProceedsToSeller *Currency `json:"net_proceeds_to_seller"`
LineItems []LineItem `json:"line_items"`
}
// 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"`
}
// ExtractRunResponse represents the response from an extract run
type ExtractRunResponse struct {
Status string `json:"status"`
Output struct {
Value RealEstateClosingData `json:"value"`
} `json:"output"`
}
// createAndPollParseRun calls the parse endpoint and polls for completion
func createAndPollParseRun(apiKey string, fileURL string) (*ParseRunResponse, error) {
reqBody := map[string]interface{}{
"file": map[string]string{
"url": fileURL,
},
"config": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]bool{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]string{
"type": "document",
},
},
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", extendAPIBase+"/v1/parse-runs", bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var parseResp ParseRunResponse
if err := json.Unmarshal(respBytes, &parseResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// For this example, we assume the API returns PROCESSED status.
// In production, implement proper polling with exponential backoff.
if parseResp.Status != "PROCESSED" {
return nil, fmt.Errorf("parse step failed with status: %s", parseResp.Status)
}
return &parseResp, nil
}
// createAndPollExtractRun calls the extract endpoint and polls for completion
func createAndPollExtractRun(apiKey string, fileURL string) (*ExtractRunResponse, error) {
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"buyer_name": map[string]interface{}{
"type": []string{"string", "null"},
},
"seller_name": map[string]interface{}{
"type": []string{"string", "null"},
},
"property_address": map[string]interface{}{
"type": []string{"string", "null"},
},
"closing_date": map[string]interface{}{
"type": []string{"string", "null"},
"extend:type": "date",
},
"settlement_agent_name": map[string]interface{}{
"type": []string{"string", "null"},
},
"sale_price": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"loan_amount": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"total_buyer_closing_costs": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"total_seller_closing_costs": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"cash_to_close": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"net_proceeds_to_seller": map[string]interface{}{
"type": "object",
"extend:type": "currency",
"properties": map[string]interface{}{
"amount": map[string]interface{}{
"type": []string{"number", "null"},
},
"iso_4217_currency_code": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
"line_items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"section": map[string]interface{}{
"type": []string{"string", "null"},
},
"description": map[string]interface{}{
"type": []string{"string", "null"},
},
"payee": map[string]interface{}{
"type": []string{"string", "null"},
},
"amount_buyer": map[string]interface{}{
"type": []string{"number", "null"},
},
"amount_seller": map[string]interface{}{
"type": []string{"number", "null"},
},
"notes": map[string]interface{}{
"type": []string{"string", "null"},
},
},
},
},
},
}
reqBody := map[string]interface{}{
"file": map[string]string{
"url": fileURL,
},
"config": map[string]interface{}{
"schema": schema,
"baseProcessor": "extraction_performance",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]bool{
"enabled": true,
},
"advancedMultimodalEnabled": true,
},
},
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", extendAPIBase+"/v1/extract-runs", bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var extractResp ExtractRunResponse
if err := json.Unmarshal(respBytes, &extractResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// For this example, we assume the API returns PROCESSED status.
// In production, implement proper polling with exponential backoff.
if extractResp.Status != "PROCESSED" {
return nil, fmt.Errorf("extract step failed with status: %s", extractResp.Status)
}
return &extractResp, nil
}
// formatCurrency formats a currency value for display
func formatCurrency(c *Currency) string {
if c == nil || c.Amount == nil {
return "N/A"
}
code := ""
if c.ISO4217CurrencyCode != nil {
code = " " + *c.ISO4217CurrencyCode
}
return fmt.Sprintf("$%.2f%s", *c.Amount, code)
}
// ProcessRealEstateClosingForm processes a real estate closing form
func ProcessRealEstateClosingForm(filePath string) (*RealEstateClosingData, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
// Read and encode file as data URL
fileBytes, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
base64Data := base64.StdEncoding.EncodeToString(fileBytes)
mimeType := "image/jpeg"
if strings.HasSuffix(strings.ToLower(filePath), ".pdf") {
mimeType = "application/pdf"
}
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
fmt.Printf("Processing Real Estate Closing Form: %s\n", filepath.Base(filePath))
fmt.Println("Step 1: Parsing document with agentic OCR...")
// Step 1: Parse the closing form to markdown
parseRun, err := createAndPollParseRun(apiKey, dataURL)
if err != nil {
return nil, err
}
markdown := ""
for _, chunk := range parseRun.Output.Chunks {
if markdown != "" {
markdown += "\n\n"
}
markdown += chunk.Content
}
fmt.Printf("✓ Parsed document (%d characters)\n", len(markdown))
fmt.Println("Step 2: Extracting structured fields...")
// Step 2: Extract all closing form fields
extractRun, err := createAndPollExtractRun(apiKey, dataURL)
if err != nil {
return nil, err
}
extractedData := &extractRun.Output.Value
fmt.Println("✓ Extraction complete")
fmt.Println("\n--- EXTRACTED REAL ESTATE CLOSING DATA ---\n")
if extractedData.BuyerName != nil {
fmt.Printf("Buyer: %s\n", *extractedData.BuyerName)
} else {
fmt.Println("Buyer: N/A")
}
if extractedData.SellerName != nil {
fmt.Printf("Seller: %s\n", *extractedData.SellerName)
} else {
fmt.Println("Seller: N/A")
}
if extractedData.PropertyAddress != nil {
fmt.Printf("Property: %s\n", *extractedData.PropertyAddress)
} else {
fmt.Println("Property: N/A")
}
if extractedData.ClosingDate != nil {
fmt.Printf("Closing Date: %s\n", *extractedData.ClosingDate)
} else {
fmt.Println("Closing Date: N/A")
}
if extractedData.SettlementAgentName != nil {
fmt.Printf("Settlement Agent: %s\n", *extractedData.SettlementAgentName)
} else {
fmt.Println("Settlement Agent: N/A")
}
fmt.Printf("Sale Price: %s\n", formatCurrency(extractedData.SalePrice))
fmt.Printf("Loan Amount: %s\n", formatCurrency(extractedData.LoanAmount))
fmt.Printf("Total Buyer Closing Costs: %s\n", formatCurrency(extractedData.TotalBuyerClosingCosts))
fmt.Printf("Total Seller Closing Costs: %s\n", formatCurrency(extractedData.TotalSellerClosingCosts))
fmt.Printf("Cash to Close: %s\n", formatCurrency(extractedData.CashToClose))
fmt.Printf("Net Proceeds to Seller: %s\n", formatCurrency(extractedData.NetProceedsToSeller))
// Display line items grouped by section
if len(extractedData.LineItems) > 0 {
fmt.Println("\n--- LINE ITEMS ---")
groupedBySection := make(map[string][]LineItem)
for _, item := range extractedData.LineItems {
section := "Other"
if item.Section != nil {
section = *item.Section
}
groupedBySection[section] = append(groupedBySection[section], item)
}
// Sort sections for consistent output
var sections []string
for s := range groupedBySection {
sections = append(sections, s)
}
sort.Strings(sections)
for _, section := range sections {
fmt.Printf("\n%s:\n", section)
for _, item := range groupedBySection[section] {
desc := "N/A"
if item.Description != nil {
desc = *item.Description
}
payee := "N/A"
if item.Payee != nil {
payee = *item.Payee
}
fmt.Printf(" - %s (Payee: %s)\n", desc, payee)
if item.AmountBuyer != nil {
fmt.Printf(" Buyer: $%.2f\n", *item.AmountBuyer)
}
if item.AmountSeller != nil {
fmt.Printf(" Seller: $%.2f\n", *item.AmountSeller)
}
if item.Notes != nil {
fmt.Printf(" Notes: %s\n", *item.Notes)
}
}
}
}
fmt.Println("\n--- RAW EXTRACTED JSON ---")
jsonBytes, _ := json.MarshalIndent(extractedData, "", " ")
fmt.Println(string(jsonBytes))
return extractedData, nil
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <path-to-closing-form>\n", os.Args[0])
os.Exit(1)
}
filePath := os.Args[1]
_, err := ProcessRealEstateClosingForm(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}// Deploy the "Real Estate Closing Form" 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/real-estate-closing-statement.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: real-estate-closing-statement).
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, "real-estate-closing-statement.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": "Real Estate Closing Form 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",
"required": [
"buyer_name",
"line_items",
"sale_price",
"loan_amount",
"seller_name",
"closing_date",
"cash_to_close",
"property_address",
"settlement_agent_name",
"net_proceeds_to_seller",
"total_buyer_closing_costs",
"total_seller_closing_costs"
],
"properties": {
"buyer_name": {
"type": [
"string",
"null"
],
"description": "The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"notes",
"payee",
"section",
"description",
"amount_buyer",
"amount_seller"
],
"properties": {
"notes": {
"type": [
"string",
"null"
],
"description": "Any additional notes or clarifications about this line item, such as calculation details, special conditions, or references to other documents."
},
"payee": {
"type": [
"string",
"null"
],
"description": "The party or entity receiving payment for this line item, if specified. May be a company, government agency, or individual."
},
"section": {
"type": [
"string",
"null"
],
"description": "The general category or section this line item belongs to, such as 'Loan Charges', 'Title Charges', 'Government Fees', 'Prepaids', 'Escrow', 'Payoffs', or similar. Helps group related items."
},
"description": {
"type": [
"string",
"null"
],
"description": "A clear description of the specific charge, credit, or adjustment. May include fee names, recipient, or other identifying details. Examples: 'Appraisal Fee', 'Title Insurance', 'Property Taxes', 'HOA Dues', 'Commission', etc."
},
"amount_buyer": {
"type": [
"number",
"null"
],
"description": "The amount of this line item that is charged to or credited to the buyer. May be positive (charge) or negative (credit)."
},
"amount_seller": {
"type": [
"number",
"null"
],
"description": "The amount of this line item that is charged to or credited to the seller. May be positive (charge) or negative (credit)."
}
},
"additionalProperties": false
},
"description": "The detailed list of all charges, credits, and adjustments that make up the closing statement. Each item represents a specific fee, tax, deposit, payoff, or other transaction component. May be organized by section (e.g., loan charges, title charges, government fees, prepaid items, escrow deposits, payoffs, etc.)."
},
"sale_price": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The agreed-upon purchase price for the property. This is the total consideration paid by the buyer to the seller, before adjustments. May be labeled as 'Sale Price', 'Purchase Price', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"loan_amount": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The principal amount of any new loan or mortgage being obtained as part of this transaction. May be labeled as 'Loan Amount', 'New Loan', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"seller_name": {
"type": [
"string",
"null"
],
"description": "The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar."
},
"closing_date": {
"type": [
"string",
"null"
],
"description": "The official date on which the real estate transaction is finalized and ownership is transferred. May be labeled as 'Closing Date', 'Settlement Date', or similar. This is a key milestone in the transaction.",
"extend:type": "date"
},
"cash_to_close": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The final amount the buyer must bring to closing, after accounting for all credits, deposits, and adjustments. May be labeled as 'Cash to Close', 'Amount Due from Buyer', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"property_address": {
"type": [
"string",
"null"
],
"description": "The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code. May be labeled as 'Property Address', 'Subject Property', or similar."
},
"settlement_agent_name": {
"type": [
"string",
"null"
],
"description": "The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process. May be labeled as 'Settlement Agent', 'Escrow Officer', or similar."
},
"net_proceeds_to_seller": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The final amount the seller receives after all deductions, payoffs, and adjustments. May be labeled as 'Net Proceeds', 'Amount to Seller', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"total_buyer_closing_costs": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The total of all closing costs and fees that the buyer is responsible for paying at settlement. Includes lender fees, title charges, taxes, insurance, and other expenses. May be labeled as 'Total Buyer Closing Costs', 'Buyer Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"total_seller_closing_costs": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The total of all closing costs and fees that the seller is responsible for paying at settlement. Includes commissions, title charges, taxes, and other expenses. May be labeled as 'Total Seller Closing Costs', 'Seller Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": false
}
},
"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 Any, Optional
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.")
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "real-estate-closing-statement.json"
state: dict[str, Optional[str]] = {}
if STATE_FILE.exists():
state = json.loads(STATE_FILE.read_text())
def save_state() -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
WORKFLOW = {
"name": "Real Estate Closing Form 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",
"required": [
"buyer_name",
"line_items",
"sale_price",
"loan_amount",
"seller_name",
"closing_date",
"cash_to_close",
"property_address",
"settlement_agent_name",
"net_proceeds_to_seller",
"total_buyer_closing_costs",
"total_seller_closing_costs",
],
"properties": {
"buyer_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar.",
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"notes",
"payee",
"section",
"description",
"amount_buyer",
"amount_seller",
],
"properties": {
"notes": {
"type": ["string", "null"],
"description": "Any additional notes or clarifications about this line item, such as calculation details, special conditions, or references to other documents.",
},
"payee": {
"type": ["string", "null"],
"description": "The party or entity receiving payment for this line item, if specified. May be a company, government agency, or individual.",
},
"section": {
"type": ["string", "null"],
"description": "The general category or section this line item belongs to, such as 'Loan Charges', 'Title Charges', 'Government Fees', 'Prepaids', 'Escrow', 'Payoffs', or similar. Helps group related items.",
},
"description": {
"type": ["string", "null"],
"description": "A clear description of the specific charge, credit, or adjustment. May include fee names, recipient, or other identifying details. Examples: 'Appraisal Fee', 'Title Insurance', 'Property Taxes', 'HOA Dues', 'Commission', etc.",
},
"amount_buyer": {
"type": ["number", "null"],
"description": "The amount of this line item that is charged to or credited to the buyer. May be positive (charge) or negative (credit).",
},
"amount_seller": {
"type": ["number", "null"],
"description": "The amount of this line item that is charged to or credited to the seller. May be positive (charge) or negative (credit).",
},
},
"additionalProperties": False,
},
"description": "The detailed list of all charges, credits, and adjustments that make up the closing statement. Each item represents a specific fee, tax, deposit, payoff, or other transaction component. May be organized by section (e.g., loan charges, title charges, government fees, prepaid items, escrow deposits, payoffs, etc.).",
},
"sale_price": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The agreed-upon purchase price for the property. This is the total consideration paid by the buyer to the seller, before adjustments. May be labeled as 'Sale Price', 'Purchase Price', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
"loan_amount": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The principal amount of any new loan or mortgage being obtained as part of this transaction. May be labeled as 'Loan Amount', 'New Loan', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
"seller_name": {
"type": ["string", "null"],
"description": "The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar.",
},
"closing_date": {
"type": ["string", "null"],
"description": "The official date on which the real estate transaction is finalized and ownership is transferred. May be labeled as 'Closing Date', 'Settlement Date', or similar. This is a key milestone in the transaction.",
"extend:type": "date",
},
"cash_to_close": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The final amount the buyer must bring to closing, after accounting for all credits, deposits, and adjustments. May be labeled as 'Cash to Close', 'Amount Due from Buyer', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
"property_address": {
"type": ["string", "null"],
"description": "The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code. May be labeled as 'Property Address', 'Subject Property', or similar.",
},
"settlement_agent_name": {
"type": ["string", "null"],
"description": "The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process. May be labeled as 'Settlement Agent', 'Escrow Officer', or similar.",
},
"net_proceeds_to_seller": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The final amount the seller receives after all deductions, payoffs, and adjustments. May be labeled as 'Net Proceeds', 'Amount to Seller', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
"total_buyer_closing_costs": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The total of all closing costs and fees that the buyer is responsible for paying at settlement. Includes lender fees, title charges, taxes, insurance, and other expenses. May be labeled as 'Total Buyer Closing Costs', 'Buyer Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
"total_seller_closing_costs": {
"type": "object",
"required": ["amount", "iso_4217_currency_code"],
"properties": {
"amount": {"type": ["number", "null"]},
"iso_4217_currency_code": {"type": ["string", "null"]},
},
"description": "The total of all closing costs and fees that the seller is responsible for paying at settlement. Includes commissions, title charges, taxes, and other expenses. May be labeled as 'Total Seller Closing Costs', 'Seller Settlement Charges', or similar.",
"extend:type": "currency",
"additionalProperties": False,
},
},
"additionalProperties": False,
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {"enabled": True},
"advancedMultimodalEnabled": True,
},
}
},
},
],
}
async def main() -> None:
client = Extend(token=API_KEY)
print(f'Deploying "{WORKFLOW["name"]}…')
if state.get("workflowId"):
workflow_id = state["workflowId"]
print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
await client.workflows.update(
id=workflow_id, body={"steps": WORKFLOW["steps"]}
)
else:
existing_id: Optional[str] = None
try:
workflows = await client.workflows.list(name=WORKFLOW["name"])
items = workflows.data if hasattr(workflows, "data") else []
for item in items:
if item.name == WORKFLOW["name"]:
existing_id = item.id
break
if existing_id:
state["workflowId"] = existing_id
save_state()
print(
f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps'
)
await client.workflows.update(
id=existing_id, body={"steps": WORKFLOW["steps"]}
)
except Exception:
pass
if not existing_id:
created = await client.workflows.create(body=WORKFLOW)
workflow_id = created.id
if not workflow_id:
raise RuntimeError("Could not read created workflow id from response")
state["workflowId"] = workflow_id
save_state()
print(f"+ created workflow ({workflow_id})")
workflow_id = state["workflowId"]
try:
await client.workflows.create_version(id=workflow_id, body={})
except Exception:
pass
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"Error: {str(e)}", file=sys.stderr)
sys.exit(1)// Uses Extend REST API directly (no official Java SDK exists yet).
// Deploy the "Real Estate Closing Form" pipeline to YOUR Extend account.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// javac Provision.java && java Provision
//
// Generated by doc1 (template: real-estate-closing-statement).
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
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("real-estate-closing-statement.json");
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
private static final HttpClient HTTP = HttpClient.newHttpClient();
private static Map<String, Object> state = new LinkedHashMap<>();
static {
try {
if (Files.exists(STATE_FILE)) {
String content = Files.readString(STATE_FILE);
state = parseJson(content);
}
} catch (IOException e) {
// If state file doesn't exist or can't be read, start with empty state
state = new LinkedHashMap<>();
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
String json = toJson(state);
Files.writeString(STATE_FILE, json);
}
private static Map<String, Object> api(String method, String pathName, Map<String, Object> body)
throws IOException, InterruptedException {
String url = API + pathName;
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(URI.create(url))
.method(method, body != null
? HttpRequest.BodyPublishers.ofString(toJson(body))
: HttpRequest.BodyPublishers.noBody())
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
reqBuilder.header("Content-Type", "application/json");
}
HttpRequest req = reqBuilder.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = new LinkedHashMap<>();
if (!res.body().isEmpty()) {
try {
data = parseJson(res.body());
} catch (Exception e) {
// If response is not valid JSON, return empty map
}
}
if (res.statusCode() < 200 || res.statusCode() >= 300) {
String errorMsg = toJson(data);
if (errorMsg.length() > 300) {
errorMsg = errorMsg.substring(0, 300);
}
throw new IOException(method + " " + pathName + " failed (" + res.statusCode() + "): " + errorMsg);
}
return data;
}
private static String toJson(Object obj) {
if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof Boolean) return obj.toString();
if (obj == null) return "null";
if (obj instanceof Map) {
Map<String, Object> map = (Map<String, Object>) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
sb.append(toJson(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
if (obj instanceof List) {
List<Object> list = (List<Object>) obj;
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append(toJson(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return "null";
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseJson(String json) throws IOException {
json = json.trim();
if (!json.startsWith("{")) throw new IOException("Expected JSON object");
Map<String, Object> result = new LinkedHashMap<>();
int depth = 0;
int i = 1;
String key = null;
while (i < json.length()) {
char c = json.charAt(i);
if (c == '"') {
int end = i + 1;
while (end < json.length()) {
if (json.charAt(end) == '"' && json.charAt(end - 1) != '\\') break;
end++;
}
String str = json.substring(i + 1, end);
i = end + 1;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i < json.length() && json.charAt(i) == ':') {
key = str;
i++;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
} else {
if (key != null) result.put(key, str);
}
} else if (c == '{' || c == '[') {
depth++;
i++;
} else if (c == '}' || c == ']') {
depth--;
i++;
} else if (c == ':' || c == ',') {
i++;
} else if (Character.isDigit(c) || c == '-') {
int end = i;
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '.')) end++;
String num = json.substring(i, end);
if (key != null) result.put(key, Double.parseDouble(num));
i = end;
} else if (json.startsWith("true", i)) {
if (key != null) result.put(key, true);
i += 4;
} else if (json.startsWith("false", i)) {
if (key != null) result.put(key, false);
i += 5;
} else if (json.startsWith("null", i)) {
if (key != null) result.put(key, null);
i += 4;
} else {
i++;
}
}
return result;
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Real Estate Closing Form Processing Pipeline");
List<Map<String, Object>> steps = new ArrayList<>();
// startTrigger1
Map<String, Object> startTrigger = new LinkedHashMap<>();
startTrigger.put("name", "startTrigger1");
startTrigger.put("type", "TRIGGER");
List<Map<String, Object>> triggerNext = new ArrayList<>();
Map<String, Object> triggerNextStep = new LinkedHashMap<>();
triggerNextStep.put("step", "parse1");
triggerNext.add(triggerNextStep);
startTrigger.put("next", triggerNext);
steps.add(startTrigger);
// parse1
Map<String, Object> parse = new LinkedHashMap<>();
parse.put("name", "parse1");
parse.put("type", "PARSE");
Map<String, Object> parseConfig = new LinkedHashMap<>();
Map<String, Object> blockOptions = new LinkedHashMap<>();
Map<String, Object> textBlock = new LinkedHashMap<>();
Map<String, Object> agentic = new LinkedHashMap<>();
agentic.put("enabled", true);
textBlock.put("agentic", agentic);
blockOptions.put("text", textBlock);
parseConfig.put("blockOptions", blockOptions);
Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
chunkingStrategy.put("type", "document");
parseConfig.put("chunkingStrategy", chunkingStrategy);
Map<String, Object> parseConfigWrapper = new LinkedHashMap<>();
parseConfigWrapper.put("parseConfig", parseConfig);
parse.put("config", parseConfigWrapper);
List<Map<String, Object>> parseNext = new ArrayList<>();
Map<String, Object> parseNextStep = new LinkedHashMap<>();
parseNextStep.put("step", "extraction2");
parseNext.add(parseNextStep);
parse.put("next", parseNext);
steps.add(parse);
// extraction2
Map<String, Object> extraction = new LinkedHashMap<>();
extraction.put("name", "extraction2");
extraction.put("type", "EXTRACT");
Map<String, Object> extractorConfigWrapper = new LinkedHashMap<>();
Map<String, Object> extractorConfig = new LinkedHashMap<>();
extractorConfig.put("schema", buildSchema());
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);
extractorConfigWrapper.put("extractorConfig", extractorConfig);
extraction.put("config", extractorConfigWrapper);
steps.add(extraction);
workflow.put("steps", steps);
return workflow;
}
private static Map<String, Object> buildSchema() {
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
List<String> required = Arrays.asList(
"buyer_name", "line_items", "sale_price", "loan_amount", "seller_name",
"closing_date", "cash_to_close", "property_address", "settlement_agent_name",
"net_proceeds_to_seller", "total_buyer_closing_costs", "total_seller_closing_costs"
);
schema.put("required", required);
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("buyer_name", buildStringProperty("The full legal name(s) of the individual(s) or entity purchasing the property. May be labeled as 'Buyer', 'Purchaser', or similar."));
properties.put("line_items", buildLineItemsProperty());
properties.put("sale_price", buildCurrencyProperty("The agreed-upon purchase price for the property. This is the total consideration paid by the buyer to the seller, before adjustments. May be labeled as 'Sale Price', 'Purchase Price', or similar."));
properties.put("loan_amount", buildCurrencyProperty("The principal amount of any new loan or mortgage being obtained as part of this transaction. May be labeled as 'Loan Amount', 'New Loan', or similar."));
properties.put("seller_name", buildStringProperty("The full legal name(s) of the individual(s) or entity selling the property. May be labeled as 'Seller', 'Owner', or similar."));
properties.put("closing_date", buildDateProperty("The official date on which the real estate transaction is finalized and ownership is transferred. May be labeled as 'Closing Date', 'Settlement Date', or similar. This is a key milestone in the transaction."));
properties.put("cash_to_close", buildCurrencyProperty("The final amount the buyer must bring to closing, after accounting for all credits, deposits, and adjustments. May be labeled as 'Cash to Close', 'Amount Due from Buyer', or similar."));
properties.put("property_address", buildStringProperty("The full address of the property being bought or sold in this transaction. Should include street address, city, state, and postal code. May be labeled as 'Property Address', 'Subject Property', or similar."));
properties.put("settlement_agent_name", buildStringProperty("The name of the settlement agent, escrow officer, or closing attorney responsible for managing the closing process. May be labeled as 'Settlement Agent', 'Escrow Officer', or similar."));
properties.put("net_proceeds_to_seller", buildCurrencyProperty("The final amount the seller receives after all deductions, payoffs, and adjustments. May be labeled as 'Net Proceeds', 'Amount to Seller', or similar."));
properties.put("total_buyer_closing_costs", buildCurrencyProperty("The total of all closing costs and fees that the buyer is responsible for paying at settlement. Includes lender fees, title charges, taxes, insurance, and other expenses. May be labeled as 'Total Buyer Closing Costs', 'Buyer Settlement Charges', or similar."));
properties.put("total_seller_closing_costs", buildCurrencyProperty("The total of all closing costs and fees that the seller is responsible for paying at settlement. Includes commissions, title charges, taxes, and other expenses. May be labeled as 'Total Seller Closing Costs', 'Seller Settlement Charges', or similar."));
schema.put("properties", properties);
schema.put("additionalProperties", false);
return schema;
}
private static Map<String, Object> buildStringProperty(String description) {
Map<String, Object> prop = new LinkedHashMap<>();
List<String> types = Arrays.asList("string", "null");
prop.put("type", types);
prop.put("description", description);
return prop;
}
private static Map<String, Object> buildDateProperty(String description) {
Map<String, Object> prop = new LinkedHashMap<>();
List<String> types = Arrays.asList("string", "null");
prop.put("type", types);
prop.put("description", description);
prop.put("extend:type", "date");
return prop;
}
private static Map<String, Object> buildCurrencyProperty(String description) {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", "object");
prop.put("required", Arrays.asList("amount", "iso_4217_currency_code"));
Map<String, Object> currencyProps = new LinkedHashMap<>();
currencyProps.put("amount", buildNumberProperty());
currencyProps.put("iso_4217_currency_code", buildStringTypeProperty());
prop.put("properties", currencyProps);
prop.put("description", description);
prop.put("extend:type", "currency");
prop.put("additionalProperties", false);
return prop;
}
private static Map<String, Object> buildNumberProperty() {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", Arrays.asList("number", "null"));
return prop;
}
private static Map<String, Object> buildStringTypeProperty() {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", Arrays.asList("string", "null"));
return prop;
}
private static Map<String, Object> buildLineItemsProperty() {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", "array");
Map<String, Object> itemSchema = new LinkedHashMap<>();
itemSchema.put("type", "object");
itemSchema.put("required", Arrays.asList("notes", "payee", "section", "description", "amount_buyer", "amount_seller"));
Map<String, Object> itemProps = new LinkedHashMap<>();
itemProps.put("notes", buildStringProperty("Any additional notes or clarifications about this line item, such as calculation details, special conditions, or references to other documents."));
itemProps.put("payee", buildStringProperty("The party or entity receiving payment for this line item, if specified. May be a company, government agency, or individual."));
itemProps.put("section", buildStringProperty("The general category or section this line item belongs to, such as 'Loan Charges', 'Title Charges', 'Government Fees', 'Prepaids', 'Escrow', 'Payoffs', or similar. Helps group related items."));
itemProps.put("description", buildStringProperty("A clear description of the specific charge, credit, or adjustment. May include fee names, recipient, or other identifying details. Examples: 'Appraisal Fee', 'Title Insurance', 'Property Taxes', 'HOA Dues', 'Commission', etc."));
itemProps.put("amount_buyer", buildNumberProperty());
itemProps.put("amount_seller", buildNumberProperty());
itemSchema.put("properties", itemProps);
itemSchema.put("additionalProperties", false);
prop.put("items", itemSchema);
prop.put("description", "The detailed list of all charges, credits, and adjustments that make up the closing statement. Each item represents a specific fee, tax, deposit, payoff, or other transaction component. May be organized by section (e.g., loan charges, title charges, government fees, prepaid items, escrow deposits, payoffs, etc.).");
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");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + workflowId, updateBody);
} else {
// Try to find existing workflow with same name
try {
String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
Map<String, Object> listResult = api("GET", "/workflows?name=" + encodedName, null);
List<Map<String, Object>> items = new ArrayList<>();
if (listResult.containsKey("data")) {
items = (List<Map<String, Object>>) listResult.get("data");
} else if (listResult.containsKey("items")) {
items = (List<Map<String, Object>>) listResult.get("items");
}
for (Map<String, Object> item : items) {
if (workflowName.equals(item.get("name"))) {
workflowId = (String) item.get("id");
if (workflowId != null && !workflowId.isEmpty()) {
state.put("workflowId", workflowId);
saveState();
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + workflowId + ") — updating steps");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + workflowId, updateBody);
break;
}
}
}
} catch (Exception e) {
// Lookup is best-effort; fall through to create
}
if (workflowId == null || workflowId.isEmpty()) {
Map<String, Object> created = api("POST", "/workflows", workflow);
workflowId = (String) created.get("id");
if (workflowId == null) {
Map<String, Object> workflowObj = (Map<String, Object>) created.get("workflow");
if (workflowObj != null) {
workflowId = (String) workflowObj.get("id");
}
}
if (workflowId == null || workflowId.isEmpty()) {
throw new IOException("Could not read created workflow id from response");
}
state.put("workflowId", workflowId);
saveState();
System.out.println("+ created workflow (" + workflowId + ")");
}
}
// Deploy the current draft as a new version
try {
api("POST", "/workflows/" + workflowId + "/versions", new LinkedHashMap<>());
} catch (Exception e) {
// Best-effort: some accounts/plans may not require this
}
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);
}
}
}package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It makes HTTP calls to https://api.extend.ai with Bearer token authentication.
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
type LineItemProperty struct {
Type interface{} `json:"type"`
Items interface{} `json:"items,omitempty"`
Enum []string `json:"enum,omitempty"`
}
type Schema struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties map[string]interface{} `json:"properties"`
AdditionalProperties bool `json:"additionalProperties"`
}
type ExtractorConfig struct {
Schema Schema `json:"schema"`
BaseProcessor string `json:"baseProcessor"`
AdvancedOptions map[string]interface{} `json:"advancedOptions"`
}
type ParseConfig struct {
BlockOptions map[string]interface{} `json:"blockOptions"`
ChunkingStrategy map[string]interface{} `json:"chunkingStrategy"`
}
type StepConfig struct {
ParseConfig *ParseConfig `json:"parseConfig,omitempty"`
ExtractorConfig *ExtractorConfig `json:"extractorConfig,omitempty"`
}
type NextStep struct {
Step string `json:"step"`
}
type Step struct {
Name string `json:"name"`
Type string `json:"type"`
Config *StepConfig `json:"config,omitempty"`
Next []NextStep `json:"next,omitempty"`
}
type Workflow struct {
Name string `json:"name"`
Steps []Step `json:"steps"`
}
type WorkflowListItem struct {
ID string `json:"id"`
Name string `json:"name"`
}
type WorkflowListResponse struct {
Data []WorkflowListItem `json:"data,omitempty"`
Items []WorkflowListItem `json:"items,omitempty"`
}
type WorkflowCreateResponse struct {
ID string `json:"id,omitempty"`
Workflow struct {
ID string `json:"id,omitempty"`
} `json:"workflow,omitempty"`
}
func getAPIKey() string {
key := os.Getenv("EXTEND_API_KEY")
if key == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
return key
}
func getStateFilePath() string {
stateDir := filepath.Join(os.Getenv("HOME"), ".extend")
if wd, err := os.Getwd(); err == nil {
stateDir = filepath.Join(wd, ".extend")
}
return filepath.Join(stateDir, "real-estate-closing-statement.json")
}
func loadState(filePath string) State {
var state State
data, err := os.ReadFile(filePath)
if err == nil {
json.Unmarshal(data, &state)
}
return state
}
func saveState(filePath string, state State) error {
stateDir := filepath.Dir(filePath)
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, _ := json.MarshalIndent(state, "", " ")
return os.WriteFile(filePath, data, 0644)
}
func apiCall(method, pathName string, body interface{}, apiKey string) (map[string]interface{}, error) {
url := API + pathName
var bodyReader io.Reader
if body != nil {
bodyBytes, _ := json.Marshal(body)
bodyReader = io.NopCloser(
nil, // Will be set below
)
bodyReader = io.NopCloser(nil)
bodyBytes, _ := json.Marshal(body)
bodyReader = io.NopCloser(nil)
bodyBytes, _ := json.Marshal(body)
bodyReader = nil
if bodyBytes != nil {
bodyReader = io.NopCloser(nil)
}
}
bodyBytes, _ := json.Marshal(body)
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
if body != nil {
bodyBytes, _ := json.Marshal(body)
req.Body = io.NopCloser(nil)
req.ContentLength = int64(len(bodyBytes))
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
bodyBytes, _ := json.Marshal(body)
req.Header.Set("Content-Type", "application/json")
req, _ := http.NewRequest(method, url, nil)
bodyBytes, _ := json.Marshal(body)
_ = req
_ = bodyBytes
}
req, _ = http.NewRequest(method, url, nil)
if body != nil {
bodyBytes, _ := json.Marshal(body)
bodyReader = io.NopCloser(nil)
_ = bodyBytes
}
bodyBytes, _ := json.Marshal(body)
if bodyBytes != nil {
req, _ = http.NewRequest(method, url, io.NopCloser(nil))
} else {
req, _ = http.NewRequest(method, url, nil)
}
req, _ = http.NewRequest(method, url, nil)
bodyData := []byte{}
if body != nil {
bodyData, _ = json.Marshal(body)
}
req, err = http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
if len(bodyData) > 0 {
req, err = http.NewRequest(method, url, io.NopCloser(nil))
if err != nil {
return nil, err
}
req.ContentLength = int64(len(bodyData))
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if len(bodyData) > 0 {
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()
respBody, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(respBody, &result)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, string(respBody[:min(300, len(respBody))]))
}
return result, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func buildWorkflow() Workflow {
schema := map[string]interface{}{
"type": "object",
"required": []string{
"buyer_name", "line_items", "sale_price", "loan_amount", "seller_name",
"closing_date", "cash_to_close", "property_address", "settlement_agent_name",
"net_proceeds_to_seller", "total_buyer_closing_costs", "total_seller_closing_costs",
},
"properties": map[string]interface{}{
"buyer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The full legal name(s) of the individual(s) or entity purchasing the property.",
},
"line_items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"required": []string{"notes", "payee", "section", "description", "amount_buyer", "amount_seller"},
"properties": map[string]interface{}{
"notes": map[string]interface{}{"type": []string{"string", "null"}},
"payee": map[string]interface{}{"type": []string{"string", "null"}},
"section": map[string]interface{}{"type": []string{"string", "null"}},
"description": map[string]interface{}{"type": []string{"string", "null"}},
"amount_buyer": map[string]interface{}{"type": []string{"number", "null"}},
"amount_seller": map[string]interface{}{"type": []string{"number", "null"}},
},
"additionalProperties": false,
},
},
"sale_price": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
"loan_amount": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
"seller_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The full legal name(s) of the individual(s) or entity selling the property.",
},
"closing_date": map[string]interface{}{
"type": []string{"string", "null"},
"extend:type": "date",
"description": "The official date on which the real estate transaction is finalized.",
},
"cash_to_close": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
"property_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The full address of the property being bought or sold.",
},
"settlement_agent_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the settlement agent, escrow officer, or closing attorney.",
},
"net_proceeds_to_seller": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
"total_buyer_closing_costs": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
"total_seller_closing_costs": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"extend:type": "currency",
"additionalProperties": false,
},
},
"additionalProperties": false,
}
return Workflow{
Name: "Real Estate Closing Form Processing Pipeline",
Steps: []Step{
{
Name: "startTrigger1",
Type: "TRIGGER",
Next: []NextStep{{Step: "parse1"}},
},
{
Name: "parse1",
Type: "PARSE",
Config: &StepConfig{
ParseConfig: &ParseConfig{
BlockOptions: map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{"enabled": true},
},
},
ChunkingStrategy: map[string]interface{}{
"type": "document",
},
},
},
Next: []NextStep{{Step: "extraction2"}},
},
{
Name: "extraction2",
Type: "EXTRACT",
Config: &StepConfig{
ExtractorConfig: &ExtractorConfig{
Schema: Schema{
Type: "object",
Required: schema["required"].([]string),
Properties: schema["properties"].(map[string]interface{}),
AdditionalProperties: false,
},
BaseProcessor: "extraction_performance",
AdvancedOptions: map[string]interface{}{
"reviewAgent": map[string]interface{}{"enabled": true},
"advancedMultimodalEnabled": true,
},
},
},
},
},
}
}
func main() {
flag.Parse()
apiKey := getAPIKey()
stateFile := getStateFilePath()
state := loadState(stateFile)
workflow := buildWorkflow()
fmt.Printf("Deploying \"%s\"…\n", workflow.Name)
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID),
map[string]interface{}{"steps": workflow.Steps}, apiKey)
} else {
// Try to find existing workflow with same name
found := false
if result, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", workflow.Name), nil, apiKey); err == nil {
var items []WorkflowListItem
if data, ok := result["data"].([]interface{}); ok {
for _, item := range data {
if itemMap, ok := item.(map[string]interface{}); ok {
if id, ok := itemMap["id"].(string); ok {
if name, ok := itemMap["name"].(string); ok && name == workflow.Name {
state.WorkflowID = id
saveState(stateFile, state)
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
apiCall("POST", fmt.Sprintf("/workflows/%s", id),
map[string]interface{}{"steps": workflow.Steps}, apiKey)
found = true
break
}
}
}
}
}
}
if !found {
result, err := apiCall("POST", "/workflows", workflow, apiKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating workflow: %v\n", err)
os.Exit(1)
}
wfID := ""
if id, ok := result["id"].(string); ok && id != "" {
wfID = id
} else if wfObj, ok := result["workflow"].(map[string]interface{}); ok {
if id, ok := wfObj["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(stateFile, state)
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version (best-effort)
apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID),
map[string]interface{}{}, apiKey)
fmt.Println("\nDone. Run documents through it with:")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}This Real Estate Closing Form template captures essential transaction data including property address, buyer/seller information, closing date, and a detailed financial settlement table. It tracks credits, debits, taxes, fees, and commission to calculate the final cash due at closing.