Financial & BankingParse → Extract

Contract Metadata Extractor

Extract key terms, parties, and conditions from master service agreements.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document957315164-Lilly-Master-Service-Agreement-V1-1.pdf

Step-by-step

A master service agreement is a foundational legal contract that establishes the terms, conditions, and service scope governing the ongoing relationship between a service provider and client, including party identification, effective dates, term duration, renewal conditions, and work order procedures. This template takes in Master Service Agreement and outputs markdown (.md) capturing the agreement's full text and structural layout, and JSON (.json) with extracted contract fields including party details, dates, term conditions, service scope, and termination references per the extraction schema by using Extend's Parse, Extract primitives.

Input
Master Service Agreement
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": "Master Service Agreement 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": {
              "agreement_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of agreement (e.g., Master Service Agreement)"
              },
              "effective_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Agreement effective date in YYYY-MM-DD format"
              },
              "party_one_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First party name and entity type"
              },
              "party_two_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Second party name and entity type"
              },
              "scope_of_services": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Description of services, products, or deliverables to be provided"
              },
              "renewal_notice_days": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Days notice required before expiration to renew"
              },
              "initial_term_end_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Initial term end date in YYYY-MM-DD format"
              },
              "party_one_jurisdiction": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First party jurisdiction of incorporation"
              },
              "work_order_requirement": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Whether work orders are required and execution requirements"
              },
              "initial_term_start_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Initial term start date in YYYY-MM-DD format"
              },
              "renewal_term_period_months": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Length of each renewal term in months"
              },
              "termination_section_reference": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Section reference for termination provisions"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Master Service Agreement Processing — Extend AI Skill

## What this pipeline does

This pipeline parses a Master Service Agreement (MSA) document to markdown using agentic OCR, then extracts 11 critical contract fields (parties, dates, terms, scope, renewal conditions) into structured JSON. The output is a fully parsed document ready for contract lifecycle management, obligation tracking, or legal review workflows.

## When to use this

- **Contract management systems**: Automatically ingest MSAs from email, document repositories, or deal desks into a structured contract database
- **Renewal tracking**: Extract effective dates, term lengths, and notice periods to feed automated renewal calendars
- **Legal review workflows**: Parse party names, jurisdictions, and termination references to route documents to the correct legal team
- **Obligation extraction**: Surface scope of services and work order requirements to feed downstream procurement or project management systems
- **AI-driven contract analysis**: Use the markdown output to feed RAG systems or Claude for cross-contract analysis, risk flagging, or clause comparison

## Processor pipeline

### Step 1: Parse (agentic_ocr mode, document-level chunking)
**Processor**: `parseRuns.createAndPoll()` with `blockOptions.text.agentic.enabled: true`

**Purpose**: Convert the MSA (often a multi-page PDF with tables, signatures, exhibits) into clean markdown with semantic structure preserved.

**Config choices**:
- `agentic: true` — enables vision-based OCR to handle handwritten dates, signature blocks, and complex table layouts common in executed MSAs
- `chunkingStrategy: "document"` — returns the entire agreement as one logical chunk rather than page-by-page fragments, preserving cross-reference context

**Why**: MSAs contain dense legal language across pages 5–50+ with exhibits. Agentic OCR captures tables (service schedules, SLAs) and the parsing chunks are scoped to the full document so extraction can reference any section.

### Step 2: Extract (extraction_performance + review agent + advanced multimodal)
**Processor**: `extractRuns.createAndPoll()` with Zod schema

**Purpose**: Pull 11 key contract fields into a typed JSON object for downstream systems.

**Config choices**:
- `baseProcessor: "extraction_performance"` — prioritizes accuracy over speed (OK for batch workflows, not real-time UX)
- `reviewAgent: true` — adds a second pass to validate extracted dates and party names against the document
- `advancedMultimodalEnabled: true` — uses vision to locate and verify signatures, execution dates, and party information in embedded images (sponsor logos, signature pages)

**Why**: MSAs are high-stakes documents; a misread party name or renewal date can break automation. The review agent catches OCR drift on dates (e.g., "Jan 1, 2025" vs "1 January 2025"). Multimodal handles signature blocks and letterheads that carry legal weight.

---

## TypeScript implementation



---

## CLI equivalent

```bash
# Step 1: Parse MSA to markdown
extend parse msa.pdf \
  --block-options '{"text":{"agentic":{"enabled":true}}}' \
  --chunking-strategy '{"type":"document"}' \
  > msa_parsed.md

# Step 2: Extract fields using schema file
extend extract msa.pdf \
  --schema msa_schema.json \
  --base-processor extraction_performance \
  --review-agent \
  --advanced-multimodal \
  > msa_extracted.json
```

**Schema file** (`msa_schema.json`):
```json
{
  "type": "object",
  "properties": {
    "agreement_type": {
      "type": ["string", "null"],
      "description": "Type of agreement (e.g., Master Service Agreement)"
    },
    "effective_date": {
      "type": ["string", "null"],
      "description": "Agreement effective date in YYYY-MM-DD format"
    },
    "party_one_name": {
      "type": ["string", "null"],
      "description": "First party name and entity type"
    },
    "party_one_jurisdiction": {
      "type": ["string", "null"],
      "description": "First party jurisdiction of incorporation"
    },
    "party_two_name": {
      "type": ["string", "null"],
      "description": "Second party name and entity type"
    },
    "initial_term_start_date": {
      "type": ["string", "null"],
      "description": "Initial term start date in YYYY-MM-DD format"
    },
    "initial_term_end_date": {
      "type": ["string", "null"],
      "description": "Initial term end date in YYYY-MM-DD format"
    },
    "renewal_term_period_months": {
      "type": ["string", "null"],
      "description": "Length of each renewal term in months"
    },
    "renewal_notice_days": {
      "type": ["string", "null"],
      "description": "Days notice required before expiration to renew"
    },
    "scope_of_services": {
      "type": ["string", "null"],
      "description": "Description of services, products, or deliverables to be provided"
    },
    "work_order_requirement": {
      "type": ["string", "null"],
      "description": "Whether work orders are required and execution requirements"
    },
    "termination_section_reference": {
      "type": ["string", "null"],
      "description": "Section reference for termination provisions"
    }
  }
}
```

---

## Schema

The extraction schema captures 11 mission-critical MSA fields:

