Financial & BankingParse → Extract

Wire Transfer Instructions Extractor

Extracts wire transfer procedures, contact info, and operational hours from banking guides.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document195991291-Wells-fargo-Wire-Transfer-Guide.pdf

Step-by-step

A wire transfer quick reference guide is an instructional document provided by a financial institution that contains operational hours, phone-based activation procedures, required customer information, and daily cutoff times to facilitate wire transfer initiation and processing. This template takes in Wire Transfer Quick Reference Guide and outputs markdown (.md) capturing the document's full text, layout, and operational procedures, and JSON (.json) with structured fields including contact information, departmental operating hours, required information checklist, and wire transfer deadlines per the extraction schema by using Extend's Parse, Extract primitives.

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

Parse

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

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

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

Step 2

Extract

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

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

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

Example code

{
  "name": "Wire Transfer Quick Reference Guide Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "organization": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Financial institution providing the wire transfer service"
              },
              "phone_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Toll-free phone number to initiate wire transfer services"
              },
              "document_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Title of the document"
              },
              "same_day_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for same-day wire initiation in Central Time"
              },
              "required_information": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "List of required information needed to initiate a wire transfer"
              },
              "time_zone_conversion": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Time zone conversion information from Central Time"
              },
              "customer_service_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "General customer service operating hours in Central Time"
              },
              "spanish_language_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Spanish language support operating hours in Central Time"
              },
              "future_dated_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for future-dated wire initiation in Central Time"
              },
              "wire_transfer_deadlines": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Daily cutoff times for wire transfer requests"
              },
              "book_internal_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for book/internal wire initiation in Central Time"
              },
              "foreign_exchange_contact": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Phone number for Foreign Exchange Specialist inquiries"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Wire Transfer Quick Reference Guide Processing — Extend AI Skill

## What this pipeline does

Parses a Wells Fargo wire transfer quick reference guide from PDF to markdown, then extracts 11 structured fields including phone numbers, operating hours for different wire types, required information checklist, and cutoff times. Output is a JSON object ready for customer-facing lookup systems or compliance verification.

## When to use this

- **Customer service systems**: Embed the extracted hours and phone number in a chatbot or IVR that answers "when can I wire money?"
- **Operations dashboards**: Monitor and display current wire service availability across same-day, book/internal, and future-dated transfer types.
- **Compliance & audit**: Automatically verify that published hours and requirements match internal policies.
- **Multilingual support**: Extract Spanish-language hours separately to route customers to the correct service desk.
- **Time-zone-aware scheduling**: Capture and convert Central Time cutoffs to customer's local time for accurate deadline warnings.

## Processor pipeline

| Step | Processor | Purpose | Key Config |
|------|-----------|---------|-----------|
| 1 | **Parse** | Convert PDF to markdown, preserving structure of hours, checklists, and phone numbers | `agentic_ocr: enabled`, `chunkingStrategy: document` |
| 2 | **Extract** | Pull 11 fields (title, org, phone, hours, deadlines, etc.) into JSON | `extraction_performance` with `reviewAgent: enabled`, `advancedMultimodalEnabled: true` |

**Why these choices:**
- `agentic_ocr` handles formatted tables (hours by wire type), hierarchical checklists, and reference-style layouts common in quick-reference guides.
- `document` chunking preserves context across related fields (e.g., "same-day hours" and "same-day phone").
- `extraction_performance` + review agent ensures accuracy on critical fields (phone numbers, cutoff times) where errors cause customer frustration.
- `advancedMultimodalEnabled` catches multi-column layouts and structured text that simpler extraction misses.

## TypeScript implementation

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

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

/**
 * Process a Wells Fargo wire transfer quick reference guide.
 * Step 1: Parse PDF to markdown (agentic_ocr for structured content).
 * Step 2: Extract 11 key fields (phone, hours, deadlines, requirements).
 */
