LegalParse → Extract

Legal Contract Extractor

Extracts key terms from standard legal agreements and fee arrangements.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document371424596-Contract-for-Legal-Services.pdf

Step-by-step

A legal commercial contract establishes the scope of services, fee structures (such as retainer amounts or hourly rates), cost reimbursement terms, and conditions for termination of the engagement. This template takes in contracts and outputs markdown (.md) capturing the agreement's full text and layout, and JSON (.json) with structured contract fields including parties, signatures, payment terms, termination conditions, and other essential provisions per the extraction schema by using Extend's Parse, Extract primitives.

Input
contracts
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 — 16 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": "Legal Contract Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "required": [
              "parties",
              "signatures",
              "governing_law",
              "payment_terms",
              "renewal_terms",
              "term_end_date",
              "agreement_date",
              "notices_clause",
              "agreement_title",
              "term_start_date",
              "liability_clause",
              "termination_clause",
              "definitions_section",
              "confidentiality_clause",
              "dispute_resolution_clause",
              "agreement_reference_number"
            ],
            "properties": {
              "parties": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "party_name",
                    "party_role",
                    "party_address"
                  ],
                  "properties": {
                    "party_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The full legal name of the party to the agreement. This may be a company, organization, or individual."
                    },
                    "party_role": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'."
                    },
                    "party_address": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee')."
              },
              "signatures": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "signatory_name",
                    "signature_date",
                    "signatory_party",
                    "signatory_title"
                  ],
                  "properties": {
                    "signatory_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The name of the individual signing the agreement on behalf of a party."
                    },
                    "signature_date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The date on which the agreement was signed by this signatory.",
                      "extend:type": "date"
                    },
                    "signatory_party": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The party on whose behalf the individual is signing."
                    },
                    "signatory_title": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The title or position of the signatory within the organization, if applicable."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed."
              },
              "governing_law": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'."
              },
              "payment_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar."
              },
              "renewal_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements."
              },
              "term_end_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent.",
                "extend:type": "date"
              },
              "agreement_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations.",
                "extend:type": "date"
              },
              "notices_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing."
              },
              "agreement_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'."
              },
              "term_start_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context.",
                "extend:type": "date"
              },
              "liability_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps."
              },
              "termination_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination."
              },
              "definitions_section": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'."
              },
              "confidentiality_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar."
              },
              "dispute_resolution_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings."
              },
              "agreement_reference_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
              }
            },
            "additionalProperties": false
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Legal Contract Processing — Extend AI Skill

## What this pipeline does

This pipeline ingests legal service contracts (attorney-client agreements, retainer terms, etc.) and extracts all critical contract elements into structured JSON. It parses the document to markdown using agentic OCR for layout handling, then extracts 16 key fields including parties, signatures, dates, payment terms, liability clauses, and dispute resolution procedures. Output is a fully typed object ready for contract management systems, compliance audits, or downstream legal workflows.

## When to use this

- **Contract intake automation**: Law firms receiving signed agreements need instant extraction of parties, dates, and payment terms into a contract database without manual data entry.
- **Contract compliance review**: In-house legal teams must verify governing law, liability caps, and termination clauses match company policy before execution.
- **Legal workspace setup**: Extract signatory dates and party roles to automatically trigger onboarding workflows, billing system configuration, and document filing.
- **Portfolio analysis**: Legal departments analyzing multiple contracts need structured party, term, and payment data for reporting and risk assessment.
- **Renewal management**: Extract term_start_date, term_end_date, and renewal_terms to feed an automated renewal calendar or alert system.

## Processor pipeline

### Step 1: Parse (agentic_ocr mode)
**Processor**: `parse` with `extraction_performance` engine  
**Purpose**: Convert PDF pages to markdown chunks, handling complex legal formatting, signature blocks, and multi-column layouts.  
**Key config**:
- `blockOptions.text.agentic.enabled: true` — enables layout-aware parsing for structured legal text
- `chunkingStrategy.type: "document"` — preserves document-level context instead of breaking at page boundaries
- Engine: `extraction_performance` — balances speed and accuracy for dense legal text

**Why**: Legal contracts have intricate formatting (signature blocks, definitions, multi-part clauses). Agentic parsing respects logical document structure, improving downstream extraction accuracy by 15–25%.

### Step 2: Extract (schema-driven)
**Processor**: `extract` with `extraction_performance` baseProcessor  
**Purpose**: Pull 16 contract fields into JSON using a detailed Zod schema with field descriptions and array handling for parties and signatures.  
**Key config**:
- `advancedOptions.reviewAgent.enabled: true` — agent reviews and corrects extraction errors
- `advancedMultimodalEnabled: true` — handles signature images and metadata
- `baseProcessor: "extraction_performance"` — optimized for dense, structured legal documents

**Why**: Legal contracts demand high accuracy on dates, party names, and liability language. Review agent catches hallucinations (e.g., wrong signatory dates). Multimodal processing handles signature block images.

---

## TypeScript implementation



---

## CLI equivalent

```bash
# Step 1: Parse contract to markdown (agentic mode for layout handling)
extend parse contract.pdf \
  --block-options-text-agentic-enabled true \
  --chunking-strategy-type document

# Step 2: Extract contract fields using the schema (review agent enabled)
extend extract contract.pdf \
  --schema legal_contract_schema.json \
  --base-processor extraction_performance \
  --review-agent-enabled true \
  --advanced-multimodal-enabled true
```

**Schema file** (`legal_contract_schema.json`):
```json
{
  "type": "object",
  "properties": {
    "agreement_title": {
      "type": ["string", "null"],
      "description": "The official title or heading of the legal agreement or contract."
    },
    "agreement_reference_number": {
      "type": ["string", "null"],
      "description": "A unique identifier or reference number assigned to this agreement or contract."
    },
    "agreement_date": {
      "type": ["string", "null"],
      "extend:type": "date",
      "description": "The effective date when the agreement becomes valid or enforceable."
    },
    "term_start_date": {
      "type": ["string", "null"],
      "extend:type": "date",
      "description": "The date on which the contractual obligations begin."
    },
    "term_end_date": {
      "type": ["string", "null"],
      "extend:type": "date",
      "description": "The date on which the agreement expires or terminates."
    },
    "parties": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "party_name": {
            "type": ["string", "null"],
            "description": "The full legal name of the party to the agreement."
          },
          "party_role": {
            "type": ["string", "null"],
            "description": "The role or designation of the party within the agreement."
          },
          "party_address": {
            "type": ["string", "null"],
            "description": "The official address of the party as stated in the agreement."
          }
        },
        "required": ["party_name", "party_role", "party_address"]
      },
      "description": "The entities or individuals entering into the agreement."
    },
    "signatures": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "signatory_name": {
            "type": ["string", "null"],
            "description": "The name of the individual signing the agreement."
          },
          "signature_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The date on which the agreement was signed by this signatory."
          },
          "signatory_party": {
            "type": ["string", "null"],
            "description": "The party on whose behalf the individual is signing."
          },
          "signatory_title": {
            "type": ["string", "null"],
            "description": "The title or position of the signatory within the organization."
          }
        },
        "required": ["signatory_name", "signature_date", "signatory_party", "signatory_title"]
      },
      "description": "The signatures or signature blocks of the parties to the agreement."
    },
    "payment_terms": {
      "type":
import { ExtendClient, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";

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

/**
 * Zod schema for legal contract extraction.
 * All string fields are nullable (contracts may omit sections).
 * Dates use extendDate() helper for ISO format (yyyy-mm-dd).
 * Arrays (parties, signatures) are NOT nullable but may be empty.
 */
const LegalContractSchema = z.object({
  agreement_title: z.string().nullable().describe(
    "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'."
  ),
  agreement_reference_number: z.string().nullable().describe(
    "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
  ),
  agreement_date: extendDate().describe(
    "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations."
  ),
  term_start_date: extendDate().describe(
    "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context."
  ),
  term_end_date: extendDate().describe(
    "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent."
  ),
  parties: z.array(
    z.object({
      party_name: z.string().nullable().describe(
        "The full legal name of the party to the agreement. This may be a company, organization, or individual."
      ),
      party_role: z.string().nullable().describe(
        "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'."
      ),
      party_address: z.string().nullable().describe(
        "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country."
      ),
    })
  ).describe(
    "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee')."
  ),
  signatures: z.array(
    z.object({
      signatory_name: z.string().nullable().describe(
        "The name of the individual signing the agreement on behalf of a party."
      ),
      signature_date: extendDate().describe(
        "The date on which the agreement was signed by this signatory."
      ),
      signatory_party: z.string().nullable().describe(
        "The party on whose behalf the individual is signing."
      ),
      signatory_title: z.string().nullable().describe(
        "The title or position of the signatory within the organization, if applicable."
      ),
    })
  ).describe(
    "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed."
  ),
  payment_terms: z.string().nullable().describe(
    "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar."
  ),
  renewal_terms: z.string().nullable().describe(
    "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements."
  ),
  governing_law: z.string().nullable().describe(
    "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'."
  ),
  definitions_section: z.string().nullable().describe(
    "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'."
  ),
  confidentiality_clause: z.string().nullable().describe(
    "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar."
  ),
  liability_clause: z.string().nullable().describe(
    "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps."
  ),
  termination_clause: z.string().nullable().describe(
    "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination."
  ),
  dispute_resolution_clause: z.string().nullable().describe(
    "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings."
  ),
  notices_clause: z.string().nullable().describe(
    "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing."
  ),
});

/**
 * Process a legal contract: parse to markdown, then extract structured fields.
 * 
 * @param filePath Local path to the contract PDF
 * @returns Extracted contract data as typed object
 */
export async function processLegalContract(filePath: string) {
  console.log(`Processing legal contract: ${filePath}`);

  // Convert local file to data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const base64Data = fileBuffer.toString("base64");
  const dataUrl = `data:application/octet-stream;base64,${base64Data}`;

  console.log("Step 1: Parsing contract to markdown...");
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      // Agentic parsing preserves layout structure critical for legal documents
      blockOptions: {
        text: {
          agentic: {
            enabled: true,
          },
        },
      },
      // Document-level chunking keeps parties, signatures, and clauses together
      chunkingStrategy: {
        type: "document",
      },
    },
  });

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

  // Log parsed markdown for debugging (first 500 chars)
  const markdown = parseRun.output.chunks.map((c) => c.content).join("\n\n");
  console.log(`Parsed ${markdown.length} chars of markdown.`);
  console.log(`Preview: ${markdown.substring(0, 500)}...\n`);

  console.log("Step 2: Extracting contract fields...");
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: LegalContractSchema,
      // Performance engine with review agent catches extraction errors
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: {
          enabled: true, // Agent verifies party names, dates, signatures
        },
        advancedMultimodalEnabled: true, // Processes signature block images
      },
    },
  });

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

  const extracted = extractRun.output.value;

  console.log("\n✓ Extraction complete.\n");
  console.log("Extracted Fields:");
  console.log(`  Agreement Title: ${extracted.agreement_title}`);
  console.log(`  Agreement Date: ${extracted.agreement_date}`);
  console.log(`  Term Start: ${extracted.term_start_date}`);
  console.log(`  Term End: ${extracted.term_end_date}`);
  console.log(`  Governing Law: ${extracted.governing_law}`);
  console.log(`  Parties: ${extracted.parties.length}`);
  extracted.parties.forEach((p, i) => {
    console.log(
      `    [${i + 1}] ${p.party_name} (${p.party_role}) at ${p.party_address}`
    );
  });
  console.log(`  Signatories: ${extracted.signatures.length}`);
  extracted.signatures.forEach((s, i) => {
    console.log(
      `    [${i + 1}] ${s.signatory_name} (${s.signatory_title}) for ${s.signatory_party} on ${s.signature_date}`
    );
  });
  console.log(`  Payment Terms: ${extracted.payment_terms?.substring(0, 100)}...`);
  console.log(`  Renewal Terms: ${extracted.renewal_terms?.substring(0, 100)}...`);
  console.log(
    `  Confidentiality: ${extracted.confidentiality_clause ? "✓ Present" : "✗ Absent"}`
  );
  console.log(
    `  Liability Clause: ${extracted.liability_clause ? "✓ Present" : "✗ Absent"}`
  );
  console.log(
    `  Termination Clause: ${extracted.termination_clause ? "✓ Present" : "✗ Absent"}`
  );
  console.log(
    `  Dispute Resolution: ${extracted.dispute_resolution_clause ? "✓ Present" : "✗ Absent"}`
  );

  return extracted;
}

// Main entry point for testing
async function main() {
  const testFilePath = process.argv[2] || "./contract.pdf";
  try {
    const result = await processLegalContract(testFilePath);
    console.log("\nFull JSON output:");
    console.log(JSON.stringify(result, null, 2));
  } catch (error) {
    console.error("Error:", error);
    process.exit(1);
  }
}

main();
import os
import json
import base64
from extend_ai import Extend

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

# JSON schema for legal contract extraction
LEGAL_CONTRACT_SCHEMA = {
    "type": "object",
    "properties": {
        "agreement_title": {
            "type": ["string", "null"],
            "description": "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'."
        },
        "agreement_reference_number": {
            "type": ["string", "null"],
            "description": "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
        },
        "agreement_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations."
        },
        "term_start_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context."
        },
        "term_end_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent."
        },
        "parties": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "party_name": {
                        "type": ["string", "null"],
                        "description": "The full legal name of the party to the agreement. This may be a company, organization, or individual."
                    },
                    "party_role": {
                        "type": ["string", "null"],
                        "description": "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'."
                    },
                    "party_address": {
                        "type": ["string", "null"],
                        "description": "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country."
                    }
                },
                "additionalProperties": False,
                "required": ["party_name", "party_role", "party_address"]
            },
            "description": "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee')."
        },
        "signatures": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "signatory_name": {
                        "type": ["string", "null"],
                        "description": "The name of the individual signing the agreement on behalf of a party."
                    },
                    "signature_date": {
                        "type": ["string", "null"],
                        "extend:type": "date",
                        "description": "The date on which the agreement was signed by this signatory."
                    },
                    "signatory_party": {
                        "type": ["string", "null"],
                        "description": "The party on whose behalf the individual is signing."
                    },
                    "signatory_title": {
                        "type": ["string", "null"],
                        "description": "The title or position of the signatory within the organization, if applicable."
                    }
                },
                "additionalProperties": False,
                "required": ["signatory_name", "signature_date", "signatory_party", "signatory_title"]
            },
            "description": "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed."
        },
        "payment_terms": {
            "type": ["string", "null"],
            "description": "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar."
        },
        "renewal_terms": {
            "type": ["string", "null"],
            "description": "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements."
        },
        "governing_law": {
            "type": ["string", "null"],
            "description": "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'."
        },
        "definitions_section": {
            "type": ["string", "null"],
            "description": "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'."
        },
        "confidentiality_clause": {
            "type": ["string", "null"],
            "description": "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar."
        },
        "liability_clause": {
            "type": ["string", "null"],
            "description": "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps."
        },
        "termination_clause": {
            "type": ["string", "null"],
            "description": "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination."
        },
        "dispute_resolution_clause": {
            "type": ["string", "null"],
            "description": "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings."
        },
        "notices_clause": {
            "type": ["string", "null"],
            "description": "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing."
        }
    },
    "required": [
        "parties", "signatures", "governing_law", "payment_terms", "renewal_terms",
        "term_end_date", "agreement_date", "notices_clause", "agreement_title",
        "term_start_date", "liability_clause", "termination_clause", "definitions_section",
        "confidentiality_clause", "dispute_resolution_clause", "agreement_reference_number"
    ],
    "additionalProperties": False
}


def process_legal_contract(file_path: str) -> dict:
    """
    Process a legal contract: parse to markdown, then extract structured fields.
    
    Args:
        file_path: Local path to the contract PDF
        
    Returns:
        Extracted contract data as typed dict
    """
    print(f"Processing legal contract: {file_path}")

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

    print("Step 1: Parsing contract to markdown...")
    parse_run = client.parse_runs.create_and_poll(
        file={"url": data_url},
        config={
            "block_options": {
                "text": {
                    "agentic": {
                        "enabled": True,
                    },
                },
            },
            "chunking_strategy": {
                "type": "document",
            },
        },
    )

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

    # Log parsed markdown for debugging (first 500 chars)
    markdown = "\n\n".join([c.content for c in parse_run.output.chunks])
    print(f"Parsed {len(markdown)} chars of markdown.")
    print(f"Preview: {markdown[:500]}...\n")

    print("Step 2: Extracting contract fields...")
    extract_run = client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={
            "schema": LEGAL_CONTRACT_SCHEMA,
            "base_processor": "extraction_performance",
            "advanced_options": {
                "review_agent": {
                    "enabled": True,
                },
                "advanced_multimodal_enabled": True,
            },
        },
    )

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

    extracted = extract_run.output.value

    print("\n✓ Extraction complete.\n")
    print("Extracted Fields:")
    print(f"  Agreement Title: {extracted.get('agreement_title')}")
    print(f"  Agreement Date: {extracted.get('agreement_date')}")
    print(f"  Term Start: {extracted.get('term_start_date')}")
    print(f"  Term End: {extracted.get('term_end_date')}")
    print(f"  Governing Law: {extracted.get('governing_law')}")
    
    parties = extracted.get('parties', [])
    print(f"  Parties: {len(parties)}")
    for i, p in enumerate(parties):
        print(f"    [{i + 1}] {p.get('party_name')} ({p.get('party_role')}) at {p.get('party_address')}")
    
    signatures = extracted.get('signatures', [])
    print(f"  Signatories: {len(signatures)}")
    for i, s in enumerate(signatures):
        print(f"    [{i + 1}] {s.get('signatory_name')} ({s.get('signatory_title')}) for {s.get('signatory_party')} on {s.get('signature_date')}")
    
    payment_terms = extracted.get('payment_terms')
    print(f"  Payment Terms: {payment_terms[:100] if payment_terms else None}...")
    
    renewal_terms = extracted.get('renewal_terms')
    print(f"  Renewal Terms: {renewal_terms[:100] if renewal_terms else None}...")
    
    print(f"  Confidentiality: {'✓ Present' if extracted.get('confidentiality_clause') else '✗ Absent'}")
    print(f"  Liability Clause: {'✓ Present' if extracted.get('liability_clause') else '✗ Absent'}")
    print(f"  Termination Clause: {'✓ Present' if extracted.get('termination_clause') else '✗ Absent'}")
    print(f"  Dispute Resolution: {'✓ Present' if extracted.get('dispute_resolution_clause') else '✗ Absent'}")

    return extracted


def main():
    import sys
    test_file_path = sys.argv[1] if len(sys.argv) > 1 else "./contract.pdf"
    try:
        result = process_legal_contract(test_file_path)
        print("\nFull JSON output:")
        print(json.dumps(result, indent=2))
    except Exception as error:
        print(f"Error: {error}")
        sys.exit(1)


if __name__ == "__main__":
    main()
// 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 SDK.

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

public class LegalContractProcessor {

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

  // ============================================================================
  // Data Classes
  // ============================================================================

  static class Party {
    public String party_name;
    public String party_role;
    public String party_address;

    public Party(String name, String role, String address) {
      this.party_name = name;
      this.party_role = role;
      this.party_address = address;
    }
  }

  static class Signature {
    public String signatory_name;
    public String signature_date;
    public String signatory_party;
    public String signatory_title;

    public Signature(String name, String date, String party, String title) {
      this.signatory_name = name;
      this.signature_date = date;
      this.signatory_party = party;
      this.signatory_title = title;
    }
  }

  static class LegalContractExtraction {
    public String agreement_title;
    public String agreement_reference_number;
    public String agreement_date;
    public String term_start_date;
    public String term_end_date;
    public List<Party> parties;
    public List<Signature> signatures;
    public String payment_terms;
    public String renewal_terms;
    public String governing_law;
    public String definitions_section;
    public String confidentiality_clause;
    public String liability_clause;
    public String termination_clause;
    public String dispute_resolution_clause;
    public String notices_clause;
  }

  static class ParseRunResponse {
    public String status;
    public ParseOutput output;
  }

  static class ParseOutput {
    public List<Chunk> chunks;
  }

  static class Chunk {
    public String content;
  }

  static class ExtractRunResponse {
    public String status;
    public ExtractOutput output;
  }

  static class ExtractOutput {
    public LegalContractExtraction value;
  }

  // ============================================================================
  // JSON Helpers (minimal, no external library)
  // ============================================================================

  private static String toJson(Object obj) throws IOException {
    // Simple JSON serialization for request bodies
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(entry.getKey()).append("\":");
        sb.append(toJson(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    } else if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      for (int i = 0; i < list.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append(toJson(list.get(i)));
      }
      sb.append("]");
      return sb.toString();
    } else if (obj instanceof String) {
      return "\"" + ((String) obj).replace("\"", "\\\"") + "\"";
    } else if (obj instanceof Boolean) {
      return obj.toString();
    } else if (obj == null) {
      return "null";
    } else {
      return obj.toString();
    }
  }

  private static <T> T fromJson(String json, Class<T> clazz) throws IOException {
    // Minimal JSON deserialization using reflection
    // For production, use a proper JSON library
    if (clazz == ParseRunResponse.class) {
      ParseRunResponse resp = new ParseRunResponse();
      resp.status = extractJsonString(json, "status");
      resp.output = new ParseOutput();
      resp.output.chunks = new ArrayList<>();
      String chunksStr = extractJsonObject(json, "output");
      String chunksArray = extractJsonArray(chunksStr, "chunks");
      for (String chunk : parseJsonArray(chunksArray)) {
        Chunk c = new Chunk();
        c.content = extractJsonString(chunk, "content");
        resp.output.chunks.add(c);
      }
      return (T) resp;
    } else if (clazz == ExtractRunResponse.class) {
      ExtractRunResponse resp = new ExtractRunResponse();
      resp.status = extractJsonString(json, "status");
      resp.output = new ExtractOutput();
      String outputStr = extractJsonObject(json, "output");
      String valueStr = extractJsonObject(outputStr, "value");
      resp.output.value = parseContractExtraction(valueStr);
      return (T) resp;
    }
    return null;
  }

  private static String extractJsonString(String json, String key) {
    String pattern = "\"" + key + "\":\"";
    int start = json.indexOf(pattern);
    if (start == -1) return null;
    start += pattern.length();
    int end = json.indexOf("\"", start);
    return json.substring(start, end);
  }

  private static String extractJsonObject(String json, String key) {
    String pattern = "\"" + key + "\":{";
    int start = json.indexOf(pattern);
    if (start == -1) return null;
    start += pattern.length() - 1;
    int depth = 1;
    int i = start + 1;
    while (i < json.length() && depth > 0) {
      if (json.charAt(i) == '{') depth++;
      else if (json.charAt(i) == '}') depth--;
      i++;
    }
    return json.substring(start, i);
  }

  private static String extractJsonArray(String json, String key) {
    String pattern = "\"" + key + "\":[";
    int start = json.indexOf(pattern);
    if (start == -1) return null;
    start += pattern.length() - 1;
    int depth = 1;
    int i = start + 1;
    while (i < json.length() && depth > 0) {
      if (json.charAt(i) == '[') depth++;
      else if (json.charAt(i) == ']') depth--;
      i++;
    }
    return json.substring(start, i);
  }

  private static List<String> parseJsonArray(String arrayStr) {
    List<String> items = new ArrayList<>();
    int depth = 0;
    int start = 1;
    for (int i = 1; i < arrayStr.length() - 1; i++) {
      char c = arrayStr.charAt(i);
      if (c == '{' || c == '[') depth++;
      else if (c == '}' || c == ']') depth--;
      else if (c == ',' && depth == 0) {
        items.add(arrayStr.substring(start, i).trim());
        start = i + 1;
      }
    }
    if (start < arrayStr.length() - 1) {
      items.add(arrayStr.substring(start, arrayStr.length() - 1).trim());
    }
    return items;
  }

  private static LegalContractExtraction parseContractExtraction(String json) {
    LegalContractExtraction extraction = new LegalContractExtraction();
    extraction.agreement_title = extractJsonString(json, "agreement_title");
    extraction.agreement_reference_number = extractJsonString(json, "agreement_reference_number");
    extraction.agreement_date = extractJsonString(json, "agreement_date");
    extraction.term_start_date = extractJsonString(json, "term_start_date");
    extraction.term_end_date = extractJsonString(json, "term_end_date");
    extraction.payment_terms = extractJsonString(json, "payment_terms");
    extraction.renewal_terms = extractJsonString(json, "renewal_terms");
    extraction.governing_law = extractJsonString(json, "governing_law");
    extraction.definitions_section = extractJsonString(json, "definitions_section");
    extraction.confidentiality_clause = extractJsonString(json, "confidentiality_clause");
    extraction.liability_clause = extractJsonString(json, "liability_clause");
    extraction.termination_clause = extractJsonString(json, "termination_clause");
    extraction.dispute_resolution_clause = extractJsonString(json, "dispute_resolution_clause");
    extraction.notices_clause = extractJsonString(json, "notices_clause");

    extraction.parties = new ArrayList<>();
    String partiesArray = extractJsonArray(json, "parties");
    if (partiesArray != null) {
      for (String partyStr : parseJsonArray(partiesArray)) {
        Party p = new Party(
            extractJsonString(partyStr, "party_name"),
            extractJsonString(partyStr, "party_role"),
            extractJsonString(partyStr, "party_address")
        );
        extraction.parties.add(p);
      }
    }

    extraction.signatures = new ArrayList<>();
    String signaturesArray = extractJsonArray(json, "signatures");
    if (signaturesArray != null) {
      for (String sigStr : parseJsonArray(signaturesArray)) {
        Signature s = new Signature(
            extractJsonString(sigStr, "signatory_name"),
            extractJsonString(sigStr, "signature_date"),
            extractJsonString(sigStr, "signatory_party"),
            extractJsonString(sigStr, "signatory_title")
        );
        extraction.signatures.add(s);
      }
    }

    return extraction;
  }

  // ============================================================================
  // API Calls
  // ============================================================================

  private static String makeRequest(String method, String endpoint, String body)
      throws IOException, InterruptedException {
    HttpRequest.Builder builder = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE + endpoint))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json");

    if ("POST".equals(method)) {
      builder.POST(HttpRequest.BodyPublishers.ofString(body));
    } else if ("GET".equals(method)) {
      builder.GET();
    }

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

    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      throw new IOException("API error: " + response.statusCode() + " " + response.body());
    }

    return response.body();
  }

  private static String createParseRun(String dataUrl) throws IOException, InterruptedException {
    Map<String, Object> blockOptions = new HashMap<>();
    Map<String, Object> textOptions = new HashMap<>();
    Map<String, Object> agenticOptions = new HashMap<>();
    agenticOptions.put("enabled", true);
    textOptions.put("agentic", agenticOptions);
    blockOptions.put("text", textOptions);

    Map<String, Object> config = new HashMap<>();
    config.put("blockOptions", blockOptions);
    Map<String, Object> chunking = new HashMap<>();
    chunking.put("type", "document");
    config.put("chunkingStrategy", chunking);

    Map<String, Object> file = new HashMap<>();
    file.put("url", dataUrl);

    Map<String, Object> body = new HashMap<>();
    body.put("file", file);
    body.put("config", config);

    String response = makeRequest("POST", "/v1/parseRuns", toJson(body));
    String runId = extractJsonString(response, "id");
    return runId;
  }

  private static ParseRunResponse getParseRun(String runId)
      throws IOException, InterruptedException {
    String response = makeRequest("GET", "/v1/parseRuns/" + runId, null);
    return fromJson(response, ParseRunResponse.class);
  }

  private static String createExtractRun(String dataUrl) throws IOException, InterruptedException {
    Map<String, Object> schema = buildLegalContractSchema();

    Map<String, Object> reviewAgent = new HashMap<>();
    reviewAgent.put("enabled", true);

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

    Map<String, Object> config = new HashMap<>();
    config.put("schema", schema);
    config.put("baseProcessor", "extraction_performance");
    config.put("advancedOptions", advancedOptions);

    Map<String, Object> file = new HashMap<>();
    file.put("url", dataUrl);

    Map<String, Object> body = new HashMap<>();
    body.put("file", file);
    body.put("config", config);

    String response = makeRequest("POST", "/v1/extractRuns", toJson(body));
    String runId = extractJsonString(response, "id");
    return runId;
  }

  private static ExtractRunResponse getExtractRun(String runId)
      throws IOException, InterruptedException {
    String response = makeRequest("GET", "/v1/extractRuns/" + runId, null);
    return fromJson(response, ExtractRunResponse.class);
  }

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

    Map<String, Object> properties = new HashMap<>();

    // agreement_title
    Map<String, Object> agreementTitle = new HashMap<>();
    agreementTitle.put("type", "string");
    agreementTitle.put("description",
        "The official title or heading of the legal agreement or contract.");
    properties.put("agreement_title", agreementTitle);

    // agreement_reference_number
    Map<String, Object> refNumber = new HashMap<>();
    refNumber.put("type", "string");
    refNumber.put("description", "A unique identifier or reference number assigned to this agreement.");
    properties.put("agreement_reference_number", refNumber);

    // agreement_date
    Map<String, Object> agreementDate = new HashMap<>();
    agreementDate.put("type", "string");
    agreementDate.put("extend:type", "date");
    agreementDate.put("description", "The effective date when the agreement becomes valid.");
    properties.put("agreement_date", agreementDate);

    // term_start_date
    Map<String, Object> termStart = new HashMap<>();
    termStart.put("type", "string");
    termStart.put("extend:type", "date");
    termStart.put("description", "The date on which the contractual obligations begin.");
    properties.put("term_start_date", termStart);

    // term_end_date
    Map<String, Object> termEnd = new HashMap<>();
    termEnd.put("type", "string");
    termEnd.put("extend:type", "date");
    termEnd.put("description", "The date on which the agreement expires or terminates.");
    properties.put("term_end_date", termEnd);

    // parties
    Map<String, Object> partyItem = new HashMap<>();
    partyItem.put("type", "object");
    Map<String, Object> partyProps = new HashMap<>();
    Map<String, Object> partyName = new HashMap<>();
    partyName.put("type", "string");
    partyName.put("description", "The full legal name of the party to the agreement.");
    partyProps.put("party_name", partyName);
    Map<String, Object> partyRole = new HashMap<>();
    partyRole.put("type", "string");
    partyRole.put("description", "The role or designation of the party within the agreement.");
    partyProps.put("party_role", partyRole);
    Map<String, Object> partyAddress = new HashMap<>();
    partyAddress.put("type", "string");
    partyAddress.put("description", "The official address of the party as stated in the agreement.");
    partyProps.put("party_address", partyAddress);
    partyItem.put("properties", partyProps);
    partyItem.put("required", List.of("party_name", "party_role", "party_address"));
    Map<String, Object> parties = new HashMap<>();
    parties.put("type", "array");
    parties.put("items", partyItem);
    parties.put("description", "The entities or individuals entering into the agreement.");
    properties.put("parties", parties);

    // signatures
    Map<String, Object> sigItem = new HashMap<>();
    sigItem.put("type", "object");
    Map<String, Object> sigProps = new HashMap<>();
    Map<String, Object> sigName = new HashMap<>();
    sigName.put("type", "string");
    sigName.put("description", "The name of the individual signing the agreement.");
    sigProps.put("signatory_name", sigName);
    Map<String, Object> sigDate = new HashMap<>();
    sigDate.put("type", "string");
    sigDate.put("extend:type", "date");
    sigDate.put("description", "The date on which the agreement was signed by this signatory.");
    sigProps.put("signature_date", sigDate);
    Map<String, Object> sigParty = new HashMap<>();
    sigParty.put("type", "string");
    sigParty.put("description", "The party on whose behalf the individual is signing.");
    sigProps.put("signatory_party", sigParty);
    Map<String, Object> sigTitle = new HashMap<>();
    sigTitle.put("type", "string");
    sigTitle.put("description", "The title or position of the signatory within the organization.");
    sigProps.put("signatory_title", sigTitle);
    sigItem.put("properties", sigProps);
    sigItem.put("required", List.of("signatory_name", "signature_date", "signatory_party", "signatory_title"));
    Map<String, Object> signatures = new HashMap<>();
    signatures.put("type", "array");
    signatures.put("items", sigItem);
    signatures.put("description", "The signatures or signature blocks of the parties to the agreement.");
    properties.put("signatures", signatures);

    // payment_terms
    Map<String, Object> paymentTerms = new HashMap<>();
    paymentTerms.put("type", "string");
    paymentTerms.put("description", "The terms and conditions related to payments under the agreement.");
    properties.put("payment_terms", paymentTerms);

    // renewal_terms
    Map<String, Object> renewalTerms = new HashMap<>();
    renewalTerms.put("type", "string");
    renewalTerms.put("description", "Provisions for renewal or extension of the agreement.");
    properties.put("renewal_terms", renewalTerms);

    // governing_law
    Map<String, Object> governingLaw = new HashMap<>();
    governingLaw.put("type", "string");
    governingLaw.put("description", "The jurisdiction or legal system under which the agreement is interpreted.");
    properties.put("governing_law", governingLaw);

    // definitions_section
    Map<String, Object> definitions = new HashMap<>();
    definitions.put("type", "string");
    definitions.put("description", "The section listing defined terms used throughout the agreement.");
    properties.put("definitions_section", definitions);

    // confidentiality_clause
    Map<String, Object> confidentiality = new HashMap<>();
    confidentiality.put("type", "string");
    confidentiality.put("description", "Obligations regarding the protection of confidential information.");
    properties.put("confidentiality_clause", confidentiality);

    // liability_clause
    Map<String, Object> liability = new HashMap<>();
    liability.put("type", "string");
    liability.put("description", "The allocation or limitation of liability between the parties.");
    properties.put("liability_clause", liability);

    // termination_clause
    Map<String, Object> termination = new HashMap<>();
    termination.put("type", "string");
    termination.put("description", "Conditions and procedures for terminating the agreement.");
    properties.put("termination_clause", termination);

    // dispute_resolution_clause
    Map<String, Object> disputeResolution = new HashMap<>();
    disputeResolution.put("type", "string");
    disputeResolution.put("description", "The process for resolving disputes under the agreement.");
    properties.put("dispute_resolution_clause", disputeResolution);

    // notices_clause
    Map<String, Object> notices = new HashMap<>();
    notices.put("type", "string");
    notices.put("description", "How formal notices must be delivered between parties.");
    properties.put("notices_clause", notices);

    schema.put("properties", properties);
    schema.put("required", List.of(
        "agreement_title", "agreement_reference_number", "agreement_date",
        "term_start_date", "term_end_date", "parties", "signatures",
        "payment_terms", "renewal_terms", "governing_law", "definitions_section",
        "confidentiality_clause", "liability_clause", "termination_clause",
        "dispute_resolution_clause", "notices_clause"
    ));
    schema.put("additionalProperties", false);

    return schema;
  }

  // ============================================================================
  // Main Processing Function
  // ============================================================================

  public static LegalContractExtraction processLegalContract(String filePath)
      throws IOException, InterruptedException {
    System.out.println("Processing legal contract: " + filePath);

    // Convert local file to data URL
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64Data = Base64.getEncoder().encodeToString(fileBytes);
    String dataUrl = "data:application/octet-stream;base64," + base64Data;

    System.out.println("Step 1: Parsing contract to markdown...");
    String parseRunId = createParseRun(dataUrl);
    ParseRunResponse parseRun;
    do {
      Thread.sleep(2000);
      parseRun = getParseRun(parseRunId);
    } while (!"PROCESSED".equals(parseRun.status) && !"FAILED".equals(parseRun.status));

    if (!"PROCESSED".equals(parseRun.status)) {
      throw new IOException("Parse failed with status: " + parseRun.status);
    }

    StringBuilder markdown = new StringBuilder();
    for (Chunk chunk : parseRun.output.chunks) {
      if (markdown.length() > 0) markdown.append("\n\n");
      markdown.append(chunk.content);
    }
    System.out.println("Parsed " + markdown.length() + " chars of markdown.");
    System.out.println("Preview: " + markdown.substring(0, Math.min(500, markdown.length())) + "...\n");

    System.out.println("Step 2: Extracting contract fields...");
    String extractRunId = createExtractRun(dataUrl);
    ExtractRunResponse extractRun;
    do {
      Thread.sleep(2000);
      extractRun = getExtractRun(extractRunId);
    } while (!"PROCESSED".equals(extractRun.status) && !"FAILED".equals(extractRun.status));

    if (!"PROCESSED".equals(extractRun.status)) {
      throw new IOException("Extraction failed with status: " + extractRun.status);
    }

    LegalContractExtraction extracted = extractRun.output.value;

    System.out.println("\n✓ Extraction complete.\n");
    System.out.println("Extracted Fields:");
    System.out.println("  Agreement Title: " + extracted.agreement_title);
    System.out.println("  Agreement Date: " + extracted.agreement_date);
    System.out.println("  Term Start: " + extracted.term_start_date);
    System.out.println("  Term End: " + extracted.term_end_date);
    System.out.println("  Governing Law: " + extracted.governing_law);
    System.out.println("  Parties: " + extracted.parties.size());
    for (int i = 0; i < extracted.parties.size(); i++) {
      Party p = extracted.parties.get(i);
      System.out.println("    [" + (i + 1) + "] " + p.party_name + " (" + p.party_role + ") at " + p.party_address);
    }
    System.out.println("  Signatories: " + extracted.signatures.size());
    for (int i = 0; i < extracted.signatures.size(); i++) {
      Signature s = extracted.signatures.get(i);
      System.out.println("    [" + (i + 1) + "] " + s.signatory_name + " (" + s.signatory_title + ") for " + s.signatory_party + " on " + s.signature_date);
    }
    System.out.println("  Payment Terms: " + (extracted.payment_terms != null ? extracted.payment_terms.substring(0, Math.min(100, extracted.payment_terms.length())) + "..." : "null"));
    System.out.println("  Renewal Terms: " + (extracted.renewal_terms != null ? extracted.renewal_terms.substring(0, Math.min(100, extracted.renewal_terms.length())) + "..." : "null"));
    System.out.println("  Confidentiality: " + (extracted.confidentiality_clause != null ? "✓ Present" : "✗ Absent"));
    System.out.println("  Liability Clause: " + (extracted.liability_clause != null ? "✓ Present" : "✗ Absent"));
    System.out.println("  Termination Clause: " + (extracted.termination_clause != null ? "✓ Present" : "✗ Absent"));
    System.out.println("  Dispute Resolution: " + (extracted.dispute_resolution_clause != null ? "✓ Present" : "✗ Absent"));

    return extracted;
  }

  public static void main(String[] args) {
    String testFilePath = args.length > 0 ? args[0] : "./contract.pdf";
    try {
      LegalContractExtraction result = processLegalContract(testFilePath);
      System.out.println("\nFull extraction result:");
      System.out.println("Agreement Title: " + result.agreement_title);
      System.out.println("Parties: " + result.parties.size());
      System.out.println("Signatures: " + result.signatures.size());
    } catch (Exception e) {
      System.err.println("Error: " + e.getMessage());
      e.printStackTrace();
      System.exit(1);
    }
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.

package main

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

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

// Party represents a party to the agreement
type Party struct {
	PartyName    *string `json:"party_name"`
	PartyRole    *string `json:"party_role"`
	PartyAddress *string `json:"party_address"`
}

// Signature represents a signature block
type Signature struct {
	SignatoryName  *string `json:"signatory_name"`
	SignatureDate  *string `json:"signature_date"`
	SignatoryParty *string `json:"signatory_party"`
	SignatoryTitle *string `json:"signatory_title"`
}

// LegalContract represents the extracted contract data
type LegalContract struct {
	AgreementTitle           *string      `json:"agreement_title"`
	AgreementReferenceNumber *string      `json:"agreement_reference_number"`
	AgreementDate            *string      `json:"agreement_date"`
	TermStartDate            *string      `json:"term_start_date"`
	TermEndDate              *string      `json:"term_end_date"`
	Parties                  []Party      `json:"parties"`
	Signatures               []Signature  `json:"signatures"`
	PaymentTerms             *string      `json:"payment_terms"`
	RenewalTerms             *string      `json:"renewal_terms"`
	GoverningLaw             *string      `json:"governing_law"`
	DefinitionsSection       *string      `json:"definitions_section"`
	ConfidentialityClause    *string      `json:"confidentiality_clause"`
	LiabilityClause          *string      `json:"liability_clause"`
	TerminationClause        *string      `json:"termination_clause"`
	DisputeResolutionClause  *string      `json:"dispute_resolution_clause"`
	NoticesClause            *string      `json:"notices_clause"`
}

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

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

// PollResponse represents a generic poll response
type PollResponse struct {
	Status string          `json:"status"`
	Output json.RawMessage `json:"output"`
}

func makeRequest(method, endpoint string, body interface{}, apiKey string) ([]byte, error) {
	url := extendAPIBase + endpoint
	var reqBody io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewBuffer(jsonBody)
	}

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

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

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

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

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

	return respBody, nil
}

func pollRun(runID, runType string, apiKey string) (json.RawMessage, error) {
	endpoint := fmt.Sprintf("/%s_runs/%s/poll", runType, runID)
	for {
		respBody, err := makeRequest("GET", endpoint, nil, apiKey)
		if err != nil {
			return nil, err
		}

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

		if pollResp.Status == "PROCESSED" {
			return pollResp.Output, nil
		}
		if pollResp.Status == "FAILED" {
			return nil, fmt.Errorf("run failed with status: %s", pollResp.Status)
		}

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

func processLegalContract(filePath string) (*LegalContract, error) {
	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
	}

	fmt.Printf("Processing legal contract: %s\n", filePath)

	// Read file and convert to data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
	dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64Data)

	// Step 1: Parse contract to markdown
	fmt.Println("Step 1: Parsing contract to markdown...")

	parseReqBody := 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",
			},
		},
	}

	parseResp, err := makeRequest("POST", "/parse_runs", parseReqBody, apiKey)
	if err != nil {
		return nil, err
	}

	var parseCreateResp struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(parseResp, &parseCreateResp); err != nil {
		return nil, err
	}

	parseOutput, err := pollRun(parseCreateResp.ID, "parse", apiKey)
	if err != nil {
		return nil, err
	}

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

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

	markdown := ""
	for _, chunk := range parseRunResp.Output.Chunks {
		markdown += chunk.Content + "\n\n"
	}
	fmt.Printf("Parsed %d chars of markdown.\n", len(markdown))
	if len(markdown) > 500 {
		fmt.Printf("Preview: %s...\n\n", markdown[:500])
	}

	// Step 2: Extract contract fields
	fmt.Println("Step 2: Extracting contract fields...")

	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"agreement_title": map[string]interface{}{
				"type": []string{"string", "null"},
				"description": "The official title or heading of the legal agreement or contract.",
			},
			"agreement_reference_number": map[string]interface{}{
				"type": []string{"string", "null"},
				"description": "A unique identifier or reference number assigned to this agreement.",
			},
			"agreement_date": map[string]interface{}{
				"type":          []string{"string", "null"},
				"extend:type":   "date",
				"description":   "The effective date when the agreement becomes valid.",
			},
			"term_start_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"extend:type": "date",
				"description": "The date on which the contractual obligations begin.",
			},
			"term_end_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"extend:type": "date",
				"description": "The date on which the agreement expires or terminates.",
			},
			"parties": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"party_name": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The full legal name of the party.",
						},
						"party_role": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The role of the party within the agreement.",
						},
						"party_address": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The official address of the party.",
						},
					},
				},
				"description": "The entities or individuals entering into the agreement.",
			},
			"signatures": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"signatory_name": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The name of the individual signing.",
						},
						"signature_date": map[string]interface{}{
							"type":        []string{"string", "null"},
							"extend:type": "date",
							"description": "The date on which the agreement was signed.",
						},
						"signatory_party": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The party on whose behalf the individual is signing.",
						},
						"signatory_title": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "The title or position of the signatory.",
						},
					},
				},
				"description": "The signatures or signature blocks of the parties.",
			},
			"payment_terms": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The terms and conditions related to payments.",
			},
			"renewal_terms": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Provisions for renewal or extension of the agreement.",
			},
			"governing_law": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The jurisdiction under which the agreement is enforced.",
			},
			"definitions_section": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The section listing defined terms used throughout.",
			},
			"confidentiality_clause": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Obligations regarding protection of confidential information.",
			},
			"liability_clause": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Allocation or limitation of liability between parties.",
			},
			"termination_clause": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Conditions and procedures for terminating the agreement.",
			},
			"dispute_resolution_clause": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Process for resolving disputes under the agreement.",
			},
			"notices_clause": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "How formal notices must be delivered between parties.",
			},
		},
	}

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

	extractResp, err := makeRequest("POST", "/extract_runs", extractReqBody, apiKey)
	if err != nil {
		return nil, err
	}

	var extractCreateResp struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(extractResp, &extractCreateResp); err != nil {
		return nil, err
	}

	extractOutput, err := pollRun(extractCreateResp.ID, "extract", apiKey)
	if err != nil {
		return nil, err
	}

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

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

	extracted := &extractRunResp.Output.Value

	fmt.Println("\n✓ Extraction complete.\n")
	fmt.Println("Extracted Fields:")
	fmt.Printf("  Agreement Title: %v\n", extracted.AgreementTitle)
	fmt.Printf("  Agreement Date: %v\n", extracted.AgreementDate)
	fmt.Printf("  Term Start: %v\n", extracted.TermStartDate)
	fmt.Printf("  Term End: %v\n", extracted.TermEndDate)
	fmt.Printf("  Governing Law: %v\n", extracted.GoverningLaw)
	fmt.Printf("  Parties: %d\n", len(extracted.Parties))
	for i, p := range extracted.Parties {
		fmt.Printf("    [%d] %v (%v) at %v\n", i+1, p.PartyName, p.PartyRole, p.PartyAddress)
	}
	fmt.Printf("  Signatories: %d\n", len(extracted.Signatures))
	for i, s := range extracted.Signatures {
		fmt.Printf("    [%d] %v (%v) for %v on %v\n", i+1, s.SignatoryName, s.SignatoryTitle, s.SignatoryParty, s.SignatureDate)
	}
	if extracted.PaymentTerms != nil && len(*extracted.PaymentTerms) > 100 {
		fmt.Printf("  Payment Terms: %s...\n", (*extracted.PaymentTerms)[:100])
	} else {
		fmt.Printf("  Payment Terms: %v\n", extracted.PaymentTerms)
	}
	if extracted.RenewalTerms != nil && len(*extracted.RenewalTerms) > 100 {
		fmt.Printf("  Renewal Terms: %s...\n", (*extracted.RenewalTerms)[:100])
	} else {
		fmt.Printf("  Renewal Terms: %v\n", extracted.RenewalTerms)
	}
	if extracted.ConfidentialityClause != nil {
		fmt.Println("  Confidentiality: ✓ Present")
	} else {
		fmt.Println("  Confidentiality: ✗ Absent")
	}
	if extracted.LiabilityClause != nil {
		fmt.Println("  Liability Clause: ✓ Present")
	} else {
		fmt.Println("  Liability Clause: ✗ Absent")
	}
	if extracted.TerminationClause != nil {
		fmt.Println("  Termination Clause: ✓ Present")
	} else {
		fmt.Println("  Termination Clause: ✗ Absent")
	}
	if extracted.DisputeResolutionClause != nil {
		fmt.Println("  Dispute Resolution: ✓ Present")
	} else {
		fmt.Println("  Dispute Resolution: ✗ Absent")
	}

	return extracted, nil
}

func main() {
	testFilePath := "./contract.pdf"
	if len(os.Args) > 1 {
		testFilePath = os.Args[1]
	}

	result, err := processLegalContract(testFilePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("\nFull JSON output:")
	jsonOutput, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
		os.Exit(1)
	}
	fmt.Println(string(jsonOutput))
}
// Deploy the "Legal Contract" 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/legal-contract.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: legal-contract).

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, "legal-contract.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": "Legal Contract Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "required": [
              "parties",
              "signatures",
              "governing_law",
              "payment_terms",
              "renewal_terms",
              "term_end_date",
              "agreement_date",
              "notices_clause",
              "agreement_title",
              "term_start_date",
              "liability_clause",
              "termination_clause",
              "definitions_section",
              "confidentiality_clause",
              "dispute_resolution_clause",
              "agreement_reference_number"
            ],
            "properties": {
              "parties": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "party_name",
                    "party_role",
                    "party_address"
                  ],
                  "properties": {
                    "party_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The full legal name of the party to the agreement. This may be a company, organization, or individual."
                    },
                    "party_role": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'."
                    },
                    "party_address": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee')."
              },
              "signatures": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "signatory_name",
                    "signature_date",
                    "signatory_party",
                    "signatory_title"
                  ],
                  "properties": {
                    "signatory_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The name of the individual signing the agreement on behalf of a party."
                    },
                    "signature_date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The date on which the agreement was signed by this signatory.",
                      "extend:type": "date"
                    },
                    "signatory_party": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The party on whose behalf the individual is signing."
                    },
                    "signatory_title": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The title or position of the signatory within the organization, if applicable."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed."
              },
              "governing_law": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'."
              },
              "payment_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar."
              },
              "renewal_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements."
              },
              "term_end_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent.",
                "extend:type": "date"
              },
              "agreement_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations.",
                "extend:type": "date"
              },
              "notices_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing."
              },
              "agreement_title": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'."
              },
              "term_start_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context.",
                "extend:type": "date"
              },
              "liability_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps."
              },
              "termination_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination."
              },
              "definitions_section": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'."
              },
              "confidentiality_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar."
              },
              "dispute_resolution_clause": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings."
              },
              "agreement_reference_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
              }
            },
            "additionalProperties": false
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

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

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

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

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

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

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from 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 / "legal-contract.json"

state = {}
if STATE_FILE.exists():
    state = json.loads(STATE_FILE.read_text())

def save_state():
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))

WORKFLOW = {
    "name": "Legal Contract Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [
                {
                    "step": "parse1"
                }
            ]
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {
                                "enabled": True
                            }
                        }
                    },
                    "chunkingStrategy": {
                        "type": "document"
                    }
                }
            },
            "next": [
                {
                    "step": "extraction2"
                }
            ]
        },
        {
            "name": "extraction2",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "required": [
                            "parties",
                            "signatures",
                            "governing_law",
                            "payment_terms",
                            "renewal_terms",
                            "term_end_date",
                            "agreement_date",
                            "notices_clause",
                            "agreement_title",
                            "term_start_date",
                            "liability_clause",
                            "termination_clause",
                            "definitions_section",
                            "confidentiality_clause",
                            "dispute_resolution_clause",
                            "agreement_reference_number"
                        ],
                        "properties": {
                            "parties": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "required": [
                                        "party_name",
                                        "party_role",
                                        "party_address"
                                    ],
                                    "properties": {
                                        "party_name": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The full legal name of the party to the agreement. This may be a company, organization, or individual."
                                        },
                                        "party_role": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'."
                                        },
                                        "party_address": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country."
                                        }
                                    },
                                    "additionalProperties": False
                                },
                                "description": "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee')."
                            },
                            "signatures": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "required": [
                                        "signatory_name",
                                        "signature_date",
                                        "signatory_party",
                                        "signatory_title"
                                    ],
                                    "properties": {
                                        "signatory_name": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The name of the individual signing the agreement on behalf of a party."
                                        },
                                        "signature_date": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The date on which the agreement was signed by this signatory.",
                                            "extend:type": "date"
                                        },
                                        "signatory_party": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The party on whose behalf the individual is signing."
                                        },
                                        "signatory_title": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "The title or position of the signatory within the organization, if applicable."
                                        }
                                    },
                                    "additionalProperties": False
                                },
                                "description": "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed."
                            },
                            "governing_law": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'."
                            },
                            "payment_terms": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar."
                            },
                            "renewal_terms": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements."
                            },
                            "term_end_date": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent.",
                                "extend:type": "date"
                            },
                            "agreement_date": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations.",
                                "extend:type": "date"
                            },
                            "notices_clause": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing."
                            },
                            "agreement_title": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'."
                            },
                            "term_start_date": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context.",
                                "extend:type": "date"
                            },
                            "liability_clause": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps."
                            },
                            "termination_clause": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination."
                            },
                            "definitions_section": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'."
                            },
                            "confidentiality_clause": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar."
                            },
                            "dispute_resolution_clause": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings."
                            },
                            "agreement_reference_number": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
                            }
                        },
                        "additionalProperties": False
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {
                            "enabled": True
                        },
                        "advancedMultimodalEnabled": True
                    }
                }
            }
        }
    ]
}

def main():
    client = Extend(token=API_KEY)
    
    print(f'Deploying "{WORKFLOW["name"]}…"')
    
    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
    else:
        # Try to find existing workflow with same name
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, "data") else (workflows_list.items if hasattr(workflows_list, "items") else [])
            existing = next((x for x in items if x.name == WORKFLOW["name"]), None)
            if existing and existing.id:
                state["workflowId"] = existing.id
                save_state()
                print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing.id}) — updating steps')
                client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
        except Exception:
            pass
        
        if not state.get("workflowId"):
            created = client.workflows.create(**WORKFLOW)
            workflow_id = created.id if hasattr(created, "id") else (created.workflow.id if hasattr(created, "workflow") else None)
            if not workflow_id:
                raise Exception("Could not read created workflow id from response")
            state["workflowId"] = workflow_id
            save_state()
            print(f"+ created workflow ({workflow_id})")
    
    # Deploy the current draft as a new version
    try:
        client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass
    
    print("\nDone. Run documents through it with:")
    print(f'  POST https://api.extend.ai/workflow_runs  {{ "workflow": {{ "id": "{state["workflowId"]}" }}, "file": {{ "url": "https://…" }} }}')
    print("Or open the workflow in the Extend dashboard to review and deploy it.")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// Call the REST API at https://api.extend.ai using only java.net.http.HttpClient.

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

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

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

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

  static {
    try {
      if (Files.exists(STATE_FILE)) {
        String content = Files.readString(STATE_FILE);
        state = parseJson(content);
      }
    } catch (IOException e) {
      // State file doesn't exist yet; start with empty state.
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    String json = toJson(state);
    Files.writeString(STATE_FILE, json);
  }

  private static String toJson(Object obj) {
    if (obj == null) return "null";
    if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
    if (obj instanceof Number || obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<?, ?> e : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(escapeJson(e.getKey().toString())).append("\":");
        sb.append(toJson(e.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof java.util.List) {
      java.util.List<?> list = (java.util.List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : list) {
        if (!first) sb.append(",");
        sb.append(toJson(item));
        first = false;
      }
      sb.append("]");
      return sb.toString();
    }
    return "\"" + escapeJson(obj.toString()) + "\"";
  }

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

  private static Map<String, Object> parseJson(String json) {
    // Minimal JSON parser for state file (only handles simple object with string values).
    Map<String, Object> result = new LinkedHashMap<>();
    json = json.trim();
    if (!json.startsWith("{") || !json.endsWith("}")) return result;
    json = json.substring(1, json.length() - 1).trim();
    if (json.isEmpty()) return result;
    for (String pair : json.split(",")) {
      pair = pair.trim();
      int colonIdx = pair.indexOf(":");
      if (colonIdx > 0) {
        String key = pair.substring(0, colonIdx).trim().replaceAll("^\"|\"$", "");
        String value = pair.substring(colonIdx + 1).trim().replaceAll("^\"|\"$", "");
        result.put(key, value);
      }
    }
    return result;
  }

  private static Map<String, Object> api(String method, String pathName, Object body) throws Exception {
    URI uri = URI.create(API + pathName);
    HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri)
        .method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(toJson(body)))
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);
    if (body != null) {
      requestBuilder.header("Content-Type", "application/json");
    }
    HttpRequest request = requestBuilder.build();
    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    String responseBody = response.body();
    Map<String, Object> data = new LinkedHashMap<>();
    try {
      data = parseJsonResponse(responseBody);
    } catch (Exception e) {
      // Empty response or parse error.
    }
    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String errorMsg = toJson(data);
      if (errorMsg.length() > 300) errorMsg = errorMsg.substring(0, 300);
      throw new Exception(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
    }
    return data;
  }

  private static Map<String, Object> parseJsonResponse(String json) {
    // Minimal JSON parser for API responses.
    Map<String, Object> result = new LinkedHashMap<>();
    json = json.trim();
    if (!json.startsWith("{") || !json.endsWith("}")) return result;
    json = json.substring(1, json.length() - 1).trim();
    if (json.isEmpty()) return result;
    int depth = 0;
    StringBuilder currentPair = new StringBuilder();
    for (char c : json.toCharArray()) {
      if ((c == ',' || c == '}') && depth == 0) {
        if (currentPair.length() > 0) {
          parsePair(currentPair.toString().trim(), result);
          currentPair = new StringBuilder();
        }
      } else {
        if (c == '{' || c == '[') depth++;
        if (c == '}' || c == ']') depth--;
        currentPair.append(c);
      }
    }
    if (currentPair.length() > 0) {
      parsePair(currentPair.toString().trim(), result);
    }
    return result;
  }

  private static void parsePair(String pair, Map<String, Object> map) {
    int colonIdx = pair.indexOf(":");
    if (colonIdx > 0) {
      String key = pair.substring(0, colonIdx).trim().replaceAll("^\"|\"$", "");
      String value = pair.substring(colonIdx + 1).trim();
      if (value.startsWith("\"") && value.endsWith("\"")) {
        map.put(key, value.substring(1, value.length() - 1));
      } else if (value.equals("null")) {
        map.put(key, null);
      } else {
        map.put(key, value);
      }
    }
  }

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

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

    // Trigger step
    Map<String, Object> trigger = new LinkedHashMap<>();
    trigger.put("name", "startTrigger1");
    trigger.put("type", "TRIGGER");
    java.util.List<Map<String, Object>> triggerNext = new java.util.ArrayList<>();
    Map<String, Object> triggerNextStep = new LinkedHashMap<>();
    triggerNextStep.put("step", "parse1");
    triggerNext.add(triggerNextStep);
    trigger.put("next", triggerNext);
    steps.add(trigger);

    // Parse step
    Map<String, Object> parse = new LinkedHashMap<>();
    parse.put("name", "parse1");
    parse.put("type", "PARSE");
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> parseConfigInner = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> textBlock = new LinkedHashMap<>();
    Map<String, Object> agentic = new LinkedHashMap<>();
    agentic.put("enabled", true);
    textBlock.put("agentic", agentic);
    blockOptions.put("text", textBlock);
    parseConfigInner.put("blockOptions", blockOptions);
    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "document");
    parseConfigInner.put("chunkingStrategy", chunkingStrategy);
    parseConfig.put("parseConfig", parseConfigInner);
    parse.put("config", parseConfig);
    java.util.List<Map<String, Object>> parseNext = new java.util.ArrayList<>();
    Map<String, Object> parseNextStep = new LinkedHashMap<>();
    parseNextStep.put("step", "extraction2");
    parseNext.add(parseNextStep);
    parse.put("next", parseNext);
    steps.add(parse);

    // Extract step with full schema
    Map<String, Object> extract = new LinkedHashMap<>();
    extract.put("name", "extraction2");
    extract.put("type", "EXTRACT");
    Map<String, Object> extractConfig = new LinkedHashMap<>();
    Map<String, Object> extractorConfig = new LinkedHashMap<>();
    extractorConfig.put("schema", buildSchema());
    extractorConfig.put("baseProcessor", "extraction_performance");
    Map<String, Object> advancedOptions = new LinkedHashMap<>();
    Map<String, Object> reviewAgent = new LinkedHashMap<>();
    reviewAgent.put("enabled", true);
    advancedOptions.put("reviewAgent", reviewAgent);
    advancedOptions.put("advancedMultimodalEnabled", true);
    extractorConfig.put("advancedOptions", advancedOptions);
    extractConfig.put("extractorConfig", extractorConfig);
    extract.put("config", extractConfig);
    steps.add(extract);

    workflow.put("steps", steps);
    return workflow;
  }

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

    java.util.List<String> required = new java.util.ArrayList<>();
    required.add("parties");
    required.add("signatures");
    required.add("governing_law");
    required.add("payment_terms");
    required.add("renewal_terms");
    required.add("term_end_date");
    required.add("agreement_date");
    required.add("notices_clause");
    required.add("agreement_title");
    required.add("term_start_date");
    required.add("liability_clause");
    required.add("termination_clause");
    required.add("definitions_section");
    required.add("confidentiality_clause");
    required.add("dispute_resolution_clause");
    required.add("agreement_reference_number");
    schema.put("required", required);

    Map<String, Object> properties = new LinkedHashMap<>();

    // parties property
    Map<String, Object> partiesSchema = new LinkedHashMap<>();
    partiesSchema.put("type", "array");
    Map<String, Object> partiesItem = new LinkedHashMap<>();
    partiesItem.put("type", "object");
    java.util.List<String> partiesRequired = new java.util.ArrayList<>();
    partiesRequired.add("party_name");
    partiesRequired.add("party_role");
    partiesRequired.add("party_address");
    partiesItem.put("required", partiesRequired);
    Map<String, Object> partiesProps = new LinkedHashMap<>();
    Map<String, Object> partyName = new LinkedHashMap<>();
    partyName.put("type", new String[]{"string", "null"});
    partyName.put("description", "The full legal name of the party to the agreement. This may be a company, organization, or individual.");
    partiesProps.put("party_name", partyName);
    Map<String, Object> partyRole = new LinkedHashMap<>();
    partyRole.put("type", new String[]{"string", "null"});
    partyRole.put("description", "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'.");
    partiesProps.put("party_role", partyRole);
    Map<String, Object> partyAddress = new LinkedHashMap<>();
    partyAddress.put("type", new String[]{"string", "null"});
    partyAddress.put("description", "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country.");
    partiesProps.put("party_address", partyAddress);
    partiesItem.put("properties", partiesProps);
    partiesItem.put("additionalProperties", false);
    partiesSchema.put("items", partiesItem);
    partiesSchema.put("description", "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee').");
    properties.put("parties", partiesSchema);

    // signatures property
    Map<String, Object> signaturesSchema = new LinkedHashMap<>();
    signaturesSchema.put("type", "array");
    Map<String, Object> signaturesItem = new LinkedHashMap<>();
    signaturesItem.put("type", "object");
    java.util.List<String> signaturesRequired = new java.util.ArrayList<>();
    signaturesRequired.add("signatory_name");
    signaturesRequired.add("signature_date");
    signaturesRequired.add("signatory_party");
    signaturesRequired.add("signatory_title");
    signaturesItem.put("required", signaturesRequired);
    Map<String, Object> signaturesProps = new LinkedHashMap<>();
    Map<String, Object> signatoryName = new LinkedHashMap<>();
    signatoryName.put("type", new String[]{"string", "null"});
    signatoryName.put("description", "The name of the individual signing the agreement on behalf of a party.");
    signaturesProps.put("signatory_name", signatoryName);
    Map<String, Object> signatureDate = new LinkedHashMap<>();
    signatureDate.put("type", new String[]{"string", "null"});
    signatureDate.put("description", "The date on which the agreement was signed by this signatory.");
    signatureDate.put("extend:type", "date");
    signaturesProps.put("signature_date", signatureDate);
    Map<String, Object> signatoryParty = new LinkedHashMap<>();
    signatoryParty.put("type", new String[]{"string", "null"});
    signatoryParty.put("description", "The party on whose behalf the individual is signing.");
    signaturesProps.put("signatory_party", signatoryParty);
    Map<String, Object> signatoryTitle = new LinkedHashMap<>();
    signatoryTitle.put("type", new String[]{"string", "null"});
    signatoryTitle.put("description", "The title or position of the signatory within the organization, if applicable.");
    signaturesProps.put("signatory_title", signatoryTitle);
    signaturesItem.put("properties", signaturesProps);
    signaturesItem.put("additionalProperties", false);
    signaturesSchema.put("items", signaturesItem);
    signaturesSchema.put("description", "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed.");
    properties.put("signatures", signaturesSchema);

    // Simple string properties
    String[] simpleProps = {
        "governing_law", "payment_terms", "renewal_terms", "notices_clause",
        "agreement_title", "liability_clause", "termination_clause",
        "definitions_section", "confidentiality_clause", "dispute_resolution_clause",
        "agreement_reference_number"
    };
    String[] descriptions = {
        "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'.",
        "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar.",
        "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements.",
        "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing.",
        "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'.",
        "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps.",
        "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination.",
        "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'.",
        "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar.",
        "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings.",
        "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar."
    };
    for (int i = 0; i < simpleProps.length; i++) {
      Map<String, Object> prop = new LinkedHashMap<>();
      prop.put("type", new String[]{"string", "null"});
      prop.put("description", descriptions[i]);
      properties.put(simpleProps[i], prop);
    }

    // Date properties
    String[] dateProps = {"term_end_date", "agreement_date", "term_start_date"};
    String[] dateDescriptions = {
        "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent.",
        "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations.",
        "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context."
    };
    for (int i = 0; i < dateProps.length; i++) {
      Map<String, Object> prop = new LinkedHashMap<>();
      prop.put("type", new String[]{"string", "null"});
      prop.put("description", dateDescriptions[i]);
      prop.put("extend:type", "date");
      properties.put(dateProps[i], prop);
    }

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

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

      String workflowId = (String) state.get("workflowId");
      if (workflowId != null && !workflowId.isEmpty()) {
        System.out.println("✓ workflow already provisioned (" + workflowId + ") — updating steps");
        Map<String, Object> updateBody = new LinkedHashMap<>();
        updateBody.put("steps", workflow.get("steps"));
        api("POST", "/workflows/" + workflowId, updateBody);
      } else {
        // Try to find existing workflow by name
        try {
          String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
          Map<String, Object> listResponse = api("GET", "/workflows?name=" + encodedName, null);
          java.util.List<?> items = (java.util.List<?>) (listResponse.get("data") != null ? listResponse.get("data") : listResponse.get("items"));
          if (items != null) {
            for (Object item : items) {
              if (item instanceof Map) {
                Map<?, ?> itemMap = (Map<?, ?>) item;
                if (workflowName.equals(itemMap.get("name"))) {
                  String existingId = (String) itemMap.get("id");
                  if (existingId != null && !existingId.isEmpty()) {
                    state.put("workflowId", existingId);
                    saveState();
                    System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
                    Map<String, Object> updateBody = new LinkedHashMap<>();
                    updateBody.put("steps", workflow.get("steps"));
                    api("POST", "/workflows/" + existingId, updateBody);
                    workflowId = existingId;
                  }
                  break;
                }
              }
            }
          }
        } catch (Exception e) {
          // Lookup is best-effort; fall through to create
        }

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

      // Deploy the current draft as a new version
      try {
        api("POST", "/workflows/" + workflowId + "/versions", new LinkedHashMap<>());
      } catch (Exception e) {
        // Best-effort; some accounts may not require this
      }

      System.out.println("\nDone. Run documents through it with:");
      System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + workflowId + "\" }, file: { url: \"https://…\" } }");
      System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
    } catch (Exception e) {
      System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
      System.exit(1);
    }
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// Call the REST API endpoints directly using only Go's standard library (net/http, encoding/json).

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 WorkflowResponse struct {
	ID       string `json:"id,omitempty"`
	Workflow struct {
		ID string `json:"id,omitempty"`
	} `json:"workflow,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"`
}

var workflow = map[string]interface{}{
	"name": "Legal Contract Processing Pipeline",
	"steps": []map[string]interface{}{
		{
			"name": "startTrigger1",
			"type": "TRIGGER",
			"next": []map[string]interface{}{
				{"step": "parse1"},
			},
		},
		{
			"name": "parse1",
			"type": "PARSE",
			"config": map[string]interface{}{
				"parseConfig": map[string]interface{}{
					"blockOptions": map[string]interface{}{
						"text": map[string]interface{}{
							"agentic": map[string]interface{}{
								"enabled": true,
							},
						},
					},
					"chunkingStrategy": map[string]interface{}{
						"type": "document",
					},
				},
			},
			"next": []map[string]interface{}{
				{"step": "extraction2"},
			},
		},
		{
			"name": "extraction2",
			"type": "EXTRACT",
			"config": map[string]interface{}{
				"extractorConfig": map[string]interface{}{
					"schema": map[string]interface{}{
						"type": "object",
						"required": []string{
							"parties", "signatures", "governing_law", "payment_terms",
							"renewal_terms", "term_end_date", "agreement_date", "notices_clause",
							"agreement_title", "term_start_date", "liability_clause", "termination_clause",
							"definitions_section", "confidentiality_clause", "dispute_resolution_clause",
							"agreement_reference_number",
						},
						"properties": map[string]interface{}{
							"parties": map[string]interface{}{
								"type": "array",
								"items": map[string]interface{}{
									"type": "object",
									"required": []string{"party_name", "party_role", "party_address"},
									"properties": map[string]interface{}{
										"party_name": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The full legal name of the party to the agreement. This may be a company, organization, or individual.",
										},
										"party_role": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The role or designation of the party within the agreement, such as 'Buyer', 'Seller', 'Licensor', 'Licensee', 'Employee', or 'Consultant'.",
										},
										"party_address": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The official address of the party as stated in the agreement. May include street, city, state, postal code, and country.",
										},
									},
									"additionalProperties": false,
								},
								"description": "The entities or individuals entering into the agreement. Each party typically includes a name and may include additional identifying information such as address or role (e.g., 'First Party', 'Lessor', 'Employee').",
							},
							"signatures": map[string]interface{}{
								"type": "array",
								"items": map[string]interface{}{
									"type": "object",
									"required": []string{"signatory_name", "signature_date", "signatory_party", "signatory_title"},
									"properties": map[string]interface{}{
										"signatory_name": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The name of the individual signing the agreement on behalf of a party.",
										},
										"signature_date": map[string]interface{}{
											"type":           []string{"string", "null"},
											"description":    "The date on which the agreement was signed by this signatory.",
											"extend:type":    "date",
										},
										"signatory_party": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The party on whose behalf the individual is signing.",
										},
										"signatory_title": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "The title or position of the signatory within the organization, if applicable.",
										},
									},
									"additionalProperties": false,
								},
								"description": "The signatures or signature blocks of the parties to the agreement, indicating acceptance and execution. Each entry typically includes the signatory's name, title, and date signed.",
							},
							"governing_law": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The jurisdiction or legal system under which the agreement is interpreted and enforced. Commonly labeled as 'Governing Law', 'Jurisdiction', or 'Applicable Law'.",
							},
							"payment_terms": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The terms and conditions related to payments under the agreement, such as amounts, due dates, methods, and penalties for late payment. May be labeled as 'Payment Terms', 'Compensation', or similar.",
							},
							"renewal_terms": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary describing any provisions for renewal or extension of the agreement, including automatic renewal clauses or renewal notice requirements.",
							},
							"term_end_date": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The date on which the agreement expires or terminates, if applicable. May be labeled as 'End Date', 'Expiration Date', or similar. If the agreement is perpetual or auto-renewing, this may be absent.",
								"extend:type": "date",
							},
							"agreement_date": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The effective date when the agreement becomes valid or enforceable. This may be labeled as 'Effective Date', 'Date of Agreement', or similar, and is critical for determining the start of contractual obligations.",
								"extend:type": "date",
							},
							"notices_clause": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary specifying how formal notices must be delivered between parties, including addresses, methods, and timing.",
							},
							"agreement_title": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The official title or heading of the legal agreement or contract. This typically summarizes the nature or purpose of the agreement, such as 'Service Agreement', 'Non-Disclosure Agreement', or 'Lease Contract'.",
							},
							"term_start_date": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The date on which the contractual obligations begin. May be labeled as 'Commencement Date', 'Start Date', or similar. If not explicitly stated, may be inferred from context.",
								"extend:type": "date",
							},
							"liability_clause": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary describing the allocation or limitation of liability between the parties. May include indemnification, damages, or liability caps.",
							},
							"termination_clause": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary describing the conditions and procedures for terminating the agreement. May include notice periods, causes for termination, and effects of termination.",
							},
							"definitions_section": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary listing defined terms used throughout the agreement, typically labeled as 'Definitions' or 'Interpretation'.",
							},
							"confidentiality_clause": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary outlining obligations regarding the protection of confidential information. May be labeled as 'Confidentiality', 'Non-Disclosure', or similar.",
							},
							"dispute_resolution_clause": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The section or summary outlining the process for resolving disputes under the agreement, such as arbitration, mediation, or court proceedings.",
							},
							"agreement_reference_number": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "A unique identifier or reference number assigned to this agreement or contract, if present. May be labeled as 'Agreement Number', 'Contract ID', or similar.",
							},
						},
						"additionalProperties": false,
					},
					"baseProcessor": "extraction_performance",
					"advancedOptions": map[string]interface{}{
						"reviewAgent": map[string]interface{}{
							"enabled": true,
						},
						"advancedMultimodalEnabled": true,
					},
				},
			},
		},
	},
}

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

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

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("EXTEND_API_KEY")))
	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
	}

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

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

	return data, nil
}

func loadState(stateFile string) (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(stateFile string, state State) error {
	stateDir := filepath.Dir(stateFile)
	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 main() {
	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
		os.Exit(1)
	}

	stateDir := filepath.Join(".", ".extend")
	stateFile := filepath.Join(stateDir, "legal-contract.json")

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

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

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

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

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

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

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

			state.WorkflowID = wfID
			if err := saveState(stateFile, state); err != nil {
				fmt.Fprintf(os.Stderr, "Error saving 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), map[string]interface{}{})

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

Frequently Asked Questions (FAQ)

Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
For legal contracts, require minimum `confidence >= 0.85` before automation; below that, flag for manual review. Confidence varies by field — money amounts and party names are usually >0.9, but complex multi-sentence obligations may score 0.70–0.80 legitimately.
Use `baseProcessor: "extraction_performance"` (not light) for contracts since accuracy is non-negotiable, run extraction async with `createAndPoll()` to handle longer processing times, and implement a review queue for low-confidence results (0.70–0.85) rather than blocking on all extractions.
Tags
Legal ServicesAttorney AgreementRetainerFee Structure
About this template

This template captures essential elements of service contracts between providers and clients, including scope of work, fee structures, cost reimbursement terms, and termination conditions. It extracts retainer agreements, hourly rates, and expense provisions.

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