```json
{
  "type": "object",
  "properties": {
    "agreement_type": {
      "type": ["string", "null"],
      "description": "Type of agreement (e.g., Master Service Agreement). Usually appears in the title or preamble; confirms this is an MSA and not a Statement of Work or Service Level Agreement."
    },
    "effective_date": {
      "type": ["string", "null"],
      "description": "Agreement effective date in YYYY-MM-DD format. Legal start date of the contract; often differs from signature date or execution date. Critical for compliance, statute of limitations, and warranty periods."
    },
    "party_one_name": {
      "type": ["string", "null"],
      "description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation'). The service provider or vendor. Include entity type and state of incorporation to validate legal standing."
    },
    "party_one_jurisdiction": {
      "type": ["string", "null"],
      "description": "First party jurisdiction of incorporation (e.g., 'Delaware'). Used for disputes, tax treatment, and regulatory applicability. Often found in definitions or signature blocks."
    },
    "party_two_name": {
      "type": ["string", "null"],
      "description": "Second party name and entity type (e.g., 'TechCorp Inc., a California corporation'). The client or buyer. Extract full legal name and entity type."
    },
    "initial_term_start_date": {
      "type": ["string", "null"],
      "description": "Initial contract term start date in YYYY-MM-DD format. When service delivery obligations begin. Used to calculate contract age and renewal eligibility."
    },
    "initial_term_end_date": {
      "type": ["string", "null"],
      "description": "Initial contract term end date in YYYY-MM-DD format. Automatic expiration date unless renewed. Single largest trigger for renewal workflows; use this to feed calendar systems."
    },
    "renewal_term_period_months": {
      "type": ["string", "null"],
      "description": "Length of each automatic renewal term in months (e.g., '12', '24', '36'). If initial term is 3 years but renewal is 1 year, capture '12'. Essential for predicting future term dates."
    },
    "renewal_notice_days": {
      "type": ["string", "null"],
      "description": "Days notice required before expiration to renew or terminate (e.g., '30', '60', '90'). Calculate alert date as: initial_term_end_date minus renewal_notice_days. Missing this = missed opt-out windows."
    },
    "scope_of_services": {
      "type": ["string", "null"],
      "description": "Description of services, products, or deliverables to be provided. Often in 'Scope of Work', 'Services', or 'Deliverables' section; may reference an attached exhibit or Schedule A. Capture the summary or reference (e.g., 'As detailed in Schedule A', 'Cloud hosting, 24/7 support, SLA in Exhibit 1')."
    },
    "work_order_requirement": {
      "type": ["string", "null"],
      "description": "Whether work orders, Statements of Work (SOWs), or Purchase Orders (POs) are required to execute services. Capture the mechanism (e.g., 'All services require a signed SOW', 'Work orders not required; services auto-execute', 'Services under this MSA governed by separate SOW'). Affects how to trigger fulfillment."
    },
    "termination_section_reference": {
      "type": ["string", "null"],
      "description": "Section number and/or heading where termination provisions are defined (e.g., 'Section 8.2 – Termination', 'Article 7: Term and Termination'). Lets legal teams quickly locate termination clauses (for cause, without cause, convenience, wind-down obligations). Format: 'Section X.X' or 'Article N'."
    }
  }
}
```

**Field rationale:**
- **Dates** (effective_date, initial_term_start_date, initial_term_end_date, renewal_notice_days) are the #1 accuracy driver because they feed automated calendars. Use `extendDate()` to normalize.
- **Parties** must include entity type and jurisdiction to validate legal capacity and dispute resolution venue.
- **Scope** can be a summary or reference; extraction systems often can't parse 50-page exhibits, so capturing "See Schedule A" is acceptable and useful.
- **Work order requirement** determines downstream execution logic; missing this causes fulfillment workflows to fail.
- **Termination section reference** is a pointer, not a summary, because legal teams need direct navigation to the exact clause.

---

## Accuracy tips

1.
import { ExtendClient, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";

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

/**
 * Process a Master Service Agreement: parse to markdown, extract key fields.
 * @param filePath - Path to local MSA PDF file
 */
export async function processMasterServiceAgreement(filePath: string) {
  // Convert local file to data URL for SDK
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;

  console.log(`[MSA Pipeline] Processing: ${filePath}`);

  // ============================================================================
  // STEP 1: Parse MSA to markdown with agentic OCR
  // ============================================================================
  console.log("[1/2] Parsing MSA document to markdown...");
  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: ${parseRun.status}`);
  }

  // Collect markdown from all chunks
  const markdown = parseRun.output.chunks
    .map((chunk) => chunk.content)
    .join("\n\n");

  console.log(
    `[1/2] ✓ Parsed ${parseRun.output.chunks.length} chunk(s), ${markdown.length} characters`
  );

  // ============================================================================
  // STEP 2: Extract MSA fields into structured JSON
  // ============================================================================
  console.log("[2/2] Extracting MSA contract fields...");

  // Define Zod schema for MSA fields
  const msaSchema = z.object({
    agreement_type: z
      .string()
      .nullable()
      .describe(
        "Type of agreement, typically 'Master Service Agreement' or 'MSA'"
      ),
    effective_date: extendDate()
      .nullable()
      .describe(
        "Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins"
      ),
    party_one_name: z
      .string()
      .nullable()
      .describe(
        "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')"
      ),
    party_one_jurisdiction: z
      .string()
      .nullable()
      .describe(
        "First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution"
      ),
    party_two_name: z
      .string()
      .nullable()
      .describe(
        "Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')"
      ),
    initial_term_start_date: extendDate()
      .nullable()
      .describe(
        "Initial contract term start date in YYYY-MM-DD format; when service delivery begins"
      ),
    initial_term_end_date: extendDate()
      .nullable()
      .describe(
        "Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed"
      ),
    renewal_term_period_months: z
      .string()
      .nullable()
      .describe(
        "Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration"
      ),
    renewal_notice_days: z
      .string()
      .nullable()
      .describe(
        "Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts"
      ),
    scope_of_services: z
      .string()
      .nullable()
      .describe(
        "Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits"
      ),
    work_order_requirement: z
      .string()
      .nullable()
      .describe(
        "Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')"
      ),
    termination_section_reference: z
      .string()
      .nullable()
      .describe(
        "Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract"
      ),
  });

  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: msaSchema,
      advancedOptions: {
        reviewAgent: {
          enabled: true,
        },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const extracted = extractRun.output.value;
  console.log(`[2/2] ✓ Extracted ${Object.keys(extracted).length} fields`);

  // ============================================================================
  // Output results
  // ============================================================================
  console.log("\n--- MSA Processing Complete ---\n");

  console.log("📄 PARSED MARKDOWN (first 500 chars):");
  console.log(markdown.substring(0, 500) + (markdown.length > 500 ? "..." : ""));

  console.log("\n📋 EXTRACTED FIELDS:");
  console.log(JSON.stringify(extracted, null, 2));

  return {
    markdown,
    extracted,
    parseRun,
    extractRun,
  };
}

// Invoke if run directly
processMasterServiceAgreement(process.argv[2] || "msa.pdf").catch(console.error);
import os
import base64
from extend_ai import Extend
from typing import Optional

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


async def process_master_service_agreement(file_path: str):
    """
    Process a Master Service Agreement: parse to markdown, extract key fields.
    
    Args:
        file_path: Path to local MSA PDF file
    """
    # Convert local file to data URL for SDK
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    data_url = f"data:application/octet-stream;base64,{base64.b64encode(file_buffer).decode('utf-8')}"

    print(f"[MSA Pipeline] Processing: {file_path}")

    # ============================================================================
    # STEP 1: Parse MSA to markdown with agentic OCR
    # ============================================================================
    print("[1/2] Parsing MSA document to markdown...")
    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: {parse_run.status}")

    # Collect markdown from all chunks
    markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)

    print(
        f"[1/2] ✓ Parsed {len(parse_run.output.chunks)} chunk(s), {len(markdown)} characters"
    )

    # ============================================================================
    # STEP 2: Extract MSA fields into structured JSON
    # ============================================================================
    print("[2/2] Extracting MSA contract fields...")

    # Define schema for MSA fields
    msa_schema = {
        "type": "object",
        "properties": {
            "agreement_type": {
                "type": ["string", "null"],
                "description": "Type of agreement, typically 'Master Service Agreement' or 'MSA'",
            },
            "effective_date": {
                "type": ["string", "null"],
                "description": "Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins",
            },
            "party_one_name": {
                "type": ["string", "null"],
                "description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')",
            },
            "party_one_jurisdiction": {
                "type": ["string", "null"],
                "description": "First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution",
            },
            "party_two_name": {
                "type": ["string", "null"],
                "description": "Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')",
            },
            "initial_term_start_date": {
                "type": ["string", "null"],
                "description": "Initial contract term start date in YYYY-MM-DD format; when service delivery begins",
            },
            "initial_term_end_date": {
                "type": ["string", "null"],
                "description": "Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed",
            },
            "renewal_term_period_months": {
                "type": ["string", "null"],
                "description": "Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration",
            },
            "renewal_notice_days": {
                "type": ["string", "null"],
                "description": "Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts",
            },
            "scope_of_services": {
                "type": ["string", "null"],
                "description": "Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits",
            },
            "work_order_requirement": {
                "type": ["string", "null"],
                "description": "Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')",
            },
            "termination_section_reference": {
                "type": ["string", "null"],
                "description": "Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract",
            },
        },
    }

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

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

    extracted = extract_run.output.value
    print(f"[2/2] ✓ Extracted {len(extracted)} fields")

    # ============================================================================
    # Output results
    # ============================================================================
    print("\n--- MSA Processing Complete ---\n")

    print("📄 PARSED MARKDOWN (first 500 chars):")
    preview = markdown[:500] + ("..." if len(markdown) > 500 else "")
    print(preview)

    print("\n📋 EXTRACTED FIELDS:")
    import json
    print(json.dumps(extracted, indent=2))

    return {
        "markdown": markdown,
        "extracted": extracted,
        "parse_run": parse_run,
        "extract_run": extract_run,
    }


# Invoke if run directly
if __name__ == "__main__":
    import sys
    import asyncio

    file_arg = sys.argv[1] if len(sys.argv) > 1 else "msa.pdf"
    asyncio.run(process_master_service_agreement(file_arg))
// Note: This code uses the Extend REST API directly (https://api.extend.ai) because
// Extend does not publish an official Java SDK. It calls the same endpoints and
// uses the same JSON shapes as the TypeScript reference. Uses only java.net.http.HttpClient
// and the built-in java.util.* classes — no external dependencies.

import java.io.*;
import java.net.http.*;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
import javax.json.*;
import javax.json.stream.JsonParser;

public class MasterServiceAgreementProcessor {

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

    /**
     * Process a Master Service Agreement: parse to markdown, extract key fields.
     * @param filePath - Path to local MSA PDF file
     */
    public static Map<String, Object> processMasterServiceAgreement(String filePath) 
            throws IOException, InterruptedException {
        
        // Convert local file to data URL
        byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
        String base64Content = Base64.getEncoder().encodeToString(fileBuffer);
        String dataUrl = "data:application/octet-stream;base64," + base64Content;

        System.out.println("[MSA Pipeline] Processing: " + filePath);

        // ========================================================================
        // STEP 1: Parse MSA to markdown with agentic OCR
        // ========================================================================
        System.out.println("[1/2] Parsing MSA document to markdown...");
        
        String parsePayload = buildParsePayload(dataUrl);
        String parseRunId = createAndPollParseRun(parsePayload);
        
        Map<String, Object> parseResult = getParseRunResult(parseRunId);
        
        if (!"PROCESSED".equals(parseResult.get("status"))) {
            throw new RuntimeException("Parse failed: " + parseResult.get("status"));
        }

        List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseResult.get("chunks");
        StringBuilder markdownBuilder = new StringBuilder();
        for (Map<String, Object> chunk : chunks) {
            if (markdownBuilder.length() > 0) {
                markdownBuilder.append("\n\n");
            }
            markdownBuilder.append(chunk.get("content"));
        }
        String markdown = markdownBuilder.toString();

        System.out.println("[1/2] ✓ Parsed " + chunks.size() + " chunk(s), " + 
                          markdown.length() + " characters");

        // ========================================================================
        // STEP 2: Extract MSA fields into structured JSON
        // ========================================================================
        System.out.println("[2/2] Extracting MSA contract fields...");
        
        String extractPayload = buildExtractPayload(dataUrl);
        String extractRunId = createAndPollExtractRun(extractPayload);
        
        Map<String, Object> extractResult = getExtractRunResult(extractRunId);
        
        if (!"PROCESSED".equals(extractResult.get("status"))) {
            throw new RuntimeException("Extraction failed: " + extractResult.get("status"));
        }

        Map<String, Object> extracted = (Map<String, Object>) extractResult.get("value");
        System.out.println("[2/2] ✓ Extracted " + extracted.size() + " fields");

        // ========================================================================
        // Output results
        // ========================================================================
        System.out.println("\n--- MSA Processing Complete ---\n");
        
        System.out.println("📄 PARSED MARKDOWN (first 500 chars):");
        String preview = markdown.substring(0, Math.min(500, markdown.length()));
        System.out.println(preview + (markdown.length() > 500 ? "..." : ""));
        
        System.out.println("\n📋 EXTRACTED FIELDS:");
        System.out.println(prettyPrintJson(extracted));

        Map<String, Object> result = new HashMap<>();
        result.put("markdown", markdown);
        result.put("extracted", extracted);
        result.put("parseRun", parseResult);
        result.put("extractRun", extractResult);
        
        return result;
    }

    private static String buildParsePayload(String dataUrl) {
        return "{"
            + "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
            + "\"config\": {"
            + "  \"blockOptions\": {"
            + "    \"text\": {\"agentic\": {\"enabled\": true}}"
            + "  },"
            + "  \"chunkingStrategy\": {\"type\": \"document\"}"
            + "}"
            + "}";
    }

    private static String buildExtractPayload(String dataUrl) {
        return "{"
            + "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
            + "\"config\": {"
            + "  \"schema\": {"
            + "    \"type\": \"object\","
            + "    \"properties\": {"
            + "      \"agreement_type\": {\"type\": [\"string\", \"null\"], \"description\": \"Type of agreement, typically 'Master Service Agreement' or 'MSA'\"},"
            + "      \"effective_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins\"},"
            + "      \"party_one_name\": {\"type\": [\"string\", \"null\"], \"description\": \"First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')\"},"
            + "      \"party_one_jurisdiction\": {\"type\": [\"string\", \"null\"], \"description\": \"First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution\"},"
            + "      \"party_two_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Second party name and entity type; the other contracting party\"},"
            + "      \"initial_term_start_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial contract term start date in YYYY-MM-DD format; when service delivery begins\"},"
            + "      \"initial_term_end_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed\"},"
            + "      \"renewal_term_period_months\": {\"type\": [\"string\", \"null\"], \"description\": \"Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration\"},"
            + "      \"renewal_notice_days\": {\"type\": [\"string\", \"null\"], \"description\": \"Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts\"},"
            + "      \"scope_of_services\": {\"type\": [\"string\", \"null\"], \"description\": \"Description of services, products, or deliverables to be provided; defines what the vendor must deliver\"},"
            + "      \"work_order_requirement\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether work orders or purchase orders are required to execute services\"},"
            + "      \"termination_section_reference\": {\"type\": [\"string\", \"null\"], \"description\": \"Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7')\""
            + "    }"
            + "  },"
            + "  \"advancedOptions\": {"
            + "    \"reviewAgent\": {\"enabled\": true},"
            + "    \"advancedMultimodalEnabled\": true"
            + "  }"
            + "}"
            + "}";
    }

    private static String createAndPollParseRun(String payload) 
            throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(EXTEND_API_BASE + "/parse-runs"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        Map<String, Object> data = parseJsonResponse(response.body());
        return (String) data.get("id");
    }

    private static String createAndPollExtractRun(String payload) 
            throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(EXTEND_API_BASE + "/extract-runs"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        Map<String, Object> data = parseJsonResponse(response.body());
        return (String) data.get("id");
    }

    private static Map<String, Object> getParseRunResult(String runId) 
            throws IOException, InterruptedException {
        while (true) {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(EXTEND_API_BASE + "/parse-runs/" + runId))
                .header("Authorization", "Bearer " + API_KEY)
                .GET()
                .build();
            
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            Map<String, Object> data = parseJsonResponse(response.body());
            
            String status = (String) data.get("status");
            if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
                Map<String, Object> output = (Map<String, Object>) data.get("output");
                Map<String, Object> result = new HashMap<>(data);
                result.put("chunks", output.get("chunks"));
                return result;
            }
            
            Thread.sleep(2000);
        }
    }

    private static Map<String, Object> getExtractRunResult(String runId) 
            throws IOException, InterruptedException {
        while (true) {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(EXTEND_API_BASE + "/extract-runs/" + runId))
                .header("Authorization", "Bearer " + API_KEY)
                .GET()
                .build();
            
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            Map<String, Object> data = parseJsonResponse(response.body());
            
            String status = (String) data.get("status");
            if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
                return data;
            }
            
            Thread.sleep(2000);
        }
    }

    private static Map<String, Object> parseJsonResponse(String jsonStr) {
        // Simple JSON parser using built-in regex and string operations
        Map<String, Object> result = new HashMap<>();
        
        // Parse basic fields (simplified for demonstration)
        if (jsonStr.contains("\"id\"")) {
            String id = extractJsonString(jsonStr, "id");
            if (id != null) result.put("id", id);
        }
        if (jsonStr.contains("\"status\"")) {
            String status = extractJsonString(jsonStr, "status");
            if (status != null) result.put("status", status);
        }
        if (jsonStr.contains("\"output\"")) {
            result.put("output", parseNestedObject(jsonStr, "output"));
        }
        if (jsonStr.contains("\"value\"")) {
            result.put("value", parseNestedObject(jsonStr, "value"));
        }
        
        return result;
    }

    private static String extractJsonString(String json, String key) {
        String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"";
        int idx = json.indexOf("\"" + key + "\"");
        if (idx == -1) return null;
        
        int colonIdx = json.indexOf(":", idx);
        int quoteIdx = json.indexOf("\"", colonIdx);
        int endQuoteIdx = json.indexOf("\"", quoteIdx + 1);
        
        if (endQuoteIdx == -1) return null;
        return json.substring(quoteIdx + 1, endQuoteIdx);
    }

    private static Map<String, Object> parseNestedObject(String json, String key) {
        Map<String, Object> obj = new HashMap<>();
        
        if ("output".equals(key)) {
            List<Map<String, Object>> chunks = new ArrayList<>();
            int chunkStart = json.indexOf("\"chunks\"");
            if (chunkStart != -1) {
                int arrayStart = json.indexOf("[", chunkStart);
                int depth = 0;
                int i = arrayStart;
                while (i < json.length()) {
                    if (json.charAt(i) == '[') depth++;
                    if (json.charAt(i) == ']') {
                        depth--;
                        if (depth == 0) {
                            String chunkJson = json.substring(arrayStart + 1, i);
                            chunks = parseChunksArray(chunkJson);
                            break;
                        }
                    }
                    i++;
                }
            }
            obj.put("chunks", chunks);
        } else if ("value".equals(key)) {
            int valueStart = json.indexOf("\"value\"");
            if (valueStart != -1) {
                int objectStart = json.indexOf("{", valueStart);
                int depth = 0;
                int i = objectStart;
                while (i < json.length()) {
                    if (json.charAt(i) == '{') depth++;
                    if (json.charAt(i) == '}') {
                        depth--;
                        if (depth == 0) {
                            String valueJson = json.substring(objectStart, i + 1);
                            Map<String, Object> valueObj = parseValueObject(valueJson);
                            return valueObj;
                        }
                    }
                    i++;
                }
            }
        }
        
        return obj;
    }

    private static List<Map<String, Object>> parseChunksArray(String chunksJson) {
        List<Map<String, Object>> chunks = new ArrayList<>();
        
        int pos = 0;
        while (pos < chunksJson.length()) {
            int objStart = chunksJson.indexOf("{", pos);
            if (objStart == -1) break;
            
            int depth = 0;
            int i = objStart;
            while (i < chunksJson.length()) {
                if (chunksJson.charAt(i) == '{') depth++;
                if (chunksJson.charAt(i) == '}') {
                    depth--;
                    if (depth == 0) {
                        String objStr = chunksJson.substring(objStart, i + 1);
                        Map<String, Object> chunk = new HashMap<>();
                        
                        String content = extractJsonString(objStr, "content");
                        if (content != null) {
                            chunk.put("content", content);
                        }
                        
                        chunks.add(chunk);
                        pos = i + 1;
                        break;
                    }
                }
                i++;
            }
            if (i == chunksJson.length()) break;
        }
        
        return chunks;
    }

    private static Map<String, Object> parseValueObject(String valueJson) {
        Map<String, Object> result = new HashMap<>();
        
        String[] fields = {
            "agreement_type", "effective_date", "party_one_name", "party_one_jurisdiction",
            "party_two_name", "initial_term_start_date", "initial_term_end_date",
            "renewal_term_period_months", "renewal_notice_days", "scope_of_services",
            "work_order_requirement", "termination_section_reference"
        };
        
        for (String field : fields) {
            String value = extractJsonString(valueJson, field);
            result.put(field, value);
        }
        
        return result;
    }

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

    private static String prettyPrintJson(Map<String, Object> map) {
        StringBuilder sb = new StringBuilder("{\n");
        int count = 0;
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            sb.append("  \"").append(entry.getKey()).append("\": ");
            Object value = entry.getValue();
            if (value == null) {
                sb.append("null");
            } else if (value instanceof String) {
                sb.append("\"").append(escapeJson((String) value)).append("\"");
            } else {
                sb.append(value);
            }
            if (++count < map.size()) {
                sb.append(",");
            }
            sb.append("\n");
        }
        sb.append("}");
        return sb.toString();
    }

    public static void main(String[] args) {
        try {
            String filePath = args.length > 0 ? args[0] : "msa.pdf";
            processMasterServiceAgreement(filePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// This code calls the Extend REST API directly because Extend has no official Go SDK yet.
// The API is a thin wrapper; this mirrors the same endpoints and request/response shapes.

package main

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

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

var apiKey string

func init() {
	apiKey = os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		log.Fatal("EXTEND_API_KEY environment variable not set")
	}
}

// ParseRunOutput holds the parsed document output.
type ParseRunOutput struct {
	Chunks []struct {
		Content string `json:"content"`
	} `json:"chunks"`
}

// ParseRun represents the result of a parse operation.
type ParseRun struct {
	Status string         `json:"status"`
	Output ParseRunOutput `json:"output"`
}

// ExtractRunOutput holds the extracted fields.
type ExtractRunOutput struct {
	Value map[string]interface{} `json:"value"`
}

// ExtractRun represents the result of an extract operation.
type ExtractRun struct {
	Status string          `json:"status"`
	Output ExtractRunOutput `json:"output"`
}

// MSAFields holds the structured MSA extraction result.
type MSAFields struct {
	AgreementType               *string `json:"agreement_type"`
	EffectiveDate               *string `json:"effective_date"`
	PartyOneName                *string `json:"party_one_name"`
	PartyOneJurisdiction        *string `json:"party_one_jurisdiction"`
	PartyTwoName                *string `json:"party_two_name"`
	InitialTermStartDate        *string `json:"initial_term_start_date"`
	InitialTermEndDate          *string `json:"initial_term_end_date"`
	RenewalTermPeriodMonths     *string `json:"renewal_term_period_months"`
	RenewalNoticeDays           *string `json:"renewal_notice_days"`
	ScopeOfServices             *string `json:"scope_of_services"`
	WorkOrderRequirement        *string `json:"work_order_requirement"`
	TerminationSectionReference *string `json:"termination_section_reference"`
}

// processMasterServiceAgreement processes an MSA file: parse to markdown, extract key fields.
func processMasterServiceAgreement(filePath string) (map[string]interface{}, error) {
	// Read file and convert to base64 data URL
	fileBytes, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}
	dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64.StdEncoding.EncodeToString(fileBytes))

	fmt.Printf("[MSA Pipeline] Processing: %s\n", filePath)

	// ============================================================================
	// STEP 1: Parse MSA to markdown with agentic OCR
	// ============================================================================
	fmt.Println("[1/2] Parsing MSA document to markdown...")

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

	parseRun, err := createAndPollParseRun(parseReq)
	if err != nil {
		return nil, err
	}

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

	// Collect markdown from all chunks
	var mdParts []string
	for _, chunk := range parseRun.Output.Chunks {
		mdParts = append(mdParts, chunk.Content)
	}
	markdown := strings.Join(mdParts, "\n\n")

	fmt.Printf("[1/2] ✓ Parsed %d chunk(s), %d characters\n", len(parseRun.Output.Chunks), len(markdown))

	// ============================================================================
	// STEP 2: Extract MSA fields into structured JSON
	// ============================================================================
	fmt.Println("[2/2] Extracting MSA contract fields...")

	// Define schema for MSA fields
	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"agreement_type": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Type of agreement, typically 'Master Service Agreement' or 'MSA'",
			},
			"effective_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins",
			},
			"party_one_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')",
			},
			"party_one_jurisdiction": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution",
			},
			"party_two_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')",
			},
			"initial_term_start_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Initial contract term start date in YYYY-MM-DD format; when service delivery begins",
			},
			"initial_term_end_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed",
			},
			"renewal_term_period_months": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration",
			},
			"renewal_notice_days": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts",
			},
			"scope_of_services": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits",
			},
			"work_order_requirement": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')",
			},
			"termination_section_reference": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract",
			},
		},
	}

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

	extractRun, err := createAndPollExtractRun(extractReq)
	if err != nil {
		return nil, err
	}

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

	extracted := extractRun.Output.Value
	fmt.Printf("[2/2] ✓ Extracted %d fields\n", len(extracted))

	// ============================================================================
	// Output results
	// ============================================================================
	fmt.Println("\n--- MSA Processing Complete ---\n")

	fmt.Println("📄 PARSED MARKDOWN (first 500 chars):")
	if len(markdown) > 500 {
		fmt.Println(markdown[:500] + "...")
	} else {
		fmt.Println(markdown)
	}

	fmt.Println("\n📋 EXTRACTED FIELDS:")
	extractedJSON, _ := json.MarshalIndent(extracted, "", "  ")
	fmt.Println(string(extractedJSON))

	return map[string]interface{}{
		"markdown": markdown,
		"extracted": extracted,
	}, nil
}

// createAndPollParseRun creates a parse run and polls until completion.
func createAndPollParseRun(reqBody map[string]interface{}) (*ParseRun, error) {
	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", extendAPIBase+"/v1/parseRuns", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 5 * time.Minute}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("parse request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var run ParseRun
	json.Unmarshal(respBody, &run)

	// Poll until processed
	for run.Status != "PROCESSED" && run.Status != "FAILED" {
		time.Sleep(2 * time.Second)
		req, _ := http.NewRequest("GET", extendAPIBase+"/v1/parseRuns", nil)
		req.Header.Set("Authorization", "Bearer "+apiKey)
		resp, _ := client.Do(req)
		respBody, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		json.Unmarshal(respBody, &run)
	}

	return &run, nil
}

// createAndPollExtractRun creates an extract run and polls until completion.
func createAndPollExtractRun(reqBody map[string]interface{}) (*ExtractRun, error) {
	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", extendAPIBase+"/v1/extractRuns", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 5 * time.Minute}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("extract request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	var run ExtractRun
	json.Unmarshal(respBody, &run)

	// Poll until processed
	for run.Status != "PROCESSED" && run.Status != "FAILED" {
		time.Sleep(2 * time.Second)
		req, _ := http.NewRequest("GET", extendAPIBase+"/v1/extractRuns", nil)
		req.Header.Set("Authorization", "Bearer "+apiKey)
		resp, _ := client.Do(req)
		respBody, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		json.Unmarshal(respBody, &run)
	}

	return &run, nil
}

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

	_, err := processMasterServiceAgreement(filePath)
	if err != nil {
		log.Fatal(err)
	}
}
// Deploy the "Master Service Agreement" 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/contract-metadata-extraction.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: contract-metadata-extraction).

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, "contract-metadata-extraction.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": "Master Service Agreement 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": {
              "agreement_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of agreement (e.g., Master Service Agreement)"
              },
              "effective_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Agreement effective date in YYYY-MM-DD format"
              },
              "party_one_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First party name and entity type"
              },
              "party_two_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Second party name and entity type"
              },
              "scope_of_services": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Description of services, products, or deliverables to be provided"
              },
              "renewal_notice_days": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Days notice required before expiration to renew"
              },
              "initial_term_end_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Initial term end date in YYYY-MM-DD format"
              },
              "party_one_jurisdiction": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First party jurisdiction of incorporation"
              },
              "work_order_requirement": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Whether work orders are required and execution requirements"
              },
              "initial_term_start_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Initial term start date in YYYY-MM-DD format"
              },
              "renewal_term_period_months": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Length of each renewal term in months"
              },
              "termination_section_reference": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Section reference for termination provisions"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

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

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

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

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

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

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
#!/usr/bin/env python3
"""
Deploy the "Master Service Agreement" 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/contract-metadata-extraction.json,
so re-running updates the existing workflow instead of duplicating it.

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

Generated by doc1 (template: contract-metadata-extraction).
"""

import json
import os
import sys
from pathlib import Path
from typing import Any, Optional

from extend_ai import Extend

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

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "contract-metadata-extraction.json"


def load_state() -> dict[str, Optional[str]]:
    """Load workflow state from disk."""
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text())
    return {}


def save_state(state: dict[str, Any]) -> None:
    """Save workflow state to disk."""
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ── Workflow definition — extractor/classifier/splitter configs inline ──────
WORKFLOW = {
    "name": "Master Service Agreement 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": {
                            "agreement_type": {
                                "type": ["string", "null"],
                                "description": "Type of agreement (e.g., Master Service Agreement)",
                            },
                            "effective_date": {
                                "type": ["string", "null"],
                                "description": "Agreement effective date in YYYY-MM-DD format",
                            },
                            "party_one_name": {
                                "type": ["string", "null"],
                                "description": "First party name and entity type",
                            },
                            "party_two_name": {
                                "type": ["string", "null"],
                                "description": "Second party name and entity type",
                            },
                            "scope_of_services": {
                                "type": ["string", "null"],
                                "description": "Description of services, products, or deliverables to be provided",
                            },
                            "renewal_notice_days": {
                                "type": ["string", "null"],
                                "description": "Days notice required before expiration to renew",
                            },
                            "initial_term_end_date": {
                                "type": ["string", "null"],
                                "description": "Initial term end date in YYYY-MM-DD format",
                            },
                            "party_one_jurisdiction": {
                                "type": ["string", "null"],
                                "description": "First party jurisdiction of incorporation",
                            },
                            "work_order_requirement": {
                                "type": ["string", "null"],
                                "description": "Whether work orders are required and execution requirements",
                            },
                            "initial_term_start_date": {
                                "type": ["string", "null"],
                                "description": "Initial term start date in YYYY-MM-DD format",
                            },
                            "renewal_term_period_months": {
                                "type": ["string", "null"],
                                "description": "Length of each renewal term in months",
                            },
                            "termination_section_reference": {
                                "type": ["string", "null"],
                                "description": "Section reference for termination provisions",
                            },
                        },
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {"enabled": True},
                        "advancedMultimodalEnabled": True,
                    },
                }
            },
        },
    ],
}


def main() -> None:
    """Provision the workflow."""
    client = Extend(token=API_KEY)
    state = load_state()

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

    if state.get("workflow_id"):
        print(f'✓ workflow already provisioned ({state["workflow_id"]}) — updating steps')
        client.workflows.update(
            id=state["workflow_id"],
            steps=WORKFLOW["steps"],
        )
    else:
        # Reuse an existing workflow with the same name if one exists (e.g. a
        # previous run's state file was lost) instead of creating a duplicate.
        existing_id = None
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = getattr(workflows_list, "data", None) or getattr(workflows_list, "items", None) or []
            for workflow in items:
                if getattr(workflow, "name", None) == WORKFLOW["name"]:
                    existing_id = getattr(workflow, "id", None)
                    break
            if existing_id:
                state["workflow_id"] = existing_id
                save_state(state)
                print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps')
                client.workflows.update(
                    id=existing_id,
                    steps=WORKFLOW["steps"],
                )
        except Exception:
            # lookup is best-effort; fall through to create
            pass

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

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

    wf_id = state["workflow_id"]
    api_base = "https://api.extend.ai"
    print("\nDone. Run documents through it with:")
    print(f'  POST {api_base}/workflow_runs  {{ "workflow": {{ "id": "{wf_id}" }}, "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 "Master Service Agreement" pipeline to YOUR Extend account.
// Uses the Extend REST API directly (no official Java SDK exists 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/contract-metadata-extraction.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: contract-metadata-extraction).

import java.io.File;
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.util.HashMap;
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 String STATE_DIR = ".extend";
    private static final String STATE_FILE = "contract-metadata-extraction.json";
    private static final HttpClient CLIENT = HttpClient.newHttpClient();

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

    static class State {
        String workflowId;

        State(String workflowId) {
            this.workflowId = workflowId;
        }
    }

    private static State state;

    public static void main(String[] args) {
        try {
            loadState();
            String workflowName = "Master Service Agreement Processing Pipeline";
            System.out.println("Deploying \"" + workflowName + "\"…");

            if (state.workflowId != null && !state.workflowId.isEmpty()) {
                System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
                api("POST", "/workflows/" + state.workflowId, buildStepsJson());
            } else {
                // Reuse an existing workflow with the same name if one exists
                try {
                    String listResponse = api("GET", "/workflows?name=" + URLEncoder.encode(workflowName, StandardCharsets.UTF_8), null);
                    String workflowId = extractWorkflowIdFromList(listResponse, workflowName);
                    if (workflowId != null && !workflowId.isEmpty()) {
                        state.workflowId = workflowId;
                        saveState();
                        System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + workflowId + ") — updating steps");
                        api("POST", "/workflows/" + workflowId, buildStepsJson());
                    }
                } catch (Exception e) {
                    // lookup is best-effort; fall through to create
                }

                if (state.workflowId == null || state.workflowId.isEmpty()) {
                    String createResponse = api("POST", "/workflows", buildWorkflowJson());
                    String workflowId = extractWorkflowId(createResponse);
                    if (workflowId == null || workflowId.isEmpty()) {
                        throw new RuntimeException("Could not read created workflow id from response");
                    }
                    state.workflowId = workflowId;
                    saveState();
                    System.out.println("+ created workflow (" + workflowId + ")");
                }
            }

            // Deploy the current draft as a new version so the workflow is runnable
            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.toString());
            System.exit(1);
        }
    }

    private static String api(String method, String pathName, String body) throws IOException, InterruptedException {
        HttpRequest.Builder builder = 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) {
            builder.header("Content-Type", "application/json");
        }

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

        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            String errorBody = response.body().length() > 300 ? response.body().substring(0, 300) : response.body();
            throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorBody);
        }

        return response.body();
    }

    private static void loadState() throws IOException {
        Path statePath = Path.of(STATE_DIR, STATE_FILE);
        if (Files.exists(statePath)) {
            String json = Files.readString(statePath);
            String workflowId = extractFieldFromJson(json, "workflowId");
            state = new State(workflowId);
        } else {
            state = new State(null);
        }
    }

    private static void saveState() throws IOException {
        Files.createDirectories(Path.of(STATE_DIR));
        String json = "{\"workflowId\": \"" + escapeJson(state.workflowId) + "\"}";
        Files.writeString(Path.of(STATE_DIR, STATE_FILE), json);
    }

    private static String buildWorkflowJson() {
        return "{" +
            "\"name\": \"Master Service Agreement Processing Pipeline\"," +
            "\"steps\": " + buildStepsJson() +
            "}";
    }

    private static String buildStepsJson() {
        return "[" +
            "{\"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\": {" +
            "\"agreement_type\": {\"type\": [\"string\", \"null\"], \"description\": \"Type of agreement (e.g., Master Service Agreement)\"}," +
            "\"effective_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Agreement effective date in YYYY-MM-DD format\"}," +
            "\"party_one_name\": {\"type\": [\"string\", \"null\"], \"description\": \"First party name and entity type\"}," +
            "\"party_two_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Second party name and entity type\"}," +
            "\"scope_of_services\": {\"type\": [\"string\", \"null\"], \"description\": \"Description of services, products, or deliverables to be provided\"}," +
            "\"renewal_notice_days\": {\"type\": [\"string\", \"null\"], \"description\": \"Days notice required before expiration to renew\"}," +
            "\"initial_term_end_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial term end date in YYYY-MM-DD format\"}," +
            "\"party_one_jurisdiction\": {\"type\": [\"string\", \"null\"], \"description\": \"First party jurisdiction of incorporation\"}," +
            "\"work_order_requirement\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether work orders are required and execution requirements\"}," +
            "\"initial_term_start_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial term start date in YYYY-MM-DD format\"}," +
            "\"renewal_term_period_months\": {\"type\": [\"string\", \"null\"], \"description\": \"Length of each renewal term in months\"}," +
            "\"termination_section_reference\": {\"type\": [\"string\", \"null\"], \"description\": \"Section reference for termination provisions\"}" +
            "}}, \"baseProcessor\": \"extraction_performance\", \"advancedOptions\": {\"reviewAgent\": {\"enabled\": true}, \"advancedMultimodalEnabled\": true}}}}" +
            "]";
    }

    private static String extractWorkflowId(String json) {
        String id = extractFieldFromJson(json, "id");
        if (id == null || id.isEmpty()) {
            id = extractNestedFieldFromJson(json, "workflow", "id");
        }
        return id;
    }

    private static String extractWorkflowIdFromList(String json, String name) {
        // Simple JSON parsing for data/items array
        String data = extractFieldFromJson(json, "data");
        if (data == null || data.isEmpty()) {
            data = extractFieldFromJson(json, "items");
        }
        if (data != null && !data.isEmpty() && data.startsWith("[")) {
            int start = 0;
            while ((start = data.indexOf("{", start)) != -1) {
                int end = data.indexOf("}", start);
                if (end != -1) {
                    String item = data.substring(start, end + 1);
                    String itemName = extractFieldFromJson(item, "name");
                    if (name.equals(itemName)) {
                        return extractFieldFromJson(item, "id");
                    }
                    start = end;
                } else {
                    break;
                }
            }
        }
        return null;
    }

    private static String extractFieldFromJson(String json, String field) {
        String key = "\"" + field + "\":";
        int idx = json.indexOf(key);
        if (idx == -1) return null;

        int start = idx + key.length();
        while (start < json.length() && Character.isWhitespace(json.charAt(start))) start++;

        if (start >= json.length()) return null;

        if (json.charAt(start) == '"') {
            int end = start + 1;
            while (end < json.length() && json.charAt(end) != '"') {
                if (json.charAt(end) == '\\') end++;
                end++;
            }
            if (end < json.length()) {
                return json.substring(start + 1, end);
            }
        }
        return null;
    }

    private static String extractNestedFieldFromJson(String json, String outerField, String innerField) {
        String outerKey = "\"" + outerField + "\":";
        int idx = json.indexOf(outerKey);
        if (idx == -1) return null;

        int start = idx + outerKey.length();
        while (start < json.length() && Character.isWhitespace(json.charAt(start))) start++;

        if (start >= json.length() || json.charAt(start) != '{') return null;

        int depth = 1;
        int objEnd = start + 1;
        while (objEnd < json.length() && depth > 0) {
            if (json.charAt(objEnd) == '{') depth++;
            else if (json.charAt(objEnd) == '}') depth--;
            objEnd++;
        }

        String nested = json.substring(start, objEnd);
        return extractFieldFromJson(nested, innerField);
    }

    private static String escapeJson(String s) {
        if (s == null) return "";
        return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
    }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
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"`
}

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

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

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

type SchemaProperty struct {
	Type        interface{} `json:"type"`
	Description string      `json:"description"`
}

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

type ExtractorConfig struct {
	Schema         ExtractorSchema `json:"schema"`
	BaseProcessor  string          `json:"baseProcessor"`
	AdvancedOptions struct {
		ReviewAgent struct {
			Enabled bool `json:"enabled"`
		} `json:"reviewAgent"`
		AdvancedMultimodalEnabled bool `json:"advancedMultimodalEnabled"`
	} `json:"advancedOptions"`
}

type ParseConfig struct {
	BlockOptions struct {
		Text struct {
			Agentic struct {
				Enabled bool `json:"enabled"`
			} `json:"agentic"`
		} `json:"text"`
	} `json:"blockOptions"`
	ChunkingStrategy struct {
		Type string `json:"type"`
	} `json:"chunkingStrategy"`
}

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

type NextStep struct {
	Step string `json:"step"`
}

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

type WorkflowDefinition struct {
	Name  string         `json:"name"`
	Steps []WorkflowStep `json:"steps"`
}

type VersionRequest struct{}

var (
	apiKey    string
	stateDir  string
	stateFile string
)

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

	cwd, err := os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get current directory: %v\n", err)
		os.Exit(1)
	}
	stateDir = filepath.Join(cwd, ".extend")
	stateFile = filepath.Join(stateDir, "contract-metadata-extraction.json")
}

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

func saveState(state State) 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{}) (json.RawMessage, error) {
	url := API + pathName
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(data)
	}

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

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

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

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

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

	return respBody, nil
}

func buildWorkflow() WorkflowDefinition {
	schema := ExtractorSchema{
		Type: "object",
		Properties: map[string]SchemaProperty{
			"agreement_type": {
				Type:        []string{"string", "null"},
				Description: "Type of agreement (e.g., Master Service Agreement)",
			},
			"effective_date": {
				Type:        []string{"string", "null"},
				Description: "Agreement effective date in YYYY-MM-DD format",
			},
			"party_one_name": {
				Type:        []string{"string", "null"},
				Description: "First party name and entity type",
			},
			"party_two_name": {
				Type:        []string{"string", "null"},
				Description: "Second party name and entity type",
			},
			"scope_of_services": {
				Type:        []string{"string", "null"},
				Description: "Description of services, products, or deliverables to be provided",
			},
			"renewal_notice_days": {
				Type:        []string{"string", "null"},
				Description: "Days notice required before expiration to renew",
			},
			"initial_term_end_date": {
				Type:        []string{"string", "null"},
				Description: "Initial term end date in YYYY-MM-DD format",
			},
			"party_one_jurisdiction": {
				Type:        []string{"string", "null"},
				Description: "First party jurisdiction of incorporation",
			},
			"work_order_requirement": {
				Type:        []string{"string", "null"},
				Description: "Whether work orders are required and execution requirements",
			},
			"initial_term_start_date": {
				Type:        []string{"string", "null"},
				Description: "Initial term start date in YYYY-MM-DD format",
			},
			"renewal_term_period_months": {
				Type:        []string{"string", "null"},
				Description: "Length of each renewal term in months",
			},
			"termination_section_reference": {
				Type:        []string{"string", "null"},
				Description: "Section reference for termination provisions",
			},
		},
	}

	extractorCfg := ExtractorConfig{
		Schema:        schema,
		BaseProcessor: "extraction_performance",
	}
	extractorCfg.AdvancedOptions.ReviewAgent.Enabled = true
	extractorCfg.AdvancedOptions.AdvancedMultimodalEnabled = true

	parseCfg := ParseConfig{}
	parseCfg.BlockOptions.Text.Agentic.Enabled = true
	parseCfg.ChunkingStrategy.Type = "document"

	steps := []WorkflowStep{
		{
			Name: "startTrigger1",
			Type: "TRIGGER",
			Next: []NextStep{
				{Step: "parse1"},
			},
		},
		{
			Name: "parse1",
			Type: "PARSE",
			Config: &StepConfig{
				ParseConfig: &parseCfg,
			},
			Next: []NextStep{
				{Step: "extraction2"},
			},
		},
		{
			Name: "extraction2",
			Type: "EXTRACT",
			Config: &StepConfig{
				ExtractorConfig: &extractorCfg,
			},
		},
	}

	return WorkflowDefinition{
		Name:  "Master Service Agreement Processing Pipeline",
		Steps: steps,
	}
}

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

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

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		updateBody := map[string]interface{}{
			"steps": workflow.Steps,
		}
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), updateBody)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to find existing workflow by name
		query := url.QueryEscape(workflow.Name)
		respData, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", query), nil)
		found := false
		if err == nil {
			var listResp WorkflowListResponse
			if err := json.Unmarshal(respData, &listResp); err == nil {
				items := listResp.Data
				if len(items) == 0 {
					items = listResp.Items
				}
				for _, item := range items {
					if item.Name == workflow.Name && item.ID != "" {
						state.WorkflowID = item.ID
						_ = saveState(state)
						fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, item.ID)
						updateBody := map[string]interface{}{
							"steps": workflow.Steps,
						}
						_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", item.ID), updateBody)
						if err != nil {
							fmt.Fprintf(os.Stderr, "%v\n", err)
							os.Exit(1)
						}
						found = true
						break
					}
				}
			}
		}

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

			var wfResp WorkflowResponse
			if err := json.Unmarshal(respData, &wfResp); err != nil {
				fmt.Fprintf(os.Stderr, "Failed to parse workflow response: %v\n", err)
				os.Exit(1)
			}

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

			state.WorkflowID = wfID
			if err := saveState(state); err != nil {
				fmt.Fprintf(os.Stderr, "Failed to save state: %v\n", err)
				os.Exit(1)
			}
			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), VersionRequest{})

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

