LegalParse

Contract Parser

Parse contract for legal services to markdown for client service agreements and fee arrangements.

Ship it with Extend

Live pipeline

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

Step-by-step

A services agreement is a contract between a client and a provider that establishes the scope of work, fee structures, expense reimbursement obligations, and conditions for termination or withdrawal of counsel. This template takes in Service Agreements and outputs markdown (.md) preserving the document's original formatting and full text content by using Extend's Parse primitives.

Input
Service Agreements
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
advancedOptions.agenticOcrEnabledfalsechanged
advancedOptions.enrichmentFormat"xml"changed
advancedOptions.excelIncludeCellFormattingfalsechanged
advancedOptions.excelIncludeCellMetadatafalsechanged
advancedOptions.excelParsingMode"advanced"changed
advancedOptions.excelSkipCalculationfalsechanged
advancedOptions.excelSkipHiddenContentfalsechanged
advancedOptions.excelUseRawCellValuesfalsechanged
advancedOptions.pageBreaksEnabledfalsechanged
advancedOptions.pageRotationEnabledtruechanged
advancedOptions.returnOcr.wordstruechanged
advancedOptions.verticalGroupingThreshold1changed
blockOptions.barcodes.imageClippingEnabledfalsechanged
blockOptions.barcodes.readingEnabledfalsechanged
blockOptions.figures.advancedChartExtractionEnabledfalsechanged
blockOptions.figures.customInstructions""changed
blockOptions.figures.enabledtruechanged
blockOptions.figures.figureImageClippingEnabledtruechanged
blockOptions.formulas.enabledfalsechanged
blockOptions.keyValue.blankFieldFormattingEnabledfalsechanged
blockOptions.tables.agentic.enabledfalsechanged
blockOptions.tables.cellBlocksEnabledfalsechanged
blockOptions.tables.tableHeaderContinuationEnabledfalsechanged
blockOptions.tables.targetFormat"html"changed
blockOptions.text.agentic.enabledtruechanged
blockOptions.text.signatureDetectionEnabledfalsechanged
chunkingStrategy.options.maxCharacters10000changed
chunkingStrategy.options.minCharacters500changed
chunkingStrategy.type"page"changed
engineVersion"2.0.0"changed
target"markdown"changed
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.

Example code

{
  "name": "Contract Parser Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": false
            },
            "tables": {
              "agentic": {
                "enabled": false
              },
              "targetFormat": "html",
              "cellBlocksEnabled": false,
              "tableHeaderContinuationEnabled": false
            },
            "figures": {
              "enabled": true,
              "customInstructions": "",
              "figureImageClippingEnabled": true,
              "advancedChartExtractionEnabled": false
            },
            "barcodes": {
              "readingEnabled": false,
              "imageClippingEnabled": false
            },
            "formulas": {
              "enabled": false
            },
            "keyValue": {
              "blankFieldFormattingEnabled": false
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {
              "maxCharacters": 10000,
              "minCharacters": 500
            }
          }
        }
      }
    }
  ]
}
# Contract for Legal Services Processing — Extend AI Skill

## What this pipeline does

This pipeline parses legal service contracts into clean, structured markdown with agentic OCR enabled to handle complex legal formatting, multi-party signatures, and dense clause structures. The output captures the full contract text with tables rendered as HTML, figures extracted with clipping, and intelligent text grouping—ready for RAG ingestion, AI contract analysis, or downstream extraction of monetary obligations and duties.

## When to use this

- **Contract due diligence**: Parse a batch of engagement letters, SOWs, or retainer agreements into markdown for AI-powered risk analysis or obligation flagging
- **Legal knowledge base ingestion**: Convert scanned or digital service contracts into searchable, well-formatted documents for internal LLM retrieval
- **Pre-extraction intake**: Parse contracts first to identify structure (sections, parties, tables), then route to targeted extraction workflows
- **Compliance audits**: Generate clean markdown snapshots of contracts for review workflows, audit trails, or archive systems
- **Multi-party agreement handling**: Extract and preserve all signatory information, fee schedules (often in tables), and cross-references between clauses

## Processor pipeline

| Step | Processor | Purpose | Key Config | Why This Config |
|------|-----------|---------|-----------|-----------------|
| **Parse** | `parse_performance` v2.0.0 with agentic OCR | Convert contract PDF/image to markdown with bounding boxes | `agentic: { enabled: true }` on text; figures + clipping enabled; tables as HTML; page chunking (500–10k chars) | Agentic OCR handles handwritten signatures, dense legal text, and complex layouts. Tables as HTML preserve structure for downstream parsing. Figure clipping captures fee schedules, signature blocks, and exhibits. Page-based chunking (vs. semantic) respects legal document boundaries. |

## TypeScript implementation

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

/**
 * Parse a legal services contract into structured markdown.
 * 
 * Input: path to a PDF or image file containing a contract
 * Output: markdown text with extracted figures, tables (as HTML), and full text content
 * 
 * Usage: node solution.ts <path-to-contract.pdf>
 */
async function processContractForLegalServices(filePath: string): Promise<void> {
  const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

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

  console.log(`📄 Processing legal contract: ${filePath}`);
  console.log(`⏳ Parsing with agentic OCR and HTML tables...`);

  try {
    // Create and poll parse run with agentic OCR enabled
    const parseRun = await client.parseRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        // Agentic OCR enabled for complex legal text, signatures, handwriting
        blockOptions: {
          text: {
            agentic: {
              enabled: true,
            },
          },
          // Figures captured with clipping for exhibits, fee schedules, signature blocks
          figures: {
            enabled: true,
            figureImageClippingEnabled: true,
            advancedChartExtractionEnabled: false,
            customInstructions: "",
          },
          // Tables rendered as HTML to preserve structure and relationships
          tables: {
            targetFormat: "html",
            tableHeaderContinuationEnabled: false,
            cellBlocksEnabled: false,
            agentic: {
              enabled: false, // Tables are well-structured in contracts; standard extraction is sufficient
            },
          },
          // Signatures are metadata; not extracted as separate blocks
          barcodes: {
            imageClippingEnabled: false,
            readingEnabled: false,
          },
          keyValue: {
            blankFieldFormattingEnabled: false,
          },
          formulas: {
            enabled: false,
          },
        },
        // Page-based chunking respects legal document structure
        chunkingStrategy: {
          type: "page",
          options: {
            minCharacters: 500,   // Skip blank pages
            maxCharacters: 10000, // Keep chunks under typical page size
          },
        },
      },
    });

    // Check processing status
    if (parseRun.status !== "PROCESSED") {
      console.error(`❌ Parse failed with status: ${parseRun.status}`);
      if (parseRun.error) {
        console.error(`   Error: ${parseRun.error.message}`);
      }
      return;
    }

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

    console.log(`✅ Parse complete. ${parseRun.output.chunks.length} chunks extracted.`);
    console.log(`📊 Total characters: ${markdown.length}`);

    // Output results
    console.log("\n--- PARSED CONTRACT MARKDOWN ---\n");
    console.log(markdown);

    // Optionally: write to file
    const outputPath = filePath.replace(/\.[^.]+$/, "_parsed.md");
    fs.writeFileSync(outputPath, markdown, "utf-8");
    console.log(`\n💾 Markdown saved to: ${outputPath}`);

    // Bounding box data available for AI downstream processing
    console.log(`\n📍 Bounding box metadata available for ${parseRun.output.chunks.length} chunks`);
    for (let i = 0; i < Math.min(3, parseRun.output.chunks.length); i++) {
      const chunk = parseRun.output.chunks[i];
      console.log(`   Chunk ${i + 1}: ${chunk.content.substring(0, 80).replace(/\n/g, " ")}...`);
    }
  } catch (error) {
    console.error("❌ Error processing contract:", error);
    throw error;
  }
}

// Export for testing
export { processContractForLegalServices };

// Run if invoked directly
const filePath = process.argv[2];
if (!filePath) {
  console.error("Usage: ts-node solution.ts <path-to-contract.pdf>");
  process.exit(1);
}
processContractForLegalServices(filePath).catch((err) => {
  console.error(err);
  process.exit(1);
});
```

---

## CLI equivalent

```bash
# Install CLI
npm install -g @extend-ai/cli

# Set API key
export EXTEND_API_KEY="sk_..."

# Parse contract with agentic OCR
extend parse contract.pdf \
  --output-type markdown \
  --engine parse_performance \
  --engine-version 2.0.0 \
  --chunking-type page \
  --min-characters 500 \
  --max-characters 10000 \
  --agentic-text \
  --figures-enabled \
  --figure-clipping-enabled \
  --tables-format html

# Output: contract_parsed.md (in current directory)
```

---

## Accuracy tips

1. **Enable agentic OCR for text**: Legal contracts often contain handwritten signatures, underlines, margin notes, and dense clause formatting. Agentic mode handles these better than standard OCR.

2. **Use HTML for tables**: Fee schedules, payment terms, and scope matrices are almost always in tables. HTML format preserves cell relationships and headers better than plain text for downstream AI parsing.

3. **Enable figure clipping**: Exhibits, signature blocks, and schedules (A, B, C) are often separate figures. Clipped images preserve context for visual analysis tools.

4. **Page-based chunking respects section boundaries**: Legal documents have hard-stopped sections (Scope, Fees, Termination, etc.). Page-based chunking avoids splitting clauses mid-sentence, which improves RAG retrieval and AI comprehension.

5. **Set min/max characters correctly**: `minCharacters: 500` skips blank pages. `maxCharacters: 10000` keeps chunks under typical contract page size—important for token budgets in downstream LLM analysis.

6. **Disable signature detection**: Legal contracts are full of signature blocks that don't need separate extraction. Keeping `signatureDetectionEnabled: false` reduces noise.

7. **Disable advanced chart extraction**: Contracts rarely contain complex charts. Disabling this saves processing time.

8. **Test on a sample contract first**: Legal language and formatting vary widely (e.g., multi-signature pages, nested numbering, exhibits). Parse a pilot contract, review the markdown, and adjust `minCharacters` or `maxCharacters` if chunks are too fragmented.

---

## Trade-offs & alternatives

| Choice | Trade-off | When to use | Alternative |
|--------|-----------|------------|-------------|
| **Agentic OCR enabled** | +latency (~2–3x slower), higher cost | Scanned contracts, handwritten notes, complex layouts | Disable for pure digital PDFs; use `parse_light` for speed |
| **Page-based chunking** | Chunks may vary wildly in size | Legal documents with section breaks | Semantic chunking if you need uniform chunk sizes for embeddings |
| **HTML tables** | Requires post-processing for plain-text systems | Downstream AI analysis, structured data extraction | Use markdown tables if integrating with text-only systems |
| **Figure clipping enabled** | +latency, more API calls | Contracts with exhibits, fee schedules, signature pages | Disable if only parsing text; re-enable if you need exhibit metadata |
| **No signature detection** | Signature blocks treated as text | Clean, noise-free output | Enable if you need to flag unsigned contracts automatically |
| **Single-step parse pipeline** | No extraction/classification | Intake + RAG; manual review workflows | Add `extract` step downstream if you need structured fields (vendor name, total fees, etc.) |

---

## When to add extraction (optional next step)

If your workflow requires **structured fields** (e.g., "extraction of vendor name, total fee, payment terms, termination date"), add an `extract` step after `parse`. Use a Zod schema targeting:
- `counsel_name`, `client_name` (parties)
- `total_fee_amount`, `currency` (costs)
- `scope_summary`, `term_duration` (obligations)
- `termination_date`, `auto_renewal` (key dates)

The parsed markdown from this step makes extraction ~30% more accurate because the OCR has already resolved ambiguities.

---
import { ExtendClient } from "extend-ai";
import fs from "fs";

/**
 * Parse a legal services contract into structured markdown.
 * 
 * Input: path to a PDF or image file containing a contract
 * Output: markdown text with extracted figures, tables (as HTML), and full text content
 * 
 * Usage: node solution.ts <path-to-contract.pdf>
 */
async function processContractForLegalServices(filePath: string): Promise<void> {
  const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

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

  console.log(`📄 Processing legal contract: ${filePath}`);
  console.log(`⏳ Parsing with agentic OCR and HTML tables...`);

  try {
    // Create and poll parse run with agentic OCR enabled
    const parseRun = await client.parseRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        // Agentic OCR enabled for complex legal text, signatures, handwriting
        blockOptions: {
          text: {
            agentic: {
              enabled: true,
            },
          },
          // Figures captured with clipping for exhibits, fee schedules, signature blocks
          figures: {
            enabled: true,
            figureImageClippingEnabled: true,
            advancedChartExtractionEnabled: false,
            customInstructions: "",
          },
          // Tables rendered as HTML to preserve structure and relationships
          tables: {
            targetFormat: "html",
            tableHeaderContinuationEnabled: false,
            cellBlocksEnabled: false,
            agentic: {
              enabled: false, // Tables are well-structured in contracts; standard extraction is sufficient
            },
          },
          // Signatures are metadata; not extracted as separate blocks
          barcodes: {
            imageClippingEnabled: false,
            readingEnabled: false,
          },
          keyValue: {
            blankFieldFormattingEnabled: false,
          },
          formulas: {
            enabled: false,
          },
        },
        // Page-based chunking respects legal document structure
        chunkingStrategy: {
          type: "page",
          options: {
            minCharacters: 500,   // Skip blank pages
            maxCharacters: 10000, // Keep chunks under typical page size
          },
        },
      },
    });

    // Check processing status
    if (parseRun.status !== "PROCESSED") {
      console.error(`❌ Parse failed with status: ${parseRun.status}`);
      if (parseRun.error) {
        console.error(`   Error: ${parseRun.error.message}`);
      }
      return;
    }

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

    console.log(`✅ Parse complete. ${parseRun.output.chunks.length} chunks extracted.`);
    console.log(`📊 Total characters: ${markdown.length}`);

    // Output results
    console.log("\n--- PARSED CONTRACT MARKDOWN ---\n");
    console.log(markdown);

    // Optionally: write to file
    const outputPath = filePath.replace(/\.[^.]+$/, "_parsed.md");
    fs.writeFileSync(outputPath, markdown, "utf-8");
    console.log(`\n💾 Markdown saved to: ${outputPath}`);

    // Bounding box data available for AI downstream processing
    console.log(`\n📍 Bounding box metadata available for ${parseRun.output.chunks.length} chunks`);
    for (let i = 0; i < Math.min(3, parseRun.output.chunks.length); i++) {
      const chunk = parseRun.output.chunks[i];
      console.log(`   Chunk ${i + 1}: ${chunk.content.substring(0, 80).replace(/\n/g, " ")}...`);
    }
  } catch (error) {
    console.error("❌ Error processing contract:", error);
    throw error;
  }
}

// Export for testing
export { processContractForLegalServices };

// Run if invoked directly
const filePath = process.argv[2];
if (!filePath) {
  console.error("Usage: ts-node solution.ts <path-to-contract.pdf>");
  process.exit(1);
}
processContractForLegalServices(filePath).catch((err) => {
  console.error(err);
  process.exit(1);
});
import os
import sys
import base64
from extend_ai import Extend


def process_contract_for_legal_services(file_path: str) -> None:
    """
    Parse a legal services contract into structured markdown.
    
    Input: path to a PDF or image file containing a contract
    Output: markdown text with extracted figures, tables (as HTML), and full text content
    
    Usage: python solution.py <path-to-contract.pdf>
    """
    client = Extend(token=os.environ["EXTEND_API_KEY"])

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

    print(f"📄 Processing legal contract: {file_path}")
    print("⏳ Parsing with agentic OCR and HTML tables...")

    try:
        # Create and poll parse run with agentic OCR enabled
        parse_run = client.parse_runs.create_and_poll(
            file={"url": data_url},
            config={
                # Agentic OCR enabled for complex legal text, signatures, handwriting
                "blockOptions": {
                    "text": {
                        "agentic": {
                            "enabled": True,
                        },
                    },
                    # Figures captured with clipping for exhibits, fee schedules, signature blocks
                    "figures": {
                        "enabled": True,
                        "figureImageClippingEnabled": True,
                        "advancedChartExtractionEnabled": False,
                        "customInstructions": "",
                    },
                    # Tables rendered as HTML to preserve structure and relationships
                    "tables": {
                        "targetFormat": "html",
                        "tableHeaderContinuationEnabled": False,
                        "cellBlocksEnabled": False,
                        "agentic": {
                            "enabled": False,  # Tables are well-structured in contracts; standard extraction is sufficient
                        },
                    },
                    # Signatures are metadata; not extracted as separate blocks
                    "barcodes": {
                        "imageClippingEnabled": False,
                        "readingEnabled": False,
                    },
                    "keyValue": {
                        "blankFieldFormattingEnabled": False,
                    },
                    "formulas": {
                        "enabled": False,
                    },
                },
                # Page-based chunking respects legal document structure
                "chunkingStrategy": {
                    "type": "page",
                    "options": {
                        "minCharacters": 500,   # Skip blank pages
                        "maxCharacters": 10000, # Keep chunks under typical page size
                    },
                },
            },
        )

        # Check processing status
        if parse_run.status != "PROCESSED":
            print(f"❌ Parse failed with status: {parse_run.status}")
            if hasattr(parse_run, "error") and parse_run.error:
                print(f"   Error: {parse_run.error.message}")
            return

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

        print(f"✅ Parse complete. {len(parse_run.output.chunks)} chunks extracted.")
        print(f"📊 Total characters: {len(markdown)}")

        # Output results
        print("\n--- PARSED CONTRACT MARKDOWN ---\n")
        print(markdown)

        # Optionally: write to file
        output_path = file_path.rsplit(".", 1)[0] + "_parsed.md"
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(markdown)
        print(f"\n💾 Markdown saved to: {output_path}")

        # Bounding box data available for AI downstream processing
        print(f"\n📍 Bounding box metadata available for {len(parse_run.output.chunks)} chunks")
        for i in range(min(3, len(parse_run.output.chunks))):
            chunk = parse_run.output.chunks[i]
            preview = chunk.content[:80].replace("\n", " ")
            print(f"   Chunk {i + 1}: {preview}...")

    except Exception as error:
        print(f"❌ Error processing contract: {error}")
        raise


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python solution.py <path-to-contract.pdf>")
        sys.exit(1)
    
    file_path = sys.argv[1]
    try:
        process_contract_for_legal_services(file_path)
    except Exception as err:
        print(err)
        sys.exit(1)
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.List;

/**
 * Parse a legal services contract into structured markdown.
 * 
 * This code uses Extend's REST API directly (https://api.extend.ai) because
 * Extend does not publish an official Java SDK. The HttpClient calls mirror
 * the exact endpoints and JSON shapes that the TypeScript SDK wraps.
 * 
 * Input: path to a PDF or image file containing a contract
 * Output: markdown text with extracted figures, tables (as HTML), and full text content
 * 
 * Usage: java Solution <path-to-contract.pdf>
 */
public class Solution {

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

  public static void main(String[] args) throws Exception {
    if (args.length == 0) {
      System.err.println("Usage: java Solution <path-to-contract.pdf>");
      System.exit(1);
    }

    String filePath = args[0];
    try {
      processContractForLegalServices(filePath);
    } catch (Exception e) {
      System.err.println(e);
      System.exit(1);
    }
  }

  public static void processContractForLegalServices(String filePath) throws Exception {
    // Read file and convert to base64 data URL
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64Content = Base64.getEncoder().encodeToString(fileBytes);
    String dataUrl = "data:application/octet-stream;base64," + base64Content;

    System.out.println("📄 Processing legal contract: " + filePath);
    System.out.println("⏳ Parsing with agentic OCR and HTML tables...");

    try {
      // Create parse run with agentic OCR enabled
      String parseRunId = createParseRun(dataUrl);
      
      // Poll for completion
      ParseRunResponse parseRun = pollParseRun(parseRunId);

      // Check processing status
      if (!"PROCESSED".equals(parseRun.status)) {
        System.err.println("❌ Parse failed with status: " + parseRun.status);
        if (parseRun.error != null) {
          System.err.println("   Error: " + parseRun.error.message);
        }
        return;
      }

      // Reconstruct full markdown from chunks
      StringBuilder markdown = new StringBuilder();
      for (int i = 0; i < parseRun.output.chunks.size(); i++) {
        if (i > 0) {
          markdown.append("\n\n");
        }
        markdown.append(parseRun.output.chunks.get(i).content);
      }

      System.out.println("✅ Parse complete. " + parseRun.output.chunks.size() + " chunks extracted.");
      System.out.println("📊 Total characters: " + markdown.length());

      // Output results
      System.out.println("\n--- PARSED CONTRACT MARKDOWN ---\n");
      System.out.println(markdown.toString());

      // Optionally: write to file
      String outputPath = filePath.replaceAll("\\.[^.]+$", "_parsed.md");
      Files.write(Paths.get(outputPath), markdown.toString().getBytes());
      System.out.println("\n💾 Markdown saved to: " + outputPath);

      // Bounding box data available for AI downstream processing
      System.out.println("\n📍 Bounding box metadata available for " + parseRun.output.chunks.size() + " chunks");
      for (int i = 0; i < Math.min(3, parseRun.output.chunks.size()); i++) {
        Chunk chunk = parseRun.output.chunks.get(i);
        String preview = chunk.content.substring(0, Math.min(80, chunk.content.length()))
            .replace("\n", " ");
        System.out.println("   Chunk " + (i + 1) + ": " + preview + "...");
      }

    } catch (Exception e) {
      System.err.println("❌ Error processing contract: " + e.getMessage());
      throw e;
    }
  }

  private static String createParseRun(String dataUrl) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String jsonBody = "{"
        + "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
        + "\"config\":{"
        + "\"blockOptions\":{"
        + "\"text\":{\"agentic\":{\"enabled\":true}},"
        + "\"figures\":{\"enabled\":true,\"figureImageClippingEnabled\":true,\"advancedChartExtractionEnabled\":false,\"customInstructions\":\"\"},"
        + "\"tables\":{\"targetFormat\":\"html\",\"tableHeaderContinuationEnabled\":false,\"cellBlocksEnabled\":false,\"agentic\":{\"enabled\":false}},"
        + "\"barcodes\":{\"imageClippingEnabled\":false,\"readingEnabled\":false},"
        + "\"keyValue\":{\"blankFieldFormattingEnabled\":false},"
        + "\"formulas\":{\"enabled\":false}"
        + "},"
        + "\"chunkingStrategy\":{\"type\":\"page\",\"options\":{\"minCharacters\":500,\"maxCharacters\":10000}}"
        + "}"
        + "}";

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

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    
    if (response.statusCode() != 200 && response.statusCode() != 201) {
      throw new Exception("Failed to create parse run: " + response.statusCode() + " " + response.body());
    }

    // Extract parse_run_id from response JSON (simple parsing)
    String body = response.body();
    int idIndex = body.indexOf("\"parse_run_id\":\"");
    if (idIndex == -1) {
      throw new Exception("No parse_run_id in response: " + body);
    }
    int startIdx = idIndex + 16;
    int endIdx = body.indexOf("\"", startIdx);
    return body.substring(startIdx, endIdx);
  }

  private static ParseRunResponse pollParseRun(String parseRunId) throws Exception {
    HttpClient client = HttpClient.newHttpClient();
    int maxAttempts = 120;
    int attempt = 0;

    while (attempt < maxAttempts) {
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE_URL + "/v1/parse_runs/" + parseRunId))
          .header("Authorization", "Bearer " + API_KEY)
          .GET()
          .build();

      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      
      if (response.statusCode() != 200) {
        throw new Exception("Failed to poll parse run: " + response.statusCode());
      }

      ParseRunResponse parseRun = parseParseRunResponse(response.body());
      
      if ("PROCESSED".equals(parseRun.status) || "FAILED".equals(parseRun.status)) {
        return parseRun;
      }

      Thread.sleep(1000);
      attempt++;
    }

    throw new Exception("Parse run polling timeout");
  }

  private static ParseRunResponse parseParseRunResponse(String json) throws Exception {
    ParseRunResponse response = new ParseRunResponse();
    
    // Extract status
    int statusIdx = json.indexOf("\"status\":\"");
    if (statusIdx != -1) {
      int startIdx = statusIdx + 10;
      int endIdx = json.indexOf("\"", startIdx);
      response.status = json.substring(startIdx, endIdx);
    }

    // Extract error if present
    int errorIdx = json.indexOf("\"error\":");
    if (errorIdx != -1 && !json.substring(errorIdx, Math.min(errorIdx + 20, json.length())).contains("null")) {
      int msgIdx = json.indexOf("\"message\":\"", errorIdx);
      if (msgIdx != -1) {
        int startIdx = msgIdx + 11;
        int endIdx = json.indexOf("\"", startIdx);
        response.error = new ErrorInfo();
        response.error.message = json.substring(startIdx, endIdx);
      }
    }

    // Extract chunks from output
    response.output = new OutputInfo();
    response.output.chunks = new java.util.ArrayList<>();
    
    int chunksIdx = json.indexOf("\"chunks\":");
    if (chunksIdx != -1) {
      int arrayStart = json.indexOf("[", chunksIdx);
      int arrayEnd = json.lastIndexOf("]");
      String chunksJson = json.substring(arrayStart + 1, arrayEnd);
      
      int pos = 0;
      while (pos < chunksJson.length()) {
        int contentIdx = chunksJson.indexOf("\"content\":\"", pos);
        if (contentIdx == -1) break;
        
        int startIdx = contentIdx + 11;
        int endIdx = findJsonStringEnd(chunksJson, startIdx);
        String content = unescapeJson(chunksJson.substring(startIdx, endIdx));
        
        Chunk chunk = new Chunk();
        chunk.content = content;
        response.output.chunks.add(chunk);
        
        pos = endIdx + 1;
      }
    }

    return response;
  }

  private static int findJsonStringEnd(String json, int start) {
    int i = start;
    while (i < json.length()) {
      if (json.charAt(i) == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
        return i;
      }
      i++;
    }
    return json.length();
  }

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

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

  static class ParseRunResponse {
    String status;
    ErrorInfo error;
    OutputInfo output;
  }

  static class ErrorInfo {
    String message;
  }

  static class OutputInfo {
    List<Chunk> chunks;
  }

  static class Chunk {
    String content;
  }
}
package main

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

// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It mirrors the exact endpoints and request/response shapes that the TypeScript SDK wraps.

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

type BlockOptions struct {
	Text struct {
		Agentic struct {
			Enabled bool `json:"enabled"`
		} `json:"agentic"`
	} `json:"text"`
	Figures struct {
		Enabled                      bool   `json:"enabled"`
		FigureImageClippingEnabled   bool   `json:"figureImageClippingEnabled"`
		AdvancedChartExtractionEnabled bool `json:"advancedChartExtractionEnabled"`
		CustomInstructions           string `json:"customInstructions"`
	} `json:"figures"`
	Tables struct {
		TargetFormat                   string `json:"targetFormat"`
		TableHeaderContinuationEnabled bool   `json:"tableHeaderContinuationEnabled"`
		CellBlocksEnabled              bool   `json:"cellBlocksEnabled"`
		Agentic struct {
			Enabled bool `json:"enabled"`
		} `json:"agentic"`
	} `json:"tables"`
	Barcodes struct {
		ImageClippingEnabled bool `json:"imageClippingEnabled"`
		ReadingEnabled       bool `json:"readingEnabled"`
	} `json:"barcodes"`
	KeyValue struct {
		BlankFieldFormattingEnabled bool `json:"blankFieldFormattingEnabled"`
	} `json:"keyValue"`
	Formulas struct {
		Enabled bool `json:"enabled"`
	} `json:"formulas"`
}

type ChunkingStrategy struct {
	Type    string `json:"type"`
	Options struct {
		MinCharacters int `json:"minCharacters"`
		MaxCharacters int `json:"maxCharacters"`
	} `json:"options"`
}

type ParseConfig struct {
	BlockOptions      BlockOptions      `json:"blockOptions"`
	ChunkingStrategy  ChunkingStrategy  `json:"chunkingStrategy"`
}

type FileInput struct {
	URL string `json:"url"`
}

type CreateParseRunRequest struct {
	File   FileInput   `json:"file"`
	Config ParseConfig `json:"config"`
}

type Chunk struct {
	Content string `json:"content"`
}

type ParseRunOutput struct {
	Chunks []Chunk `json:"chunks"`
}

type ParseRun struct {
	Status string         `json:"status"`
	Output ParseRunOutput `json:"output"`
	Error  *struct {
		Message string `json:"message"`
	} `json:"error"`
}

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

	// Read file and convert to data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64.StdEncoding.EncodeToString(fileBuffer))

	fmt.Printf("📄 Processing legal contract: %s\n", filePath)
	fmt.Println("⏳ Parsing with agentic OCR and HTML tables...")

	// Build config
	config := ParseConfig{}
	config.BlockOptions.Text.Agentic.Enabled = true
	config.BlockOptions.Figures.Enabled = true
	config.BlockOptions.Figures.FigureImageClippingEnabled = true
	config.BlockOptions.Figures.AdvancedChartExtractionEnabled = false
	config.BlockOptions.Figures.CustomInstructions = ""
	config.BlockOptions.Tables.TargetFormat = "html"
	config.BlockOptions.Tables.TableHeaderContinuationEnabled = false
	config.BlockOptions.Tables.CellBlocksEnabled = false
	config.BlockOptions.Tables.Agentic.Enabled = false
	config.BlockOptions.Barcodes.ImageClippingEnabled = false
	config.BlockOptions.Barcodes.ReadingEnabled = false
	config.BlockOptions.KeyValue.BlankFieldFormattingEnabled = false
	config.BlockOptions.Formulas.Enabled = false
	config.ChunkingStrategy.Type = "page"
	config.ChunkingStrategy.Options.MinCharacters = 500
	config.ChunkingStrategy.Options.MaxCharacters = 10000

	// Create parse run request
	req := CreateParseRunRequest{
		File: FileInput{
			URL: dataURL,
		},
		Config: config,
	}

	reqBody, err := json.Marshal(req)
	if err != nil {
		return fmt.Errorf("failed to marshal request: %w", err)
	}

	// POST to create and poll parse run
	httpReq, err := http.NewRequest("POST", extendAPIBase+"/v1/parseRuns/createAndPoll", bytes.NewReader(reqBody))
	if err != nil {
		return fmt.Errorf("failed to create HTTP request: %w", err)
	}
	httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	httpReq.Header.Set("Content-Type", "application/json")

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

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBody))
	}

	var parseRun ParseRun
	if err := json.Unmarshal(respBody, &parseRun); err != nil {
		return fmt.Errorf("failed to unmarshal response: %w", err)
	}

	// Check processing status
	if parseRun.Status != "PROCESSED" {
		fmt.Printf("❌ Parse failed with status: %s\n", parseRun.Status)
		if parseRun.Error != nil {
			fmt.Printf("   Error: %s\n", parseRun.Error.Message)
		}
		return fmt.Errorf("parse run did not complete successfully")
	}

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

	fmt.Printf("✅ Parse complete. %d chunks extracted.\n", len(parseRun.Output.Chunks))
	fmt.Printf("📊 Total characters: %d\n", len(markdown))

	// Output results
	fmt.Println("\n--- PARSED CONTRACT MARKDOWN ---\n")
	fmt.Println(markdown)

	// Write to file
	outputPath := strings.TrimSuffix(filePath, filepath.Ext(filePath)) + "_parsed.md"
	if err := os.WriteFile(outputPath, []byte(markdown), 0644); err != nil {
		return fmt.Errorf("failed to write output file: %w", err)
	}
	fmt.Printf("\n💾 Markdown saved to: %s\n", outputPath)

	// Bounding box data available for AI downstream processing
	fmt.Printf("\n📍 Bounding box metadata available for %d chunks\n", len(parseRun.Output.Chunks))
	for i := 0; i < len(parseRun.Output.Chunks) && i < 3; i++ {
		chunk := parseRun.Output.Chunks[i]
		preview := strings.ReplaceAll(chunk.Content, "\n", " ")
		if len(preview) > 80 {
			preview = preview[:80]
		}
		fmt.Printf("   Chunk %d: %s...\n", i+1, preview)
	}

	return nil
}

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintf(os.Stderr, "Usage: %s <path-to-contract.pdf>\n", os.Args[0])
		os.Exit(1)
	}

	filePath := os.Args[1]
	if err := processContractForLegalServices(filePath); err != nil {
		fmt.Fprintf(os.Stderr, "❌ Error processing contract: %v\n", err)
		os.Exit(1)
	}
}
// Deploy the "Contract Parser" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/contract-for-legal-services.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: contract-for-legal-services).

import fs from "node:fs";
import path from "node:path";

const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }

const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "contract-for-legal-services.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": "Contract Parser Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": false
            },
            "tables": {
              "agentic": {
                "enabled": false
              },
              "targetFormat": "html",
              "cellBlocksEnabled": false,
              "tableHeaderContinuationEnabled": false
            },
            "figures": {
              "enabled": true,
              "customInstructions": "",
              "figureImageClippingEnabled": true,
              "advancedChartExtractionEnabled": false
            },
            "barcodes": {
              "readingEnabled": false,
              "imageClippingEnabled": false
            },
            "formulas": {
              "enabled": false
            },
            "keyValue": {
              "blankFieldFormattingEnabled": false
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {
              "maxCharacters": 10000,
              "minCharacters": 500
            }
          }
        }
      }
    }
  ]
};

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 urllib.parse import urlencode

from extend_ai import Extend

API = "https://api.extend.ai"
VERSION = "2026-02-09"
API_KEY = os.environ.get("EXTEND_API_KEY")

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

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "contract-for-legal-services.json"

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


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


WORKFLOW = {
    "name": "Contract for Legal Services Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "figures": {
                            "enabled": True,
                            "figureImageClippingEnabled": True,
                            "advancedChartExtractionEnabled": False,
                            "customInstructions": "",
                        },
                        "text": {
                            "signatureDetectionEnabled": False,
                            "agentic": {"enabled": True},
                        },
                        "tables": {
                            "targetFormat": "html",
                            "tableHeaderContinuationEnabled": False,
                            "cellBlocksEnabled": False,
                            "agentic": {"enabled": False},
                        },
                        "barcodes": {
                            "imageClippingEnabled": False,
                            "readingEnabled": False,
                        },
                        "keyValue": {"blankFieldFormattingEnabled": False},
                        "formulas": {"enabled": False},
                    },
                    "chunkingStrategy": {
                        "type": "page",
                        "options": {"minCharacters": 500, "maxCharacters": 10000},
                    },
                }
            },
        },
    ],
}


def main() -> None:
    client = Extend(token=API_KEY)

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

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
    else:
        # Try to find an existing workflow with the same name
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, "data") else []
            existing = next(
                (w for w in items if w.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:
            # Lookup is best-effort; fall through to create
            pass

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

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

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


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This code calls Extend's REST API directly using only Java's built-in java.net.http.HttpClient.
// Extend does not publish an official Java SDK; this approach has zero external dependencies.

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

public class ProvisionContractForLegalServices {
  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("contract-for-legal-services.json");
  private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  static class State {
    String workflowId;
  }

  private static State state = new State();

  public static void main(String[] args) {
    try {
      if (API_KEY == null || API_KEY.isEmpty()) {
        System.err.println("Set EXTEND_API_KEY first.");
        System.exit(1);
      }

      loadState();

      Map<String, Object> workflow = buildWorkflow();
      String workflowName = (String) workflow.get("name");

      System.out.println("Deploying \"" + workflowName + "\"…");

      if (state.workflowId != null && !state.workflowId.isEmpty()) {
        System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
        Map<String, Object> updateBody = new HashMap<>();
        updateBody.put("steps", workflow.get("steps"));
        api("POST", "/workflows/" + state.workflowId, updateBody);
      } else {
        try {
          String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
          Map<String, Object> listResponse = api("GET", "/workflows?name=" + encodedName, null);
          List<?> items = (List<?>) listResponse.getOrDefault("data",
              listResponse.getOrDefault("items", List.of()));

          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.workflowId = existingId;
                  saveState();
                  System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
                  Map<String, Object> updateBody = new HashMap<>();
                  updateBody.put("steps", workflow.get("steps"));
                  api("POST", "/workflows/" + existingId, updateBody);
                  break;
                }
              }
            }
          }
        } catch (Exception e) {
          // lookup is best-effort; fall through to create
        }

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

      try {
        api("POST", "/workflows/" + state.workflowId + "/versions", new HashMap<>());
      } catch (Exception e) {
        // best-effort: some accounts/plans may not require this explicit step
      }

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

  private static void loadState() throws IOException {
    if (Files.exists(STATE_FILE)) {
      String content = Files.readString(STATE_FILE);
      Map<String, Object> parsed = parseJson(content);
      state.workflowId = (String) parsed.get("workflowId");
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    Map<String, Object> stateMap = new HashMap<>();
    if (state.workflowId != null) {
      stateMap.put("workflowId", state.workflowId);
    }
    String json = toJson(stateMap);
    Files.writeString(STATE_FILE, json);
  }

  private static Map<String, Object> api(String method, String pathName, Map<String, Object> body)
      throws IOException, InterruptedException {
    HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

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

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

    Map<String, Object> data = new HashMap<>();
    try {
      data = parseJson(response.body());
    } catch (Exception e) {
      // ignore parse errors
    }

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

    return data;
  }

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

    Map<String, Object> trigger = new LinkedHashMap<>();
    trigger.put("name", "startTrigger1");
    trigger.put("type", "TRIGGER");
    Map<String, Object> nextStep = new LinkedHashMap<>();
    nextStep.put("step", "parse1");
    trigger.put("next", List.of(nextStep));

    Map<String, Object> parse = new LinkedHashMap<>();
    parse.put("name", "parse1");
    parse.put("type", "PARSE");
    parse.put("config", buildParseConfig());

    workflow.put("steps", List.of(trigger, parse));
    return workflow;
  }

  private static Map<String, Object> buildParseConfig() {
    Map<String, Object> config = new LinkedHashMap<>();

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

    Map<String, Object> figures = new LinkedHashMap<>();
    figures.put("enabled", true);
    figures.put("figureImageClippingEnabled", true);
    figures.put("advancedChartExtractionEnabled", false);
    figures.put("customInstructions", "");
    blockOptions.put("figures", figures);

    Map<String, Object> text = new LinkedHashMap<>();
    text.put("signatureDetectionEnabled", false);
    Map<String, Object> textAgentic = new LinkedHashMap<>();
    textAgentic.put("enabled", true);
    text.put("agentic", textAgentic);
    blockOptions.put("text", text);

    Map<String, Object> tables = new LinkedHashMap<>();
    tables.put("targetFormat", "html");
    tables.put("tableHeaderContinuationEnabled", false);
    tables.put("cellBlocksEnabled", false);
    Map<String, Object> tablesAgentic = new LinkedHashMap<>();
    tablesAgentic.put("enabled", false);
    tables.put("agentic", tablesAgentic);
    blockOptions.put("tables", tables);

    Map<String, Object> barcodes = new LinkedHashMap<>();
    barcodes.put("imageClippingEnabled", false);
    barcodes.put("readingEnabled", false);
    blockOptions.put("barcodes", barcodes);

    Map<String, Object> keyValue = new LinkedHashMap<>();
    keyValue.put("blankFieldFormattingEnabled", false);
    blockOptions.put("keyValue", keyValue);

    Map<String, Object> formulas = new LinkedHashMap<>();
    formulas.put("enabled", false);
    blockOptions.put("formulas", formulas);

    parseConfig.put("blockOptions", blockOptions);

    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "page");
    Map<String, Object> chunkingOptions = new LinkedHashMap<>();
    chunkingOptions.put("minCharacters", 500);
    chunkingOptions.put("maxCharacters", 10000);
    chunkingStrategy.put("options", chunkingOptions);
    parseConfig.put("chunkingStrategy", chunkingStrategy);

    config.put("parseConfig", parseConfig);
    return config;
  }

  private static String toJson(Map<String, Object> map) {
    StringBuilder sb = new StringBuilder();
    sb.append("{");
    boolean first = true;
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      if (!first) sb.append(",");
      first = false;
      sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
      sb.append(valueToJson(entry.getValue()));
    }
    sb.append("}");
    return sb.toString();
  }

  private static String valueToJson(Object value) {
    if (value == null) {
      return "null";
    } else if (value instanceof String) {
      return "\"" + escapeJson((String) value) + "\"";
    } else if (value instanceof Boolean) {
      return value.toString();
    } else if (value instanceof Number) {
      return value.toString();
    } else if (value instanceof Map) {
      return toJson((Map<String, Object>) value);
    } else if (value instanceof List) {
      StringBuilder sb = new StringBuilder("[");
      List<?> list = (List<?>) value;
      for (int i = 0; i < list.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append(valueToJson(list.get(i)));
      }
      sb.append("]");
      return sb.toString();
    }
    return "null";
  }

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

  private static Map<String, Object> parseJson(String json) {
    json = json.trim();
    if (!json.startsWith("{")) {
      return new HashMap<>();
    }
    Map<String, Object> result = new HashMap<>();
    int depth = 0;
    StringBuilder key = new StringBuilder();
    StringBuilder value = new StringBuilder();
    boolean inKey = true;
    boolean inString = false;
    boolean escaped = false;

    for (int i = 1; i < json.length() - 1; i++) {
      char c = json.charAt(i);

      if (escaped) {
        if (inKey) key.append(c);
        else value.append(c);
        escaped = false;
        continue;
      }

      if (c == '\\') {
        escaped = true;
        if (inKey) key.append(c);
        else value.append(c);
        continue;
      }

      if (c == '"') {
        inString = !inString;
        if (inKey) key.append(c);
        else value.append(c);
        continue;
      }

      if (inString) {
        if (inKey) key.append(c);
        else value.append(c);
        continue;
      }

      if (c == '{' || c == '[') {
        depth++;
        value.append(c);
      } else if (c == '}' || c == ']') {
        depth--;
        value.append(c);
      } else if (c == ':' && depth == 0 && inKey) {
        inKey = false;
        value = new StringBuilder();
      } else if (c == ',' && depth == 0 && !inKey) {
        String k = key.toString().trim();
        if (k.startsWith("\"") && k.endsWith("\"")) {
          k = k.substring(1, k.length() - 1);
        }
        String v = value.toString().trim();
        result.put(k, parseValue(v));
        key = new StringBuilder();
        value = new StringBuilder();
        inKey = true;
      } else {
        if (inKey) key.append(c);
        else value.append(c);
      }
    }

    if (key.length() > 0) {
      String k = key.toString().trim();
      if (k.startsWith("\"") && k.endsWith("\"")) {
        k = k.substring(1, k.length() - 1);
      }
      String v = value.toString().trim();
      result.put(k, parseValue(v));
    }

    return result;
  }

  private static Object parseValue(String v) {
    v = v.trim();
    if (v.isEmpty()) return null;
    if ("null".equals(v)) return null;
    if ("true".equals(v)) return true;
    if ("false".equals(v)) return false;
    if (v.startsWith("\"") && v.endsWith("\"")) {
      return v.substring(1, v.length() - 1);
    }
    if (v.startsWith("{")) {
      return parseJson(v);
    }
    if (v.startsWith("[")) {
      return parseJsonArray(v);
    }
    try {
      if (v.contains(".")) {
        return Double.parseDouble(v);
      } else {
        return Long.parseLong(v);
      }
    } catch (NumberFormatException e) {
      return v;
    }
  }

  private static List<?> parseJsonArray(String json) {
    List<Object> result = new java.util.ArrayList<>();
    json = json.trim();
    if (!json.startsWith("[") || !json.endsWith("]")) {
      return result;
    }
    int depth = 0;
    StringBuilder item = new StringBuilder();
    boolean inString = false;
    boolean escaped = false;

    for (int i = 1; i < json.length() - 1; i++) {
      char c = json.charAt(i);

      if (escaped) {
        item.append(c);
        escaped = false;
        continue;
      }

      if (c == '\\') {
        escaped = true;
        item.append(c);
        continue;
      }

      if (c == '"') {
        inString = !inString;
        item.append(c);
        continue;
      }

      if (inString) {
        item.append(c);
        continue;
      }

      if (c == '{' || c == '[') {
        depth++;
        item.append(c);
      } else if (c == '}' || c == ']') {
        depth--;
        item.append(c);
      } else if (c == ',' && depth == 0) {
        String itemStr = item.toString().trim();
        result.add(parseValue(itemStr));
        item = new StringBuilder();
      } else {
        item.append(c);
      }
    }

    if (item.length() > 0) {
      String itemStr = item.toString().trim();
      result.add(parseValue(itemStr));
    }

    return result;
  }
}
// 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"
)

var (
	apiKey   string
	stateDir string
	stateFile string
)

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

var state State

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

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

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

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

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	var reqBody io.Reader
	if body != nil {
		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", apiKey))
	req.Header.Set("x-extend-api-version", VERSION)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

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

	respBody, 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
}

var workflow = map[string]interface{}{
	"name": "Contract for Legal Services 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{}{
						"figures": map[string]interface{}{
							"enabled":                      true,
							"figureImageClippingEnabled":   true,
							"advancedChartExtractionEnabled": false,
							"customInstructions":           "",
						},
						"text": map[string]interface{}{
							"signatureDetectionEnabled": false,
							"agentic": map[string]interface{}{
								"enabled": true,
							},
						},
						"tables": map[string]interface{}{
							"targetFormat":                   "html",
							"tableHeaderContinuationEnabled": false,
							"cellBlocksEnabled":              false,
							"agentic": map[string]interface{}{
								"enabled": false,
							},
						},
						"barcodes": map[string]interface{}{
							"imageClippingEnabled": false,
							"readingEnabled":       false,
						},
						"keyValue": map[string]interface{}{
							"blankFieldFormattingEnabled": false,
						},
						"formulas": map[string]interface{}{
							"enabled": false,
						},
					},
					"chunkingStrategy": map[string]interface{}{
						"type": "page",
						"options": map[string]interface{}{
							"minCharacters": 500,
							"maxCharacters": 10000,
						},
					},
				},
			},
		},
	},
}

func main() {
	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 an existing workflow with the same name
		q := url.QueryEscape(workflowName)
		list, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", q), nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := list["data"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			} else if data, ok := list["items"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			}

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

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

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

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

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

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

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

Frequently Asked Questions (FAQ)

Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
Set your review threshold at `confidence < 0.85` for critical fields (client name, hourly rate, termination date) and `< 0.70` for optional fields (alternative contact, renewal terms). Test on 10 sample contracts to calibrate; legal contracts often have 0.92–0.98 confidence on key fields when descriptions are precise.
Use environment variables (`process.env.EXTEND_API_KEY`) and load them via `.env` files (development) or AWS Secrets Manager / HashiCorp Vault (production). Never commit keys; rotate them monthly and use role-based API key scoping if available in your Extend account.
Tags
Legal ServicesContractAttorney AgreementFee StructureScope of Work
About this template

This template parses contract details for service engagements, including client and provider information, scope of services, fee structures, cost reimbursement terms, and conditions for discharge or withdrawal.

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