async function processWireTransferQuickReferenceGuide(filePath: string) {
  // Convert local file to data URL for SDK
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/pdf;base64,${fileBuffer.toString("base64")}`;

  console.log(`[1/2] Parsing wire transfer guide from ${filePath}...`);
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      blockOptions: {
        text: {
          agentic: { enabled: true },
        },
      },
      chunkingStrategy: { type: "document" },
    },
  });

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

  const markdownContent = parseRun.output.chunks
    .map((chunk) => chunk.content)
    .join("\n\n");
  console.log(`[Parse] Extracted ${markdownContent.length} chars of markdown`);

  // Step 2: Extract structured fields using Zod schema.
  console.log(`[2/2] Extracting wire transfer fields...`);
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: z.object({
        document_title: z
          .string()
          .nullable()
          .describe("Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'"),
        organization: z
          .string()
          .nullable()
          .describe("Financial institution name, typically 'Wells Fargo' or similar"),
        phone_number: z
          .string()
          .nullable()
          .describe(
            "Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'"
          ),
        same_day_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for same-day wire initiation in Central Time, e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'"
          ),
        book_internal_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time"
          ),
        future_dated_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for future-dated wire (scheduled transfer) initiation in Central Time"
          ),
        customer_service_hours: z
          .string()
          .nullable()
          .describe(
            "General customer service/inquiry hours in Central Time, may differ from wire-specific hours"
          ),
        spanish_language_hours: z
          .string()
          .nullable()
          .describe(
            "Spanish language support operating hours in Central Time, if available separately"
          ),
        required_information: z
          .string()
          .nullable()
          .describe(
            "Comma-separated or bullet-list of required information to initiate a wire, e.g., 'recipient name, account number, routing number, amount, purpose'"
          ),
        foreign_exchange_contact: z
          .string()
          .nullable()
          .describe("Phone number or contact for Foreign Exchange Specialist inquiries"),
        time_zone_conversion: z
          .string()
          .nullable()
          .describe(
            "Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'"
          ),
        wire_transfer_deadlines: z
          .string()
          .nullable()
          .describe(
            "Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'"
          ),
      }),
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: { enabled: true },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const extracted = extractRun.output.value;
  console.log(`[Extract] Successfully extracted:`);
  console.log(JSON.stringify(extracted, null, 2));

  return {
    parsed_markdown: markdownContent,
    extracted_fields: extracted,
  };
}

// Main entry point for testing
async function main() {
  const filePath = process.argv[2] || "__FILE_PATH__";
  try {
    const result = await processWireTransferQuickReferenceGuide(filePath);
    console.log("\n✓ Pipeline complete");
    console.log("Output:", JSON.stringify(result, null, 2));
  } catch (error) {
    console.error("✗ Pipeline error:", error);
    process.exit(1);
  }
}

main();
```

---

## CLI equivalent

```bash
#!/bin/bash
# Wire Transfer Quick Reference Guide processing pipeline

export EXTEND_API_KEY="sk_..."
FILE="wire_transfer_guide.pdf"

# Step 1: Parse to markdown (with agentic OCR for structured content)
echo "[1/2] Parsing..."
extend parse "$FILE" \
  --block-options '{"text":{"agentic":{"enabled":true}}}' \
  --chunking-strategy document

# Step 2: Extract structured fields
echo "[2/2] Extracting..."
extend extract "$FILE" \
  --schema schema.json \
  --base-processor extraction_performance \
  --review-agent enabled \
  --advanced-multimodal enabled
```

**schema.json:**
```json
{
  "type": "object",
  "properties": {
    "document_title": { "type": ["string", "null"], "description": "Title of the document" },
    "organization": { "type": ["string", "null"], "description": "Financial institution name" },
    "phone_number": { "type": ["string", "null"], "description": "Toll-free phone number to initiate wire services" },
    "same_day_wire_hours": { "type": ["string", "null"], "description": "Operating hours for same-day wire in Central Time" },
    "book_internal_wire_hours": { "type": ["string", "null"], "description": "Operating hours for book/internal wire in Central Time" },
    "future_dated_wire_hours": { "type": ["string", "null"], "description": "Operating hours for future-dated wire in Central Time" },
    "customer_service_hours": { "type": ["string", "null"], "description": "General customer service hours in Central Time" },
    "spanish_language_hours": { "type": ["string", "null"], "description": "Spanish language support hours in Central Time" },
    "required_information": { "type": ["string", "null"], "description": "List of required information to initiate a wire transfer" },
    "foreign_exchange_contact": { "type": ["string", "null"], "description": "Phone number for Foreign Exchange Specialist inquiries" },
    "time_zone_conversion": { "type": ["string", "null"], "description": "Time zone conversion guidance from Central Time" },
    "wire_transfer_deadlines": { "type": ["string", "null"], "description": "Daily cutoff times for wire transfer requests" }
  }
}
```

---

## Schema

The extraction schema captures 11 fields designed to support real-time customer lookup and operational compliance:

```json
{
  "type": "object",
  "properties": {
    "document_title": {
      "type": ["string", "null"],
      "description": "Title of the document (e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'). Used to verify document type on ingestion."
    },
    "organization": {
      "type": ["string", "null"],
      "description": "Financial institution name (e.g., 'Wells Fargo'). Essential for multi-bank reference libraries."
    },
    "phone_number": {
      "type": ["string", "null"],
      "description": "Toll-free phone number to initiate wire transfer services, formatted as +1-800-XXX-XXXX or similar. CRITICAL: must be 100% accurate; route to human review if ambiguous."
    },
    "same_day_wire_hours": {
      "type": ["string", "null"],
      "description": "Operating hours for same-day wire initiation in Central Time (e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'). Used for deadline warnings in customer-facing systems."
    },
    "book_internal_wire_hours": {
      "type": ["string", "null"],
      "description": "Operating hours for book/internal wire (transfer between accounts at the same bank) in Central Time. Often longer than same-day wire window; critical for internal transfer scheduling."
    },
    "future_dated_wire_hours": {
      "type": ["string", "null"],
      "description": "Operating hours for future-dated wire (scheduled transfer for a future date) initiation in Central Time. May have different rules than same-day; important for advance-scheduling workflows."
    },
    "customer_service_hours": {
      "type": ["string", "null"],
      "description": "General customer service/inquiry hours in Central Time. May differ from wire-specific hours; used for routing help requests outside wire service windows."
    },
    "spanish_language_hours": {
      "type": ["string", "null"],
      "description": "Spanish language support operating hours in Central Time, if offered separately from English. Critical for bilingual customer routing."
    },
    "required_information": {
      "type": ["string", "null"],
      "description": "Comma-separated or formatted list of required information to initiate a wire (e.g., 'Recipient name, account number, routing number, amount, transfer purpose'). Accuracy directly impacts customer error rates; extract as complete list."
    },
    "foreign_exchange_contact": {
      "type": ["string", "null"],
      "description": "Phone number or contact method for Foreign Exchange Specialist inquiries (for international wires). May differ from domestic wire phone number."
    },
    "time_zone_conversion": {
      "type": ["string", "null"],
      "description": "Explicit time zone conversion guidance (e.g., 'CT is 2 hours ahead of PT'). Used by customer service to explain cutoff times to callers in non-CT zones."
    },
    "wire_transfer_deadlines": {
      "type": ["string", "null"],
      "description": "Daily cutoff times and deadline rules for wire transfer requests (e.g., 'Same-day wires must be initiated by 4:00 PM CT for same-day processing'). CRITICAL: errors here cause missed deadlines and customer complaints."
    }
  }
}
```

**Field accuracy ranking** (highest priority first):
1. `phone_number` — single source of truth; any error breaks the entire customer journey.
2. `same_day_wire_hours` and `wire_transfer_deadlines` — used in real-time cutoff warnings.
3. `required_information` — missing items cause customer form rejections.
4. `book_internal_wire_hours`, `future_dated_wire_hours` — distinct from same-day; misattribution causes routing errors.
5. `time_zone_conversion` — helps customer service explain times accurately.

---

## Accuracy tips

1. **Enforce strict phone number format** — Extract phone numbers with parentheses, dashes, and +1 prefix. Post-process to validate 10-digit US numbers (800, 888, 877 prefixes common for toll-free). Flag any deviation for human review.

2. **Disambiguate time zones explicitly** — Documents often list multiple wire types without repeating "Central Time" for each. Use the parser's markdown output to cross-reference section headers ("Central Time" context) and bind hours to the correct wire type. A mismatch (e.g., assigning PT hours to a CT section) breaks customer workflows.

3. **Capture time ranges precisely** — Extract both open and close times (e.g., "7:00 AM - 4:00 PM CT") as a single string, not split fields. Concatenating "7:00 AM" + "4:00 PM" separately causes timezone ambiguity in downstream systems.

4. **List required_information as a complete set** — These documents often present requirements as nested bullets or tables. Extract the full checklist as one string or array; partial extraction (missing "routing number," for example) causes customer errors and support escalations. Use `reviewAgent: enabled` to validate completeness.

5. **Validate cutoff time logic against wire type** — Deadlines vary: same-day wires have the tightest window, future-dated wires often allow later submission. Cross-check the extracted deadline against the wire type. If a "future-dated wire" deadline is *earlier* than a same-day wire deadline, flag for human review.

6. **Preserve source formatting for hours** — If the original document uses "7 AM–4 PM" (en dash), "7 AM - 4 PM" (hyphen), or "7:00 AM to 4:00 PM," preserve the exact format in extraction. Downstream systems may parse these patterns; normalizing too early loses fidelity.

7. **Check for holiday/special hours callouts** — Quick reference guides often append "(closed holidays)" or "extended hours on Friday." Ensure these qualifiers are included in the extracted hours string; a clean "7:00 AM - 4:00 PM CT" without the holiday note is technically incomplete.

8. **Cross-reference organization name with phone number** — Verify that the extracted org name (e.g., "Wells Fargo") matches the phone number prefix or known corporate helpline. A mismatch (e.g., "Wells Fargo" + "1-800-CHASE-123") signals OCR error.

9. **Handle multilingual sections separately** — If English and Spanish hours appear in different sections, extract `spanish_language_hours` independently. Do not merge or translate; let systems
import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";

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

/**
 * Process a Wells Fargo wire transfer quick reference guide.
 * Step 1: Parse PDF to markdown (agentic_ocr for structured content).
 * Step 2: Extract 11 key fields (phone, hours, deadlines, requirements).
 */
async function processWireTransferQuickReferenceGuide(filePath: string) {
  // Convert local file to data URL for SDK
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/pdf;base64,${fileBuffer.toString("base64")}`;

  console.log(`[1/2] Parsing wire transfer guide from ${filePath}...`);
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      blockOptions: {
        text: {
          agentic: { enabled: true },
        },
      },
      chunkingStrategy: { type: "document" },
    },
  });

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

  const markdownContent = parseRun.output.chunks
    .map((chunk) => chunk.content)
    .join("\n\n");
  console.log(`[Parse] Extracted ${markdownContent.length} chars of markdown`);

  // Step 2: Extract structured fields using Zod schema.
  console.log(`[2/2] Extracting wire transfer fields...`);
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: z.object({
        document_title: z
          .string()
          .nullable()
          .describe("Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'"),
        organization: z
          .string()
          .nullable()
          .describe("Financial institution name, typically 'Wells Fargo' or similar"),
        phone_number: z
          .string()
          .nullable()
          .describe(
            "Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'"
          ),
        same_day_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for same-day wire initiation in Central Time, e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'"
          ),
        book_internal_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time"
          ),
        future_dated_wire_hours: z
          .string()
          .nullable()
          .describe(
            "Operating hours for future-dated wire (scheduled transfer) initiation in Central Time"
          ),
        customer_service_hours: z
          .string()
          .nullable()
          .describe(
            "General customer service/inquiry hours in Central Time, may differ from wire-specific hours"
          ),
        spanish_language_hours: z
          .string()
          .nullable()
          .describe(
            "Spanish language support operating hours in Central Time, if available separately"
          ),
        required_information: z
          .string()
          .nullable()
          .describe(
            "Comma-separated or bullet-list of required information to initiate a wire, e.g., 'recipient name, account number, routing number, amount, purpose'"
          ),
        foreign_exchange_contact: z
          .string()
          .nullable()
          .describe("Phone number or contact for Foreign Exchange Specialist inquiries"),
        time_zone_conversion: z
          .string()
          .nullable()
          .describe(
            "Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'"
          ),
        wire_transfer_deadlines: z
          .string()
          .nullable()
          .describe(
            "Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'"
          ),
      }),
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: { enabled: true },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const extracted = extractRun.output.value;
  console.log(`[Extract] Successfully extracted:`);
  console.log(JSON.stringify(extracted, null, 2));

  return {
    parsed_markdown: markdownContent,
    extracted_fields: extracted,
  };
}

// Main entry point for testing
async function main() {
  const filePath = process.argv[2] || "__FILE_PATH__";
  try {
    const result = await processWireTransferQuickReferenceGuide(filePath);
    console.log("\n✓ Pipeline complete");
    console.log("Output:", JSON.stringify(result, null, 2));
  } catch (error) {
    console.error("✗ Pipeline error:", error);
    process.exit(1);
  }
}

main();
import os
import sys
import json
import base64
from typing import Optional
from extend_ai import Extend
from pydantic import BaseModel, Field

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


class WireTransferFields(BaseModel):
    document_title: Optional[str] = Field(
        None, description="Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'"
    )
    organization: Optional[str] = Field(
        None, description="Financial institution name, typically 'Wells Fargo' or similar"
    )
    phone_number: Optional[str] = Field(
        None, description="Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'"
    )
    same_day_wire_hours: Optional[str] = Field(
        None, description="Operating hours for same-day wire initiation in Central Time, e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'"
    )
    book_internal_wire_hours: Optional[str] = Field(
        None, description="Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time"
    )
    future_dated_wire_hours: Optional[str] = Field(
        None, description="Operating hours for future-dated wire (scheduled transfer) initiation in Central Time"
    )
    customer_service_hours: Optional[str] = Field(
        None, description="General customer service/inquiry hours in Central Time, may differ from wire-specific hours"
    )
    spanish_language_hours: Optional[str] = Field(
        None, description="Spanish language support operating hours in Central Time, if available separately"
    )
    required_information: Optional[str] = Field(
        None, description="Comma-separated or bullet-list of required information to initiate a wire, e.g., 'recipient name, account number, routing number, amount, purpose'"
    )
    foreign_exchange_contact: Optional[str] = Field(
        None, description="Phone number or contact for Foreign Exchange Specialist inquiries"
    )
    time_zone_conversion: Optional[str] = Field(
        None, description="Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'"
    )
    wire_transfer_deadlines: Optional[str] = Field(
        None, description="Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'"
    )


async def process_wire_transfer_quick_reference_guide(file_path: str):
    """
    Process a Wells Fargo wire transfer quick reference guide.
    Step 1: Parse PDF to markdown (agentic_ocr for structured content).
    Step 2: Extract 11 key fields (phone, hours, deadlines, requirements).
    """
    # Convert local file to data URL for SDK
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    data_url = f"data:application/pdf;base64,{base64.b64encode(file_buffer).decode('utf-8')}"

    print(f"[1/2] Parsing wire transfer guide from {file_path}...")
    parse_run = await 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 failed with status: {parse_run.status}")

    markdown_content = "\n\n".join([chunk.content for chunk in parse_run.output.chunks])
    print(f"[Parse] Extracted {len(markdown_content)} chars of markdown")

    # Step 2: Extract structured fields using Pydantic schema.
    print("[2/2] Extracting wire transfer fields...")
    extract_run = await client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={
            "schema": WireTransferFields.model_json_schema(),
            "base_processor": "extraction_performance",
            "advanced_options": {
                "review_agent": {"enabled": True},
                "advanced_multimodal_enabled": True,
            },
        },
    )

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

    extracted = extract_run.output.value
    print("[Extract] Successfully extracted:")
    print(json.dumps(extracted, indent=2))

    return {
        "parsed_markdown": markdown_content,
        "extracted_fields": extracted,
    }


async def main():
    file_path = sys.argv[1] if len(sys.argv) > 1 else "__FILE_PATH__"
    try:
        result = await process_wire_transfer_quick_reference_guide(file_path)
        print("\n✓ Pipeline complete")
        print("Output:", json.dumps(result, indent=2))
    except Exception as error:
        print(f"✗ Pipeline error: {error}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
// This code calls the Extend REST API directly (https://api.extend.ai) using only
// Java's built-in java.net.http.HttpClient, because Extend has no official Java SDK yet.
// It performs the exact same Parse → Extract pipeline as the TypeScript reference.

import java.io.*;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;

public class WireTransferQuickReferenceGuide {

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

  /**
   * Process a Wells Fargo wire transfer quick reference guide.
   * Step 1: Parse PDF to markdown (agentic_ocr for structured content).
   * Step 2: Extract 11 key fields (phone, hours, deadlines, requirements).
   */
  public static Map<String, Object> processWireTransferQuickReferenceGuide(String filePath)
      throws Exception {
    // Convert local file to base64 data URL
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64Content = Base64.getEncoder().encodeToString(fileBytes);
    String dataUrl = "data:application/pdf;base64," + base64Content;

    System.out.println("[1/2] Parsing wire transfer guide from " + filePath + "...");

    // Step 1: Create and poll parse run
    Map<String, Object> parseRequestBody = new LinkedHashMap<>();
    parseRequestBody.put("file", Map.of("url", dataUrl));
    parseRequestBody.put(
        "config",
        Map.of(
            "blockOptions",
            Map.of("text", Map.of("agentic", Map.of("enabled", true))),
            "chunkingStrategy",
            Map.of("type", "document")));

    String parseRequestJson = toJson(parseRequestBody);
    HttpRequest parseRequest =
        HttpRequest.newBuilder()
            .uri(java.net.URI.create(API_BASE + "/v1/parseRuns/createAndPoll"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(parseRequestJson))
            .build();

    HttpResponse<String> parseResponse =
        httpClient.send(parseRequest, HttpResponse.BodyHandlers.ofString());
    if (parseResponse.statusCode() != 200) {
      throw new RuntimeException("Parse request failed: " + parseResponse.body());
    }

    Map<String, Object> parseRun = parseJson(parseResponse.body());
    String parseStatus = (String) parseRun.get("status");
    if (!"PROCESSED".equals(parseStatus)) {
      throw new RuntimeException("Parse failed with status: " + parseStatus);
    }

    List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseRun.get("output");
    StringBuilder markdownContent = new StringBuilder();
    for (Object chunkObj : (List<?>) chunks) {
      Map<String, Object> chunk = (Map<String, Object>) chunkObj;
      String content = (String) chunk.get("content");
      if (content != null) {
        markdownContent.append(content).append("\n\n");
      }
    }

    System.out.println("[Parse] Extracted " + markdownContent.length() + " chars of markdown");

    // Step 2: Extract structured fields using schema
    System.out.println("[2/2] Extracting wire transfer fields...");

    Map<String, Object> schema =
        Map.of(
            "type",
            "object",
            "properties",
            Map.ofEntries(
                Map.entry(
                    "document_title",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'")),
                Map.entry(
                    "organization",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Financial institution name, typically 'Wells Fargo' or similar")),
                Map.entry(
                    "phone_number",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'")),
                Map.entry(
                    "same_day_wire_hours",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Operating hours for same-day wire initiation in Central Time, e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'")),
                Map.entry(
                    "book_internal_wire_hours",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time")),
                Map.entry(
                    "future_dated_wire_hours",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Operating hours for future-dated wire (scheduled transfer) initiation in Central Time")),
                Map.entry(
                    "customer_service_hours",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "General customer service/inquiry hours in Central Time, may differ from wire-specific hours")),
                Map.entry(
                    "spanish_language_hours",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Spanish language support operating hours in Central Time, if available separately")),
                Map.entry(
                    "required_information",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Comma-separated or bullet-list of required information to initiate a wire, e.g., 'recipient name, account number, routing number, amount, purpose'")),
                Map.entry(
                    "foreign_exchange_contact",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Phone number or contact for Foreign Exchange Specialist inquiries")),
                Map.entry(
                    "time_zone_conversion",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'")),
                Map.entry(
                    "wire_transfer_deadlines",
                    Map.of(
                        "type",
                        List.of("string", "null"),
                        "description",
                        "Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'"))));

    Map<String, Object> extractRequestBody = new LinkedHashMap<>();
    extractRequestBody.put("file", Map.of("url", dataUrl));
    extractRequestBody.put(
        "config",
        Map.of(
            "schema",
            schema,
            "baseProcessor",
            "extraction_performance",
            "advancedOptions",
            Map.of(
                "reviewAgent",
                Map.of("enabled", true),
                "advancedMultimodalEnabled",
                true)));

    String extractRequestJson = toJson(extractRequestBody);
    HttpRequest extractRequest =
        HttpRequest.newBuilder()
            .uri(java.net.URI.create(API_BASE + "/v1/extractRuns/createAndPoll"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(extractRequestJson))
            .build();

    HttpResponse<String> extractResponse =
        httpClient.send(extractRequest, HttpResponse.BodyHandlers.ofString());
    if (extractResponse.statusCode() != 200) {
      throw new RuntimeException("Extract request failed: " + extractResponse.body());
    }

    Map<String, Object> extractRun = parseJson(extractResponse.body());
    String extractStatus = (String) extractRun.get("status");
    if (!"PROCESSED".equals(extractStatus)) {
      throw new RuntimeException("Extract failed with status: " + extractStatus);
    }

    Map<String, Object> extractedOutput = (Map<String, Object>) extractRun.get("output");
    Map<String, Object> extracted = (Map<String, Object>) extractedOutput.get("value");

    System.out.println("[Extract] Successfully extracted:");
    System.out.println(prettyJson(extracted));

    return Map.of(
        "parsed_markdown",
        markdownContent.toString(),
        "extracted_fields",
        extracted);
  }

  // Minimal JSON utilities using only built-in classes
  private static String toJson(Object obj) throws Exception {
    if (obj == null) return "null";
    if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
    if (obj instanceof Number) return obj.toString();
    if (obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(entry.getKey()).append("\":").append(toJson(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) 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 "\"" + obj.toString() + "\"";
  }

  private static String escapeJson(String str) {
    return str.replace("\\", "\\\\")
        .replace("\"", "\\\"")
        .replace("\n", "\\n")
        .replace("\r", "\\r")
        .replace("\t", "\\t");
  }

  private static Map<String, Object> parseJson(String json) throws Exception {
    // Simple recursive descent JSON parser
    json = json.trim();
    if (!json.startsWith("{")) throw new RuntimeException("Expected JSON object");
    return parseJsonObject(json, new int[] {0});
  }

  private static Map<String, Object> parseJsonObject(String json, int[] pos) throws Exception {
    Map<String, Object> result = new LinkedHashMap<>();
    pos[0]++; // skip '{'
    skipWhitespace(json, pos);

    if (json.charAt(pos[0]) == '}') {
      pos[0]++;
      return result;
    }

    while (true) {
      skipWhitespace(json, pos);
      String key = parseJsonString(json, pos);
      skipWhitespace(json, pos);
      if (json.charAt(pos[0]) != ':') throw new RuntimeException("Expected ':'");
      pos[0]++;
      skipWhitespace(json, pos);
      Object value = parseJsonValue(json, pos);
      result.put(key, value);
      skipWhitespace(json, pos);

      if (json.charAt(pos[0]) == '}') {
        pos[0]++;
        break;
      }
      if (json.charAt(pos[0]) != ',') throw new RuntimeException("Expected ',' or '}'");
      pos[0]++;
    }
    return result;
  }

  private static List<Object> parseJsonArray(String json, int[] pos) throws Exception {
    List<Object> result = new ArrayList<>();
    pos[0]++; // skip '['
    skipWhitespace(json, pos);

    if (json.charAt(pos[0]) == ']') {
      pos[0]++;
      return result;
    }

    while (true) {
      result.add(parseJsonValue(json, pos));
      skipWhitespace(json, pos);
      if (json.charAt(pos[0]) == ']') {
        pos[0]++;
        break;
      }
      if (json.charAt(pos[0]) != ',') throw new RuntimeException("Expected ',' or ']'");
      pos[0]++;
      skipWhitespace(json, pos);
    }
    return result;
  }

  private static Object parseJsonValue(String json, int[] pos) throws Exception {
    skipWhitespace(json, pos);
    char c = json.charAt(pos[0]);

    if (c == '"') return parseJsonString(json, pos);
    if (c == '{') return parseJsonObject(json, pos);
    if (c == '[') return parseJsonArray(json, pos);
    if (c == 't') {
      pos[0] += 4;
      return true;
    }
    if (c == 'f') {
      pos[0] += 5;
      return false;
    }
    if (c == 'n') {
      pos[0] += 4;
      return null;
    }

    int start = pos[0];
    while (pos[0] < json.length()
        && "0123456789.-".indexOf(json.charAt(pos[0])) >= 0) {
      pos[0]++;
    }
    String numStr = json.substring(start, pos[0]);
    if (numStr.contains(".")) return Double.parseDouble(numStr);
    return Long.parseLong(numStr);
  }

  private static String parseJsonString(String json, int[] pos) throws Exception {
    pos[0]++; // skip opening '"'
    StringBuilder sb = new StringBuilder();
    while (pos[0] < json.length()) {
      char c = json.charAt(pos[0]);
      if (c == '"') {
        pos[0]++;
        break;
      }
      if (c == '\\') {
        pos[0]++;
        if (pos[0] < json.length()) {
          char escaped = json.charAt(pos[0]);
          switch (escaped) {
            case 'n':
              sb.append('\n');
              break;
            case 'r':
              sb.append('\r');
              break;
            case 't':
              sb.append('\t');
              break;
            case '"':
              sb.append('"');
              break;
            case '\\':
              sb.append('\\');
              break;
            default:
              sb.append(escaped);
          }
          pos[0]++;
        }
      } else {
        sb.append(c);
        pos[0]++;
      }
    }
    return sb.toString();
  }

  private static void skipWhitespace(String json, int[] pos) {
    while (pos[0] < json.length() && " \n\r\t".indexOf(json.charAt(pos[0])) >= 0) {
      pos[0]++;
    }
  }

  private static String prettyJson(Object obj) throws Exception {
    return prettyJsonHelper(obj, 0);
  }

  private static String prettyJsonHelper(Object obj, int indent) throws Exception {
    String indentStr = "  ".repeat(indent);
    String nextIndentStr = "  ".repeat(indent + 1);

    if (obj == null) return "null";
    if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
    if (obj instanceof Number || obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      if (map.isEmpty()) return "{}";
      StringBuilder sb = new StringBuilder("{\n");
      List<?> keys = new ArrayList<>(map.keySet());
      for (int i = 0; i < keys.size(); i++) {
        Object key = keys.get(i);
        Object value = map.get(key);
        sb.append(nextIndentStr)
            .append("\"")
            .append(key)
            .append("\": ")
            .append(prettyJsonHelper(value, indent + 1));
        if (i < keys.size() - 1) sb.append(",");
        sb.append("\n");
      }
      sb.append(indentStr).append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      if (list.isEmpty()) return "[]";
      StringBuilder sb = new StringBuilder("[\n");
      for (int i = 0; i < list.size(); i++) {
        sb.append(nextIndentStr).append(prettyJsonHelper(list.get(i), indent + 1));
        if (i < list.size() - 1) sb.append(",");
        sb.append("\n");
      }
      sb.append(indentStr).append("]");
      return sb.toString();
    }
    return "\"" + obj.toString() + "\"";
  }

  // Main entry point for testing
  public static void main(String[] args) {
    String filePath = args.length > 0 ? args[0] : "__FILE_PATH__";
    try {
      Map<String, Object> result = processWireTransferQuickReferenceGuide(filePath);
      System.out.println("\n✓ Pipeline complete");
      System.out.println("Output: " + prettyJson(result));
    } catch (Exception e) {
      System.err.println("✗ Pipeline error: " + e.getMessage());
      e.printStackTrace();
      System.exit(1);
    }
  }
}
// NOTE: This code uses the Extend REST API directly via net/http.
// Extend does not publish an official Go SDK; the TypeScript SDK is a thin wrapper
// over this REST API. We call the same endpoints with the same JSON shapes.

package main

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

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

// 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 WireTransferFields `json:"value"`
	} `json:"output"`
}

// WireTransferFields represents the extracted schema fields.
type WireTransferFields struct {
	DocumentTitle         *string `json:"document_title"`
	Organization          *string `json:"organization"`
	PhoneNumber           *string `json:"phone_number"`
	SameDayWireHours      *string `json:"same_day_wire_hours"`
	BookInternalWireHours *string `json:"book_internal_wire_hours"`
	FutureDatedWireHours  *string `json:"future_dated_wire_hours"`
	CustomerServiceHours  *string `json:"customer_service_hours"`
	SpanishLanguageHours  *string `json:"spanish_language_hours"`
	RequiredInformation   *string `json:"required_information"`
	ForeignExchangeContact *string `json:"foreign_exchange_contact"`
	TimeZoneConversion    *string `json:"time_zone_conversion"`
	WireTransferDeadlines *string `json:"wire_transfer_deadlines"`
}

// Result holds the final output.
type Result struct {
	ParsedMarkdown  string              `json:"parsed_markdown"`
	ExtractedFields WireTransferFields  `json:"extracted_fields"`
}

func processWireTransferQuickReferenceGuide(filePath string, apiKey string) (*Result, error) {
	// Read file and convert to data URL.
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}
	dataURL := fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileBuffer))

	// Step 1: Parse PDF to markdown.
	fmt.Printf("[1/2] Parsing wire transfer guide from %s...\n", filePath)

	parsePayload := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"blockOptions": map[string]interface{}{
				"text": map[string]interface{}{
					"agentic": map[string]bool{
						"enabled": true,
					},
				},
			},
			"chunkingStrategy": map[string]string{
				"type": "document",
			},
		},
	}

	parseRun, err := createAndPollParseRun(parsePayload, apiKey)
	if err != nil {
		return nil, fmt.Errorf("parse request failed: %w", err)
	}

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

	markdownContent := ""
	for _, chunk := range parseRun.Output.Chunks {
		if markdownContent != "" {
			markdownContent += "\n\n"
		}
		markdownContent += chunk.Content
	}
	fmt.Printf("[Parse] Extracted %d chars of markdown\n", len(markdownContent))

	// Step 2: Extract structured fields.
	fmt.Println("[2/2] Extracting wire transfer fields...")

	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"document_title": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'",
			},
			"organization": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Financial institution name, typically 'Wells Fargo' or similar",
			},
			"phone_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'",
			},
			"same_day_wire_hours": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Operating hours for same-day wire initiation in Central Time, e.g., '7:00 AM - 4:00 PM CT, Monday–Friday'",
			},
			"book_internal_wire_hours": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time",
			},
			"future_dated_wire_hours": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Operating hours for future-dated wire (scheduled transfer) initiation in Central Time",
			},
			"customer_service_hours": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "General customer service/inquiry hours in Central Time, may differ from wire-specific hours",
			},
			"spanish_language_hours": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Spanish language support operating hours in Central Time, if available separately",
			},
			"required_information": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Comma-separated or bullet-list of required information to initiate a wire, e.g., 'recipient name, account number, routing number, amount, purpose'",
			},
			"foreign_exchange_contact": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Phone number or contact for Foreign Exchange Specialist inquiries",
			},
			"time_zone_conversion": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'",
			},
			"wire_transfer_deadlines": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'",
			},
		},
	}

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

	extractRun, err := createAndPollExtractRun(extractPayload, apiKey)
	if err != nil {
		return nil, fmt.Errorf("extract request failed: %w", err)
	}

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

	extracted := extractRun.Output.Value
	fmt.Println("[Extract] Successfully extracted:")
	b, _ := json.MarshalIndent(extracted, "", "  ")
	fmt.Println(string(b))

	return &Result{
		ParsedMarkdown:  markdownContent,
		ExtractedFields: extracted,
	}, nil
}

func createAndPollParseRun(payload map[string]interface{}, apiKey string) (*ParseRunResponse, error) {
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", extendAPIBaseURL+"/v1/parseRuns", bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}

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

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

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

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(respBody))
	}

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

	// Poll until status is PROCESSED or an error status.
	for parseRun.Status != "PROCESSED" && parseRun.Status != "FAILED" {
		time.Sleep(2 * time.Second)

		// In a real implementation, we'd extract the run ID from the response and poll it.
		// For this example, we assume the synchronous endpoint returns PROCESSED immediately.
		break
	}

	return &parseRun, nil
}

func createAndPollExtractRun(payload map[string]interface{}, apiKey string) (*ExtractRunResponse, error) {
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", extendAPIBaseURL+"/v1/extractRuns", bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}

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

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

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

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(respBody))
	}

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

	// Poll until status is PROCESSED or an error status.
	for extractRun.Status != "PROCESSED" && extractRun.Status != "FAILED" {
		time.Sleep(2 * time.Second)

		// In a real implementation, we'd extract the run ID from the response and poll it.
		// For this example, we assume the synchronous endpoint returns PROCESSED immediately.
		break
	}

	return &extractRun, nil
}

func main() {
	filePath := "__FILE_PATH__"
	if len(os.Args) > 1 {
		filePath = os.Args[1]
	}

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

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

	fmt.Println("\n✓ Pipeline complete")
	fmt.Println("Output:")
	b, _ := json.MarshalIndent(result, "", "  ")
	fmt.Println(string(b))
}
// Deploy the "Wire Transfer Quick Reference Guide" 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/wire-transfer-instructions.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: wire-transfer-instructions).

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, "wire-transfer-instructions.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": "Wire Transfer Quick Reference Guide Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "organization": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Financial institution providing the wire transfer service"
              },
              "phone_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Toll-free phone number to initiate wire transfer services"
              },
              "document_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Title of the document"
              },
              "same_day_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for same-day wire initiation in Central Time"
              },
              "required_information": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "List of required information needed to initiate a wire transfer"
              },
              "time_zone_conversion": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Time zone conversion information from Central Time"
              },
              "customer_service_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "General customer service operating hours in Central Time"
              },
              "spanish_language_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Spanish language support operating hours in Central Time"
              },
              "future_dated_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for future-dated wire initiation in Central Time"
              },
              "wire_transfer_deadlines": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Daily cutoff times for wire transfer requests"
              },
              "book_internal_wire_hours": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Operating hours for book/internal wire initiation in Central Time"
              },
              "foreign_exchange_contact": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Phone number for Foreign Exchange Specialist inquiries"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

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

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

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

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

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

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
#!/usr/bin/env python3
"""Deploy the "Wire Transfer Quick Reference Guide" 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/wire-transfer-instructions.json,
so re-running updates the existing workflow instead of duplicating it.

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

Generated by doc1 (template: wire-transfer-instructions).
"""

import json
import os
import sys
from pathlib import Path

import requests

API = "https://api.extend.ai"
VERSION = "2026-02-09"
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
    print("Set EXTEND_API_KEY first.", file=sys.stderr)
    sys.exit(1)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "wire-transfer-instructions.json"

state: dict = {}
if STATE_FILE.exists():
    with open(STATE_FILE, "r") as f:
        state = json.load(f)


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


def api(method: str, path_name: str, body: dict | None = None) -> dict:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "x-extend-api-version": VERSION,
    }
    if body:
        headers["Content-Type"] = "application/json"

    res = requests.request(
        method, API + path_name, headers=headers, json=body
    )

    try:
        data = res.json()
    except Exception:
        data = {}

    if not res.ok:
        error_msg = json.dumps(data)[:300]
        raise RuntimeError(
            f"{method} {path_name} failed ({res.status_code}): {error_msg}"
        )

    return data


# ── Workflow definition — extractor/classifier/splitter configs inline ──────
WORKFLOW = {
    "name": "Wire Transfer Quick Reference Guide Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {"agentic": {"enabled": True}}
                    },
                    "chunkingStrategy": {"type": "document"},
                }
            },
            "next": [{"step": "extraction2"}],
        },
        {
            "name": "extraction2",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "organization": {
                                "type": ["string", "null"],
                                "description": "Financial institution providing the wire transfer service",
                            },
                            "phone_number": {
                                "type": ["string", "null"],
                                "description": "Toll-free phone number to initiate wire transfer services",
                            },
                            "document_title": {
                                "type": ["string", "null"],
                                "description": "Title of the document",
                            },
                            "same_day_wire_hours": {
                                "type": ["string", "null"],
                                "description": "Operating hours for same-day wire initiation in Central Time",
                            },
                            "required_information": {
                                "type": ["string", "null"],
                                "description": "List of required information needed to initiate a wire transfer",
                            },
                            "time_zone_conversion": {
                                "type": ["string", "null"],
                                "description": "Time zone conversion information from Central Time",
                            },
                            "customer_service_hours": {
                                "type": ["string", "null"],
                                "description": "General customer service operating hours in Central Time",
                            },
                            "spanish_language_hours": {
                                "type": ["string", "null"],
                                "description": "Spanish language support operating hours in Central Time",
                            },
                            "future_dated_wire_hours": {
                                "type": ["string", "null"],
                                "description": "Operating hours for future-dated wire initiation in Central Time",
                            },
                            "wire_transfer_deadlines": {
                                "type": ["string", "null"],
                                "description": "Daily cutoff times for wire transfer requests",
                            },
                            "book_internal_wire_hours": {
                                "type": ["string", "null"],
                                "description": "Operating hours for book/internal wire initiation in Central Time",
                            },
                            "foreign_exchange_contact": {
                                "type": ["string", "null"],
                                "description": "Phone number for Foreign Exchange Specialist inquiries",
                            },
                        },
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {"enabled": True},
                        "advancedMultimodalEnabled": True,
                    },
                }
            },
        },
    ],
}


def main() -> None:
    print(f'Deploying "{WORKFLOW["name"]}…"')

    if state.get("workflowId"):
        print(
            f'✓ workflow already provisioned ({state["workflowId"]}) — updating steps'
        )
        api("POST", f'/workflows/{state["workflowId"]}', {"steps": WORKFLOW["steps"]})
    else:
        # 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:
            list_resp = api("GET", f'/workflows?name={WORKFLOW["name"]}')
            items = list_resp.get("data") or list_resp.get("items") or []
            existing = next(
                (x for x in items if x.get("name") == WORKFLOW["name"]), None
            )
            if existing and existing.get("id"):
                state["workflowId"] = existing["id"]
                save_state()
                print(
                    f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing["id"]}) — updating steps'
                )
                api("POST", f'/workflows/{existing["id"]}', {"steps": WORKFLOW["steps"]})
        except Exception:
            # lookup is best-effort; fall through to create
            pass

        if not state.get("workflowId"):
            created = api("POST", "/workflows", WORKFLOW)
            wf_id = created.get("id") or (
                created.get("workflow", {}).get("id") if created.get("workflow") else None
            )
            if not wf_id:
                raise RuntimeError("Could not read created workflow id from response")
            state["workflowId"] = wf_id
            save_state()
            print(f"+ created workflow ({wf_id})")

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

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


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// Extend AI REST API provisioning script for "Wire Transfer Quick Reference Guide" template.
// Uses Extend's REST API directly via java.net.http.HttpClient (no official Java SDK exists yet).
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   javac Provision.java && java Provision
//
// Generated by doc1 (template: wire-transfer-instructions).

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

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("wire-transfer-instructions.json");
    private static final HttpClient HTTP = HttpClient.newHttpClient();

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

    private static class State {
        String workflowId;

        State() {}

        static State load() throws IOException {
            State s = new State();
            if (Files.exists(STATE_FILE)) {
                String json = Files.readString(STATE_FILE);
                int idStart = json.indexOf("\"workflowId\"");
                if (idStart != -1) {
                    idStart = json.indexOf("\"", idStart + 13);
                    if (idStart != -1) {
                        int idEnd = json.indexOf("\"", idStart + 1);
                        if (idEnd != -1) {
                            s.workflowId = json.substring(idStart + 1, idEnd);
                        }
                    }
                }
            }
            return s;
        }

        void save() throws IOException {
            Files.createDirectories(STATE_DIR);
            String json = "{\"workflowId\":\"" + (workflowId != null ? workflowId : "") + "\"}";
            Files.writeString(STATE_FILE, json);
        }
    }

    private static String api(String method, String pathName, String body) throws Exception {
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(API + pathName))
                .header("Authorization", "Bearer " + API_KEY)
                .header("x-extend-api-version", VERSION);

        if (body != null) {
            builder.header("Content-Type", "application/json").method(method, HttpRequest.BodyPublishers.ofString(body));
        } else {
            builder.method(method, HttpRequest.BodyPublishers.noBody());
        }

        HttpResponse<String> res = HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() < 200 || res.statusCode() >= 300) {
            String preview = res.body().length() > 300 ? res.body().substring(0, 300) : res.body();
            throw new Exception(method + " " + pathName + " failed (" + res.statusCode() + "): " + preview);
        }
        return res.body();
    }

    private static String buildWorkflowJson() {
        return "{\"name\":\"Wire Transfer Quick Reference Guide Processing Pipeline\",\"steps\":[{\"name\":\"startTrigger1\",\"type\":\"TRIGGER\",\"next\":[{\"step\":\"parse1\"}]},{\"name\":\"parse1\",\"type\":\"PARSE\",\"config\":{\"parseConfig\":{\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},\"chunkingStrategy\":{\"type\":\"document\"}}},\"next\":[{\"step\":\"extraction2\"}]},{\"name\":\"extraction2\",\"type\":\"EXTRACT\",\"config\":{\"extractorConfig\":{\"schema\":{\"type\":\"object\",\"properties\":{\"organization\":{\"type\":[\"string\",\"null\"],\"description\":\"Financial institution providing the wire transfer service\"},\"phone_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Toll-free phone number to initiate wire transfer services\"},\"document_title\":{\"type\":[\"string\",\"null\"],\"description\":\"Title of the document\"},\"same_day_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for same-day wire initiation in Central Time\"},\"required_information\":{\"type\":[\"string\",\"null\"],\"description\":\"List of required information needed to initiate a wire transfer\"},\"time_zone_conversion\":{\"type\":[\"string\",\"null\"],\"description\":\"Time zone conversion information from Central Time\"},\"customer_service_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"General customer service operating hours in Central Time\"},\"spanish_language_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Spanish language support operating hours in Central Time\"},\"future_dated_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for future-dated wire initiation in Central Time\"},\"wire_transfer_deadlines\":{\"type\":[\"string\",\"null\"],\"description\":\"Daily cutoff times for wire transfer requests\"},\"book_internal_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for book/internal wire initiation in Central Time\"},\"foreign_exchange_contact\":{\"type\":[\"string\",\"null\"],\"description\":\"Phone number for Foreign Exchange Specialist inquiries\"}}},\"baseProcessor\":\"extraction_performance\",\"advancedOptions\":{\"reviewAgent\":{\"enabled\":true},\"advancedMultimodalEnabled\":true}}}}]}";
    }

    private static String getStepsUpdateJson() {
        return "{\"steps\":[{\"name\":\"startTrigger1\",\"type\":\"TRIGGER\",\"next\":[{\"step\":\"parse1\"}]},{\"name\":\"parse1\",\"type\":\"PARSE\",\"config\":{\"parseConfig\":{\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},\"chunkingStrategy\":{\"type\":\"document\"}}},\"next\":[{\"step\":\"extraction2\"}]},{\"name\":\"extraction2\",\"type\":\"EXTRACT\",\"config\":{\"extractorConfig\":{\"schema\":{\"type\":\"object\",\"properties\":{\"organization\":{\"type\":[\"string\",\"null\"],\"description\":\"Financial institution providing the wire transfer service\"},\"phone_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Toll-free phone number to initiate wire transfer services\"},\"document_title\":{\"type\":[\"string\",\"null\"],\"description\":\"Title of the document\"},\"same_day_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for same-day wire initiation in Central Time\"},\"required_information\":{\"type\":[\"string\",\"null\"],\"description\":\"List of required information needed to initiate a wire transfer\"},\"time_zone_conversion\":{\"type\":[\"string\",\"null\"],\"description\":\"Time zone conversion information from Central Time\"},\"customer_service_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"General customer service operating hours in Central Time\"},\"spanish_language_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Spanish language support operating hours in Central Time\"},\"future_dated_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for future-dated wire initiation in Central Time\"},\"wire_transfer_deadlines\":{\"type\":[\"string\",\"null\"],\"description\":\"Daily cutoff times for wire transfer requests\"},\"book_internal_wire_hours\":{\"type\":[\"string\",\"null\"],\"description\":\"Operating hours for book/internal wire initiation in Central Time\"},\"foreign_exchange_contact\":{\"type\":[\"string\",\"null\"],\"description\":\"Phone number for Foreign Exchange Specialist inquiries\"}}},\"baseProcessor\":\"extraction_performance\",\"advancedOptions\":{\"reviewAgent\":{\"enabled\":true},\"advancedMultimodalEnabled\":true}}}}]}";
    }

    private static String extractIdFromJson(String json) {
        int idIdx = json.indexOf("\"id\"");
        if (idIdx != -1) {
            int colonIdx = json.indexOf(":", idIdx);
            int quoteIdx = json.indexOf("\"", colonIdx);
            if (quoteIdx != -1) {
                int endIdx = json.indexOf("\"", quoteIdx + 1);
                if (endIdx != -1) {
                    return json.substring(quoteIdx + 1, endIdx);
                }
            }
        }
        int wfIdx = json.indexOf("\"workflow\"");
        if (wfIdx != -1) {
            int idIdx2 = json.indexOf("\"id\"", wfIdx);
            if (idIdx2 != -1) {
                int colonIdx = json.indexOf(":", idIdx2);
                int quoteIdx = json.indexOf("\"", colonIdx);
                if (quoteIdx != -1) {
                    int endIdx = json.indexOf("\"", quoteIdx + 1);
                    if (endIdx != -1) {
                        return json.substring(quoteIdx + 1, endIdx);
                    }
                }
            }
        }
        return null;
    }

    private static String extractNameFromJson(String json) {
        int nameIdx = json.indexOf("\"name\"");
        if (nameIdx != -1) {
            int colonIdx = json.indexOf(":", nameIdx);
            int quoteIdx = json.indexOf("\"", colonIdx);
            if (quoteIdx != -1) {
                int endIdx = json.indexOf("\"", quoteIdx + 1);
                if (endIdx != -1) {
                    return json.substring(quoteIdx + 1, endIdx);
                }
            }
        }
        return null;
    }

    public static void main(String[] args) {
        try {
            String workflowName = "Wire Transfer Quick Reference Guide Processing Pipeline";
            System.out.println("Deploying \"" + workflowName + "\"…");

            State state = State.load();

            if (state.workflowId != null && !state.workflowId.isEmpty()) {
                System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
                api("POST", "/workflows/" + state.workflowId, getStepsUpdateJson());
            } else {
                boolean foundExisting = false;
                try {
                    String encoded = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
                    String listJson = api("GET", "/workflows?name=" + encoded, null);
                    
                    int dataIdx = listJson.indexOf("\"data\"");
                    int itemsIdx = listJson.indexOf("\"items\"");
                    int arrayStart = -1;
                    if (dataIdx != -1) {
                        arrayStart = listJson.indexOf("[", dataIdx);
                    } else if (itemsIdx != -1) {
                        arrayStart = listJson.indexOf("[", itemsIdx);
                    }

                    if (arrayStart != -1) {
                        int arrayEnd = listJson.indexOf("]", arrayStart);
                        if (arrayEnd != -1) {
                            String arrayContent = listJson.substring(arrayStart + 1, arrayEnd);
                            int objStart = 0;
                            while (true) {
                                int nextObj = arrayContent.indexOf("{", objStart);
                                if (nextObj == -1) break;
                                int nextEnd = arrayContent.indexOf("}", nextObj);
                                if (nextEnd == -1) break;
                                String obj = arrayContent.substring(nextObj, nextEnd + 1);
                                String objName = extractNameFromJson(obj);
                                if (workflowName.equals(objName)) {
                                    String objId = extractIdFromJson(obj);
                                    if (objId != null) {
                                        state.workflowId = objId;
                                        state.save();
                                        System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + objId + ") — updating steps");
                                        api("POST", "/workflows/" + objId, getStepsUpdateJson());
                                        foundExisting = true;
                                        break;
                                    }
                                }
                                objStart = nextEnd + 1;
                            }
                        }
                    }
                } catch (Exception ignored) {
                }

                if (!foundExisting && (state.workflowId == null || state.workflowId.isEmpty())) {
                    String created = api("POST", "/workflows", buildWorkflowJson());
                    String wfId = extractIdFromJson(created);
                    if (wfId == null || wfId.isEmpty()) {
                        throw new Exception("Could not read created workflow id from response");
                    }
                    state.workflowId = wfId;
                    state.save();
                    System.out.println("+ created workflow (" + wfId + ")");
                }
            }

            try {
                api("POST", "/workflows/" + state.workflowId + "/versions", "{}");
            } catch (Exception ignored) {
            }

            System.out.println("\nDone. Run documents through it with:");
            System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
            System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
        } catch (Exception e) {
            System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
            System.exit(1);
        }
    }
}
package main

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

// This script calls the Extend REST API directly (https://api.extend.ai).
// Extend does not publish an official Go SDK; we use only stdlib net/http and encoding/json.

const (
	api     = "https://api.extend.ai"
	version = "2026-02-09"
	stateDir = ".extend"
	stateFile = "wire-transfer-instructions.json"
)

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

type APIResponse struct {
	ID       string `json:"id,omitempty"`
	Workflow struct {
		ID string `json:"id,omitempty"`
	} `json:"workflow,omitempty"`
	Data  []interface{} `json:"data,omitempty"`
	Items []interface{} `json:"items,omitempty"`
}

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

var workflow = map[string]interface{}{
	"name": "Wire Transfer Quick Reference Guide Processing Pipeline",
	"steps": []map[string]interface{}{
		{
			"name": "startTrigger1",
			"type": "TRIGGER",
			"next": []map[string]interface{}{
				{"step": "parse1"},
			},
		},
		{
			"name": "parse1",
			"type": "PARSE",
			"config": map[string]interface{}{
				"parseConfig": map[string]interface{}{
					"blockOptions": map[string]interface{}{
						"text": map[string]interface{}{
							"agentic": map[string]interface{}{
								"enabled": true,
							},
						},
					},
					"chunkingStrategy": map[string]interface{}{
						"type": "document",
					},
				},
			},
			"next": []map[string]interface{}{
				{"step": "extraction2"},
			},
		},
		{
			"name": "extraction2",
			"type": "EXTRACT",
			"config": map[string]interface{}{
				"extractorConfig": map[string]interface{}{
					"schema": map[string]interface{}{
						"type": "object",
						"properties": map[string]interface{}{
							"organization": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Financial institution providing the wire transfer service",
							},
							"phone_number": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Toll-free phone number to initiate wire transfer services",
							},
							"document_title": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Title of the document",
							},
							"same_day_wire_hours": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Operating hours for same-day wire initiation in Central Time",
							},
							"required_information": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "List of required information needed to initiate a wire transfer",
							},
							"time_zone_conversion": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Time zone conversion information from Central Time",
							},
							"customer_service_hours": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "General customer service operating hours in Central Time",
							},
							"spanish_language_hours": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Spanish language support operating hours in Central Time",
							},
							"future_dated_wire_hours": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Operating hours for future-dated wire initiation in Central Time",
							},
							"wire_transfer_deadlines": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Daily cutoff times for wire transfer requests",
							},
							"book_internal_wire_hours": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Operating hours for book/internal wire initiation in Central Time",
							},
							"foreign_exchange_contact": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Phone number for Foreign Exchange Specialist inquiries",
							},
						},
					},
					"baseProcessor": "extraction_performance",
					"advancedOptions": map[string]interface{}{
						"reviewAgent": map[string]interface{}{
							"enabled": true,
						},
						"advancedMultimodalEnabled": true,
					},
				},
			},
		},
	},
}

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 loadState() (*State, error) {
	path := filepath.Join(stateDir, stateFile)
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &State{}, nil
		}
		return nil, err
	}
	var s State
	if err := json.Unmarshal(data, &s); err != nil {
		return nil, err
	}
	return &s, nil
}

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

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	url := api + pathName
	apiKey := getAPIKey()

	var bodyReader io.Reader
	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

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

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

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

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

	var data map[string]interface{}
	json.Unmarshal(resBody, &data)

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

	return data, nil
}

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

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

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		steps, _ := workflow["steps"]
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{"steps": steps})
		if err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to reuse an existing workflow with the same name
		queryURL := fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflowName))
		listResp, err := apiCall("GET", queryURL, nil)
		var found *WorkflowItem
		if err == nil {
			var items []WorkflowItem
			if dataRaw, ok := listResp["data"]; ok {
				if dataBytes, err := json.Marshal(dataRaw); err == nil {
					json.Unmarshal(dataBytes, &items)
				}
			} else if itemsRaw, ok := listResp["items"]; ok {
				if itemsBytes, err := json.Marshal(itemsRaw); err == nil {
					json.Unmarshal(itemsBytes, &items)
				}
			}
			for i := range items {
				if items[i].Name == workflowName {
					found = &items[i]
					break
				}
			}
		}

		if found != nil && found.ID != "" {
			state.WorkflowID = found.ID
			saveState(state)
			fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflowName, found.ID)
			steps, _ := workflow["steps"]
			_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", found.ID), map[string]interface{}{"steps": steps})
			if err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}
		} else {
			createResp, err := apiCall("POST", "/workflows", workflow)
			if err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}
			wfID := ""
			if id, ok := createResp["id"].(string); ok && id != "" {
				wfID = id
			} else if workflowMap, ok := createResp["workflow"].(map[string]interface{}); ok {
				if id, ok := workflowMap["id"].(string); ok {
					wfID = id
				}
			}
			if wfID == "" {
				fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
				os.Exit(1)
			}
			state.WorkflowID = wfID
			saveState(state)
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

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

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

Frequently Asked Questions (FAQ)

Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
Extract both `transfer_amount` (e.g., "$50,000.00") and `currency_code` (e.g., "USD") as separate string fields in your schema, not as numbers—the SDK returns them as strings to preserve formatting and prevent floating-point errors. In post-processing, parse and validate using a currency library (e.g., `dinero.js`) to catch mismatches.
Extract returns `confidence` per field; require ≥0.95 for critical fields (beneficiary name, account number, amount) to auto-process, and route anything below 0.90 to compliance review. For moderate risk (0.90–0.95), auto-approve but flag for post-audit; this balances fraud prevention with operational speed.
Tags
Wire TransferBanking OperationsCustomer ServiceFund Transfer
About this template

This template processes wire transfer packets. It captures phone-based voice activation procedures, department operating hours across different wire transfer types, and a comprehensive checklist of required information for processing transfers.

Document formats
  • PDF
  • Word / DOCX
Requirements
  • Long tables

Relevant templates for Financial & Banking

  1. 01
    Driver's License ExtractorParse → Extract
    Extracts personal identification and licensing data from driver license documents.
    PDFImages & Scanswww.extend.ai/templates/driver-license-template
  2. 02
    Receipt ExtractorParse → Extract
    Extracts itemized sales, pricing, GST tax, and payment details from retail receipts.
    PDFImages & Scanswww.extend.ai/templates/receipt-parse-extract
  3. 03
    Bank Statement ExtractorParse → Extract
    Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.
    PDFImages & Scanswww.extend.ai/templates/bank-statement
  4. 04
    Vendor Invoice ExtractorParse → Extract
    Extracts charges, billing details, line items, totals, and due dates vendor invoices.
    PDFImages & Scanswww.extend.ai/templates/vendor-invoice
  5. 05
    Pay Stub ExtractorParse → Extract
    Extracts employee earnings, deductions, taxes, and net pay from pay stubs.
    PDFwww.extend.ai/templates/pay-stub
  6. 06
    Check ExtractorParse → Extract
    Extracts check details including payee, amount, date, and bank routing information.
    PDFImages & Scanswww.extend.ai/templates/check
  7. 07
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement
  8. 08
    1040 Tax Return ExtractorParse → Extract
    Extracts personal and dependent information from U.S. individual income tax returns.
    PDFImages & Scanswww.extend.ai/templates/tax-return-form-1040