Frequently Asked Questions (FAQ)

Use the `agentic_ocr` parse mode to convert the full document to markdown, then craft extraction field descriptions that explicitly exclude signature blocks, exhibits, and standard clauses (e.g., 'Liability cap in Section 5, numeric value only, ignore legal qualifiers'). The description is the primary accuracy lever—be specific about what you want and what to skip.
Use async (`parseRuns.createAndPoll`) for production MSAs—sync extraction has a ~10-page practical limit. Async mode scales to 100+ pages and handles complex numbering schemes without additional cost beyond processing time.
Extraction confidence for legal terms typically ranges 0.75–0.95 depending on how clearly written the original MSA is; always implement human review for payment terms and termination conditions before automation.
For MSAs with multiple exhibits (schedules, pricing tables, SLAs), use split first to isolate the main agreement from attachments, then extract each part with a targeted schema—this improves accuracy and lets you route exhibits to specialized extraction (e.g., tables for pricing, compliance clauses for SLAs). If the document is a single cohesive agreement, skip split and extract end-to-end.
Tags
Master Service AgreementContract ManagementLegal TermsService Engagement
About this template

A service contract establishes the foundational terms and conditions governing the relationship between a service provider and client. This template captures critical information including party identification, effective dates, term length, renewal conditions, work order procedures, and exhibits defining deliverables and service scope.

Document formats
  • PDF
  • Word / DOCX
Requirements
  • Complex layouts

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