Financial & BankingParse → Extract

Wire Instruction Verification 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 Guides 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. by using Extend's Parse, Extract primitives.

Input
Wire Transfer Quick Reference Guides
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 json
import sys
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}")
        sys.exit(1)


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())
// NOTE: This code uses Extend's REST API directly via Java's built-in HttpClient.
// Extend does not publish an official Java SDK; the REST API is called with Bearer token auth.

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class WireTransferQuickReferenceProcessor {

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

  public static void main(String[] args) throws Exception {
    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: " + jsonStringify(result, 2));
    } catch (Exception e) {
      System.err.println("✗ Pipeline error: " + e.getMessage());
      e.printStackTrace();
      System.exit(1);
    }
  }

  public static Map<String, Object> processWireTransferQuickReferenceGuide(String filePath)
      throws Exception {
    // Convert local file to 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: Parse with agentic OCR
    String parseRequestBody =
        jsonStringify(
            Map.of(
                "file",
                Map.of("url", dataUrl),
                "config",
                Map.of(
                    "blockOptions",
                    Map.of("text", Map.of("agentic", Map.of("enabled", true))),
                    "chunkingStrategy",
                    Map.of("type", "document"))),
            0);

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

    HttpResponse<String> parseResponse = httpClient.send(parseRequest, HttpResponse.BodyHandlers.ofString());
    Map<String, Object> parseRun = jsonParse(parseResponse.body());

    String parseStatus = (String) parseRun.get("status");
    if (!"PROCESSED".equals(parseStatus)) {
      throw new Exception("Parse failed with status: " + parseStatus);
    }

    StringBuilder markdownContent = new StringBuilder();
    Map<String, Object> output = (Map<String, Object>) parseRun.get("output");
    Object chunksObj = output.get("chunks");
    if (chunksObj instanceof java.util.List) {
      java.util.List<Object> chunks = (java.util.List<Object>) chunksObj;
      for (int i = 0; i < chunks.size(); i++) {
        if (i > 0) {
          markdownContent.append("\n\n");
        }
        Map<String, Object> chunk = (Map<String, Object>) chunks.get(i);
        markdownContent.append((String) chunk.get("content"));
      }
    }
    System.out.println("[Parse] Extracted " + markdownContent.length() + " chars of markdown");

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

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

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

    HttpResponse<String> extractResponse =
        httpClient.send(extractRequest, HttpResponse.BodyHandlers.ofString());
    Map<String, Object> extractRun = jsonParse(extractResponse.body());

    String extractStatus = (String) extractRun.get("status");
    if (!"PROCESSED".equals(extractStatus)) {
      throw new Exception("Extract failed with status: " + extractStatus);
    }

    Map<String, Object> extractOutput = (Map<String, Object>) extractRun.get("output");
    Map<String, Object> extracted = (Map<String, Object>) extractOutput.get("value");
    System.out.println("[Extract] Successfully extracted:");
    System.out.println(jsonStringify(extracted, 2));

    Map<String, Object> result = new HashMap<>();
    result.put("parsed_markdown", markdownContent.toString());
    result.put("extracted_fields", extracted);
    return result;
  }

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

    Map<String, Object> properties = new HashMap<>();
    properties.put(
        "document_title",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Title of the document, e.g., 'Wells Fargo Wire Transfer Quick Reference Guide'"));
    properties.put(
        "organization",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Financial institution name, typically 'Wells Fargo' or similar"));
    properties.put(
        "phone_number",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Toll-free phone number to initiate wire transfer services, e.g., '1-800-XXX-XXXX'"));
    properties.put(
        "same_day_wire_hours",
        Map.of(
            "type",
            new 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'"));
    properties.put(
        "book_internal_wire_hours",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Operating hours for book/internal wire (between Wells Fargo accounts) in Central Time"));
    properties.put(
        "future_dated_wire_hours",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Operating hours for future-dated wire (scheduled transfer) initiation in Central Time"));
    properties.put(
        "customer_service_hours",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "General customer service/inquiry hours in Central Time, may differ from wire-specific hours"));
    properties.put(
        "spanish_language_hours",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Spanish language support operating hours in Central Time, if available separately"));
    properties.put(
        "required_information",
        Map.of(
            "type",
            new 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'"));
    properties.put(
        "foreign_exchange_contact",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Phone number or contact for Foreign Exchange Specialist inquiries"));
    properties.put(
        "time_zone_conversion",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Explicit time zone conversion guidance, e.g., 'Central Time (CT) is 2 hours ahead of Pacific Time (PT)'"));
    properties.put(
        "wire_transfer_deadlines",
        Map.of(
            "type",
            new String[] {"string", "null"},
            "description",
            "Daily cutoff times for wire transfer requests, e.g., 'Same-day wires must be initiated by 4:00 PM CT'"));

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

  private static Map<String, Object> jsonParse(String json) {
    return SimpleJsonParser.parse(json);
  }

  private static String jsonStringify(Object obj, int indent) {
    return SimpleJsonBuilder.stringify(obj, indent);
  }

  // Minimal JSON parser helper (without external dependencies)
  static class SimpleJsonParser {
    static Map<String, Object> parse(String json) {
      json = json.trim();
      if (json.startsWith("{") && json.endsWith("}")) {
        return parseObject(json.substring(1, json.length() - 1));
      }
      return new HashMap<>();
    }

    static Map<String, Object> parseObject(String content) {
      Map<String, Object> map = new HashMap<>();
      int depth = 0;
      StringBuilder currentKey = new StringBuilder();
      StringBuilder currentValue = new StringBuilder();
      boolean inString = false;
      boolean parsingKey = true;

      for (int i = 0; i < content.length(); i++) {
        char c = content.charAt(i);

        if (c == '"' && (i == 0 || content.charAt(i - 1) != '\\')) {
          inString = !inString;
          if (!parsingKey) {
            currentValue.append(c);
          }
          continue;
        }

        if (!inString) {
          if (c == ':' && parsingKey && depth == 0) {
            parsingKey = false;
            continue;
          }
          if (c == ',' && depth == 0) {
            String key = currentKey.toString().trim().replaceAll("^\"|\"$", "");
            String value = currentValue.toString().trim();
            map.put(key, parseValue(value));
            currentKey = new StringBuilder();
            currentValue = new StringBuilder();
            parsingKey = true;
            continue;
          }
          if ((c == '{' || c == '[') && !parsingKey) {
            depth++;
          } else if ((c == '}' || c == ']') && !parsingKey) {
            depth--;
          }
        }

        if (parsingKey) {
          currentKey.append(c);
        } else {
          currentValue.append(c);
        }
      }

      if (currentKey.length() > 0) {
        String key = currentKey.toString().trim().replaceAll("^\"|\"$", "");
        String value = currentValue.toString().trim();
        map.put(key, parseValue(value));
      }

      return map;
    }

    static Object parseValue(String value) {
      value = value.trim();
      if (value.startsWith("\"") && value.endsWith("\"")) {
        return value.substring(1, value.length() - 1);
      }
      if (value.equals("null")) {
        return null;
      }
      if (value.equals("true")) {
        return true;
      }
      if (value.equals("false")) {
        return false;
      }
      if (value.startsWith("{") && value.endsWith("}")) {
        return parseObject(value.substring(1, value.length() - 1));
      }
      if (value.startsWith("[") && value.endsWith("]")) {
        return parseArray(value.substring(1, value.length() - 1));
      }
      try {
        if (value.contains(".")) {
          return Double.parseDouble(value);
        }
        return Long.parseLong(value);
      } catch (NumberFormatException e) {
        return value;
      }
    }

    static java.util.List<Object> parseArray(String content) {
      java.util.List<Object> list = new java.util.ArrayList<>();
      int depth = 0;
      StringBuilder current = new StringBuilder();
      boolean inString = false;

      for (int i = 0; i < content.length(); i++) {
        char c = content.charAt(i);

        if (c == '"' && (i == 0 || content.charAt(i - 1) != '\\')) {
          inString = !inString;
          current.append(c);
          continue;
        }

        if (!inString) {
          if ((c == '{' || c == '[')) {
            depth++;
          } else if ((c == '}' || c == ']')) {
            depth--;
          }
          if (c == ',' && depth == 0) {
            list.add(parseValue(current.toString()));
            current = new StringBuilder();
            continue;
          }
        }

        current.append(c);
      }

      if (current.length() > 0) {
        list.add(parseValue(current.toString()));
      }

      return list;
    }
  }

  // Minimal JSON builder helper
  static class SimpleJsonBuilder {
    static String stringify(Object obj, int indent) {
      return stringifyInternal(obj, indent, 0);
    }

    static String stringifyInternal(Object obj, int indent, int depth) {
      if (obj == null) {
        return "null";
      }
      if (obj instanceof String) {
        return "\"" + escapeString((String) obj) + "\"";
      }
      if (obj instanceof Boolean || obj instanceof Number) {
        return obj.toString();
      }
      if (obj instanceof Map) {
        return stringifyMap((Map<String, Object>) obj, indent, depth);
      }
      if (obj instanceof java.util.List) {
        return stringifyList((java.util.List<Object>) obj, indent, depth);
      }
      if (obj instanceof String[]) {
        java.util.List<Object> list = new java.util.ArrayList<>();
        for (String s : (String[]) obj) {
          list.add(s);
        }
        return stringifyList(list, indent, depth);
      }
      return "\"" + obj.toString() + "\"";
    }

    static String stringifyMap(Map<String, Object> map, int indent, int depth) {
      if (map.isEmpty()) {
        return "{}";
      }
      StringBuilder sb = new StringBuilder("{");
      if (indent > 0) {
        sb.append("\n");
      }
      boolean first = true;
      for (Map.Entry<String, Object> entry : map.entrySet()) {
        if (!first) {
          sb.append(",");
          if (indent > 0) {
            sb.append("\n");
          }
        }
        if (indent > 0) {
          sb.append(" ".repeat((depth + 1) * indent));
        }
        sb.append("\"").append(entry.getKey()).append("\": ");
        sb.append(stringifyInternal(entry.getValue(), indent, depth + 1));
        first = false;
      }
      if (indent > 0) {
        sb.append("\n").append(" ".repeat(depth * indent));
      }
      sb.append("}");
      return sb.toString();
    }

    static String stringifyList(java.util.List<Object> list, int indent, int depth) {
      if (list.isEmpty()) {
        return "[]";
      }
      StringBuilder sb = new StringBuilder("[");
      if (indent > 0) {
        sb.append("\n");
      }
      for (int i = 0; i < list.size(); i++) {
        if (i > 0) {
          sb.append(",");
          if (indent > 0) {
            sb.append("\n");
          }
        }
        if (indent > 0) {
          sb.append(" ".repeat((depth + 1) * indent));
        }
        sb.append(stringifyInternal(list.get(i), indent, depth + 1));
      }
      if (indent > 0) {
        sb.append("\n").append(" ".repeat(depth * indent));
      }
      sb.append("]");
      return sb.toString();
    }

    static String escapeString(String s) {
      return s.replace("\\", "\\\\")
          .replace("\"", "\\\"")
          .replace("\n", "\\n")
          .replace("\r", "\\r")
          .replace("\t", "\\t");
    }
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls the same endpoints and uses the same JSON shapes that the TypeScript SDK wraps.

package main

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

const extendAPIBase = "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"`
}

// ExtractedFields represents the extracted wire transfer fields
type ExtractedFields struct {
	DocumentTitle       *string `json:"document_title"`
	Organization        *string `json:"organization"`
	PhoneNumber         *string `json:"phone_number"`
	SameDayWireHours    *string `json:"same_day_wire_hours"`
	BookInternalWireHrs *string `json:"book_internal_wire_hours"`
	FutureDatedWireHrs  *string `json:"future_dated_wire_hours"`
	CustomerServiceHrs  *string `json:"customer_service_hours"`
	SpanishLanguageHrs  *string `json:"spanish_language_hours"`
	RequiredInfo        *string `json:"required_information"`
	ForeignExchContact  *string `json:"foreign_exchange_contact"`
	TimeZoneConversion  *string `json:"time_zone_conversion"`
	WireTransferDeadln  *string `json:"wire_transfer_deadlines"`
}

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

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

// makeRequest sends a POST request to the Extend API
func makeRequest(endpoint string, payload interface{}, apiKey string) ([]byte, error) {
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", extendAPIBase+endpoint, bytes.NewReader(payloadBytes))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

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

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

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

	return body, nil
}

// pollParseRun polls until parse run completes
func pollParseRun(runID string, apiKey string) (*ParseRunResponse, error) {
	for {
		body, err := makeRequest("/parse-runs/"+runID, nil, apiKey)
		if err != nil {
			return nil, err
		}

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

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

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

// pollExtractRun polls until extract run completes
func pollExtractRun(runID string, apiKey string) (*ExtractRunResponse, error) {
	for {
		body, err := makeRequest("/extract-runs/"+runID, nil, apiKey)
		if err != nil {
			return nil, err
		}

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

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

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

// ProcessWireTransferQuickReferenceGuide processes a wire transfer guide PDF
func ProcessWireTransferQuickReferenceGuide(filePath string, apiKey string) (*PipelineResult, error) {
	// Read file and convert to data URL
	fileBuffer, err := ioutil.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %v", err)
	}

	dataURL := "data:application/pdf;base64," + 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",
			},
		},
	}

	parseReqBody, err := json.Marshal(parsePayload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", extendAPIBase+"/parse-runs", bytes.NewReader(parseReqBody))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

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

	var parseInitResp struct {
		ID string `json:"id"`
	}
	respBody, _ := ioutil.ReadAll(resp.Body)
	if err := json.Unmarshal(respBody, &parseInitResp); err != nil {
		return nil, err
	}

	parseRun, err := pollParseRun(parseInitResp.ID, apiKey)
	if err != nil {
		return nil, err
	}

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

	markdownContent := ""
	for i, chunk := range parseRun.Output.Chunks {
		if i > 0 {
			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...")

	extractPayload := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"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'",
					},
				},
			},
			"baseProcessor": "extraction_performance",
			"advancedOptions": map[string]interface{}{
				"reviewAgent": map[string]bool{
					"enabled": true,
				},
				"advancedMultimodalEnabled": true,
			},
		},
	}

	extractReqBody, err := json.Marshal(extractPayload)
	if err != nil {
		return nil, err
	}

	req, err = http.NewRequest("POST", extendAPIBase+"/extract-runs", bytes.NewReader(extractReqBody))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

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

	var extractInitResp struct {
		ID string `json:"id"`
	}
	respBody, _ = ioutil.ReadAll(resp.Body)
	if err := json.Unmarshal(respBody, &extractInitResp); err != nil {
		return nil, err
	}

	extractRun, err := pollExtractRun(extractInitResp.ID, apiKey)
	if err != nil {
		return nil, err
	}

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

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

	return &PipelineResult{
		ParsedMarkdown:  markdownContent,
		ExtractedFields: extractRun.Output.Value,
	}, nil
}

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

	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintln(os.Stderr, "✗ EXTEND_API_KEY environment variable not set")
		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")
	output, _ := json.MarshalIndent(result, "", "  ")
	fmt.Printf("Output: %s\n", string(output))
}
// 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-instruction-verification.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-instruction-verification).

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-instruction-verification.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); });
import json
import os
import sys
from pathlib import Path

from extend_ai import Extend

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

client = Extend(token=API_KEY)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "wire-instruction-verification.json"


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


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


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():
    state = load_state()
    print(f'Deploying "{WORKFLOW["name"]}…')

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(workflow_id, steps=WORKFLOW["steps"])
    else:
        existing_id = None
        try:
            workflows = client.workflows.list(name=WORKFLOW["name"])
            items = workflows.data if hasattr(workflows, "data") else []
            for item in items:
                if item.name == WORKFLOW["name"]:
                    existing_id = item.id
                    break
        except Exception:
            pass

        if existing_id:
            state["workflowId"] = existing_id
            save_state(state)
            print(
                f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps'
            )
            client.workflows.update(existing_id, steps=WORKFLOW["steps"])
        else:
            created = client.workflows.create(**WORKFLOW)
            workflow_id = created.id
            if not workflow_id:
                raise ValueError("Could not read created workflow id from response")
            state["workflowId"] = workflow_id
            save_state(state)
            print(f"+ created workflow ({workflow_id})")

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

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


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// Deploy the "Wire Transfer Quick Reference Guide" pipeline to YOUR Extend account.
// Uses Extend's REST API directly (https://api.extend.ai) because Extend has no official Java SDK yet.
//
// 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-instruction-verification.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)
//   javac Provision.java && java Provision
//
// Generated by doc1 (template: wire-instruction-verification).

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.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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-instruction-verification.json");
    private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

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

    static class State {
        String workflowId;

        static State load() throws IOException {
            if (Files.exists(STATE_FILE)) {
                String json = Files.readString(STATE_FILE);
                State s = new State();
                s.workflowId = parseJsonWorkflowId(json);
                return s;
            }
            return new State();
        }

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

    static String parseJsonWorkflowId(String json) {
        int start = json.indexOf("\"workflowId\"");
        if (start == -1) return null;
        int colon = json.indexOf(':', start);
        if (colon == -1) return null;
        int quote1 = json.indexOf('"', colon);
        if (quote1 == -1) return null;
        int quote2 = json.indexOf('"', quote1 + 1);
        if (quote2 == -1) return null;
        return json.substring(quote1 + 1, quote2);
    }

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

    static Map<String, Object> api(String method, String pathName, String body) throws IOException, InterruptedException {
        HttpRequest.Builder req = HttpRequest.newBuilder()
            .uri(URI.create(API + pathName))
            .method(method, body != null ? HttpRequest.BodyPublishers.ofString(body) : HttpRequest.BodyPublishers.noBody())
            .header("Authorization", "Bearer " + API_KEY)
            .header("x-extend-api-version", VERSION);
        if (body != null) {
            req.header("Content-Type", "application/json");
        }
        HttpResponse<String> res = HTTP_CLIENT.send(req.build(), HttpResponse.BodyHandlers.ofString());
        Map<String, Object> data = new HashMap<>();
        if (!res.body().isEmpty()) {
            data = parseJsonObject(res.body());
        }
        if (res.statusCode() < 200 || res.statusCode() >= 300) {
            String errorMsg = res.body().substring(0, Math.min(300, res.body().length()));
            throw new IOException(method + " " + pathName + " failed (" + res.statusCode() + "): " + errorMsg);
        }
        return data;
    }

    static Map<String, Object> parseJsonObject(String json) {
        Map<String, Object> map = new LinkedHashMap<>();
        json = json.trim();
        if (!json.startsWith("{") || !json.endsWith("}")) return map;
        json = json.substring(1, json.length() - 1).trim();
        if (json.isEmpty()) return map;
        
        int i = 0;
        while (i < json.length()) {
            while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
            if (i >= json.length() || json.charAt(i) != '"') break;
            i++;
            int keyEnd = json.indexOf('"', i);
            if (keyEnd == -1) break;
            String key = json.substring(i, keyEnd);
            i = keyEnd + 1;
            while (i < json.length() && json.charAt(i) != ':') i++;
            if (i >= json.length()) break;
            i++;
            while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
            Object value = null;
            if (i < json.length()) {
                if (json.charAt(i) == '"') {
                    i++;
                    StringBuilder val = new StringBuilder();
                    while (i < json.length() && json.charAt(i) != '"') {
                        if (json.charAt(i) == '\\' && i + 1 < json.length()) {
                            i++;
                        }
                        val.append(json.charAt(i));
                        i++;
                    }
                    value = val.toString();
                    i++;
                } else if (json.charAt(i) == '{' || json.charAt(i) == '[') {
                    int depth = 0;
                    int start = i;
                    while (i < json.length()) {
                        if (json.charAt(i) == '{' || json.charAt(i) == '[') depth++;
                        else if (json.charAt(i) == '}' || json.charAt(i) == ']') depth--;
                        i++;
                        if (depth == 0) break;
                    }
                    value = json.substring(start, i);
                } else {
                    int start = i;
                    while (i < json.length() && json.charAt(i) != ',' && json.charAt(i) != '}') i++;
                    String token = json.substring(start, i).trim();
                    if (token.equals("null")) value = null;
                    else if (token.equals("true")) value = true;
                    else if (token.equals("false")) value = false;
                    else {
                        try {
                            value = Long.parseLong(token);
                        } catch (NumberFormatException e) {
                            value = token;
                        }
                    }
                }
            }
            map.put(key, value);
            while (i < json.length() && json.charAt(i) != ',') i++;
            if (i < json.length() && json.charAt(i) == ',') i++;
        }
        return map;
    }

    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"
            + "          }"
            + "        }"
            + "      }"
            + "    }"
            + "  ]"
            + "}";
    }

    static String buildStepsJson() {
        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}}}}"
            + "  ]}";
    }

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

            if (state.workflowId != null) {
                System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
                api("POST", "/workflows/" + state.workflowId, buildStepsJson());
            } else {
                try {
                    String query = URLEncoder.encode("Wire Transfer Quick Reference Guide Processing Pipeline", StandardCharsets.UTF_8);
                    Map<String, Object> list = api("GET", "/workflows?name=" + query, null);
                    List<?> items = (List<?>) list.getOrDefault("data", list.getOrDefault("items", List.of()));
                    for (Object item : items) {
                        if (item instanceof Map) {
                            Map<?, ?> map = (Map<?, ?>) item;
                            if ("Wire Transfer Quick Reference Guide Processing Pipeline".equals(map.get("name"))) {
                                Object id = map.get("id");
                                if (id != null) {
                                    state.workflowId = id.toString();
                                    state.save();
                                    System.out.println("✓ workflow \"Wire Transfer Quick Reference Guide Processing Pipeline\" found in your account (" + state.workflowId + ") — updating steps");
                                    api("POST", "/workflows/" + state.workflowId, buildStepsJson());
                                    break;
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    // lookup is best-effort; fall through to create
                }

                if (state.workflowId == null) {
                    Map<String, Object> created = api("POST", "/workflows", buildWorkflowJson());
                    String wfId = (String) created.get("id");
                    if (wfId == null) {
                        Map<?, ?> wf = (Map<?, ?>) created.get("workflow");
                        if (wf != null) {
                            wfId = (String) wf.get("id");
                        }
                    }
                    if (wfId == null) {
                        throw new IOException("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 e) {
                // best-effort: some accounts/plans may not require this explicit step
            }

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

        } catch (Exception e) {
            System.err.println(e.getMessage() != null ? e.getMessage() : e);
            System.exit(1);
        }
    }
}
// This script uses Extend's REST API directly because Extend has no official Go SDK yet.
// Deploy the "Wire Transfer Quick Reference Guide" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every PARSE/EXTRACT step carries its 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-instruction-verification.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)
//   go run provision.go
//
// Generated by doc1 (template: wire-instruction-verification).

package main

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

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

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

var (
	apiKey   string
	stateDir string
	stateFile string
	state    State
)

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

	wd, err := os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
		os.Exit(1)
	}
	stateDir = filepath.Join(wd, ".extend")
	stateFile = filepath.Join(stateDir, "wire-instruction-verification.json")

	// Load existing state if it exists
	if data, err := os.ReadFile(stateFile); err == nil {
		json.Unmarshal(data, &state)
	}
}

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

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

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

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

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

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

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

	return respData, nil
}

func main() {
	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,
						},
					},
				},
			},
		},
	}

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

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
			"steps": workflow["steps"],
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to reuse an existing workflow with the same name
		listResp, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflow["name"].(string))), nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := listResp["data"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			} else if data, ok := listResp["items"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			}

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

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

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

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

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

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

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

Frequently Asked Questions (FAQ)

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
    Wire Transfer Instructions ExtractorParse → Extract
    Extracts wire transfer procedures, contact info, and operational hours from banking guides.
    PDFWord / DOCXwww.extend.ai/templates/wire-transfer-instructions
  8. 08
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement