Extract key terms, parties, and conditions from master service agreements.
A master service agreement is a foundational legal contract that establishes the terms, conditions, and service scope governing the ongoing relationship between a service provider and client, including party identification, effective dates, term duration, renewal conditions, and work order procedures. This template takes in Master Service Agreement and outputs markdown (.md) capturing the agreement's full text and structural layout, and JSON (.json) with extracted contract fields including party details, dates, term conditions, service scope, and termination references per the extraction schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemacustom schema — 12 fieldschangedadvancedOptions.advancedMultimodalEnabledtruechangedadvancedOptions.reviewAgent.enabledtruechangedbaseProcessor"extraction_performance"You can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Master Service Agreement Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"agreement_type": {
"type": [
"string",
"null"
],
"description": "Type of agreement (e.g., Master Service Agreement)"
},
"effective_date": {
"type": [
"string",
"null"
],
"description": "Agreement effective date in YYYY-MM-DD format"
},
"party_one_name": {
"type": [
"string",
"null"
],
"description": "First party name and entity type"
},
"party_two_name": {
"type": [
"string",
"null"
],
"description": "Second party name and entity type"
},
"scope_of_services": {
"type": [
"string",
"null"
],
"description": "Description of services, products, or deliverables to be provided"
},
"renewal_notice_days": {
"type": [
"string",
"null"
],
"description": "Days notice required before expiration to renew"
},
"initial_term_end_date": {
"type": [
"string",
"null"
],
"description": "Initial term end date in YYYY-MM-DD format"
},
"party_one_jurisdiction": {
"type": [
"string",
"null"
],
"description": "First party jurisdiction of incorporation"
},
"work_order_requirement": {
"type": [
"string",
"null"
],
"description": "Whether work orders are required and execution requirements"
},
"initial_term_start_date": {
"type": [
"string",
"null"
],
"description": "Initial term start date in YYYY-MM-DD format"
},
"renewal_term_period_months": {
"type": [
"string",
"null"
],
"description": "Length of each renewal term in months"
},
"termination_section_reference": {
"type": [
"string",
"null"
],
"description": "Section reference for termination provisions"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
]
}# Master Service Agreement Processing — Extend AI Skill
## What this pipeline does
This pipeline parses a Master Service Agreement (MSA) document to markdown using agentic OCR, then extracts 11 critical contract fields (parties, dates, terms, scope, renewal conditions) into structured JSON. The output is a fully parsed document ready for contract lifecycle management, obligation tracking, or legal review workflows.
## When to use this
- **Contract management systems**: Automatically ingest MSAs from email, document repositories, or deal desks into a structured contract database
- **Renewal tracking**: Extract effective dates, term lengths, and notice periods to feed automated renewal calendars
- **Legal review workflows**: Parse party names, jurisdictions, and termination references to route documents to the correct legal team
- **Obligation extraction**: Surface scope of services and work order requirements to feed downstream procurement or project management systems
- **AI-driven contract analysis**: Use the markdown output to feed RAG systems or Claude for cross-contract analysis, risk flagging, or clause comparison
## Processor pipeline
### Step 1: Parse (agentic_ocr mode, document-level chunking)
**Processor**: `parseRuns.createAndPoll()` with `blockOptions.text.agentic.enabled: true`
**Purpose**: Convert the MSA (often a multi-page PDF with tables, signatures, exhibits) into clean markdown with semantic structure preserved.
**Config choices**:
- `agentic: true` — enables vision-based OCR to handle handwritten dates, signature blocks, and complex table layouts common in executed MSAs
- `chunkingStrategy: "document"` — returns the entire agreement as one logical chunk rather than page-by-page fragments, preserving cross-reference context
**Why**: MSAs contain dense legal language across pages 5–50+ with exhibits. Agentic OCR captures tables (service schedules, SLAs) and the parsing chunks are scoped to the full document so extraction can reference any section.
### Step 2: Extract (extraction_performance + review agent + advanced multimodal)
**Processor**: `extractRuns.createAndPoll()` with Zod schema
**Purpose**: Pull 11 key contract fields into a typed JSON object for downstream systems.
**Config choices**:
- `baseProcessor: "extraction_performance"` — prioritizes accuracy over speed (OK for batch workflows, not real-time UX)
- `reviewAgent: true` — adds a second pass to validate extracted dates and party names against the document
- `advancedMultimodalEnabled: true` — uses vision to locate and verify signatures, execution dates, and party information in embedded images (sponsor logos, signature pages)
**Why**: MSAs are high-stakes documents; a misread party name or renewal date can break automation. The review agent catches OCR drift on dates (e.g., "Jan 1, 2025" vs "1 January 2025"). Multimodal handles signature blocks and letterheads that carry legal weight.
---
## TypeScript implementation
---
## CLI equivalent
```bash
# Step 1: Parse MSA to markdown
extend parse msa.pdf \
--block-options '{"text":{"agentic":{"enabled":true}}}' \
--chunking-strategy '{"type":"document"}' \
> msa_parsed.md
# Step 2: Extract fields using schema file
extend extract msa.pdf \
--schema msa_schema.json \
--base-processor extraction_performance \
--review-agent \
--advanced-multimodal \
> msa_extracted.json
```
**Schema file** (`msa_schema.json`):
```json
{
"type": "object",
"properties": {
"agreement_type": {
"type": ["string", "null"],
"description": "Type of agreement (e.g., Master Service Agreement)"
},
"effective_date": {
"type": ["string", "null"],
"description": "Agreement effective date in YYYY-MM-DD format"
},
"party_one_name": {
"type": ["string", "null"],
"description": "First party name and entity type"
},
"party_one_jurisdiction": {
"type": ["string", "null"],
"description": "First party jurisdiction of incorporation"
},
"party_two_name": {
"type": ["string", "null"],
"description": "Second party name and entity type"
},
"initial_term_start_date": {
"type": ["string", "null"],
"description": "Initial term start date in YYYY-MM-DD format"
},
"initial_term_end_date": {
"type": ["string", "null"],
"description": "Initial term end date in YYYY-MM-DD format"
},
"renewal_term_period_months": {
"type": ["string", "null"],
"description": "Length of each renewal term in months"
},
"renewal_notice_days": {
"type": ["string", "null"],
"description": "Days notice required before expiration to renew"
},
"scope_of_services": {
"type": ["string", "null"],
"description": "Description of services, products, or deliverables to be provided"
},
"work_order_requirement": {
"type": ["string", "null"],
"description": "Whether work orders are required and execution requirements"
},
"termination_section_reference": {
"type": ["string", "null"],
"description": "Section reference for termination provisions"
}
}
}
```
---
## Schema
The extraction schema captures 11 mission-critical MSA fields:
```json
{
"type": "object",
"properties": {
"agreement_type": {
"type": ["string", "null"],
"description": "Type of agreement (e.g., Master Service Agreement). Usually appears in the title or preamble; confirms this is an MSA and not a Statement of Work or Service Level Agreement."
},
"effective_date": {
"type": ["string", "null"],
"description": "Agreement effective date in YYYY-MM-DD format. Legal start date of the contract; often differs from signature date or execution date. Critical for compliance, statute of limitations, and warranty periods."
},
"party_one_name": {
"type": ["string", "null"],
"description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation'). The service provider or vendor. Include entity type and state of incorporation to validate legal standing."
},
"party_one_jurisdiction": {
"type": ["string", "null"],
"description": "First party jurisdiction of incorporation (e.g., 'Delaware'). Used for disputes, tax treatment, and regulatory applicability. Often found in definitions or signature blocks."
},
"party_two_name": {
"type": ["string", "null"],
"description": "Second party name and entity type (e.g., 'TechCorp Inc., a California corporation'). The client or buyer. Extract full legal name and entity type."
},
"initial_term_start_date": {
"type": ["string", "null"],
"description": "Initial contract term start date in YYYY-MM-DD format. When service delivery obligations begin. Used to calculate contract age and renewal eligibility."
},
"initial_term_end_date": {
"type": ["string", "null"],
"description": "Initial contract term end date in YYYY-MM-DD format. Automatic expiration date unless renewed. Single largest trigger for renewal workflows; use this to feed calendar systems."
},
"renewal_term_period_months": {
"type": ["string", "null"],
"description": "Length of each automatic renewal term in months (e.g., '12', '24', '36'). If initial term is 3 years but renewal is 1 year, capture '12'. Essential for predicting future term dates."
},
"renewal_notice_days": {
"type": ["string", "null"],
"description": "Days notice required before expiration to renew or terminate (e.g., '30', '60', '90'). Calculate alert date as: initial_term_end_date minus renewal_notice_days. Missing this = missed opt-out windows."
},
"scope_of_services": {
"type": ["string", "null"],
"description": "Description of services, products, or deliverables to be provided. Often in 'Scope of Work', 'Services', or 'Deliverables' section; may reference an attached exhibit or Schedule A. Capture the summary or reference (e.g., 'As detailed in Schedule A', 'Cloud hosting, 24/7 support, SLA in Exhibit 1')."
},
"work_order_requirement": {
"type": ["string", "null"],
"description": "Whether work orders, Statements of Work (SOWs), or Purchase Orders (POs) are required to execute services. Capture the mechanism (e.g., 'All services require a signed SOW', 'Work orders not required; services auto-execute', 'Services under this MSA governed by separate SOW'). Affects how to trigger fulfillment."
},
"termination_section_reference": {
"type": ["string", "null"],
"description": "Section number and/or heading where termination provisions are defined (e.g., 'Section 8.2 – Termination', 'Article 7: Term and Termination'). Lets legal teams quickly locate termination clauses (for cause, without cause, convenience, wind-down obligations). Format: 'Section X.X' or 'Article N'."
}
}
}
```
**Field rationale:**
- **Dates** (effective_date, initial_term_start_date, initial_term_end_date, renewal_notice_days) are the #1 accuracy driver because they feed automated calendars. Use `extendDate()` to normalize.
- **Parties** must include entity type and jurisdiction to validate legal capacity and dispute resolution venue.
- **Scope** can be a summary or reference; extraction systems often can't parse 50-page exhibits, so capturing "See Schedule A" is acceptable and useful.
- **Work order requirement** determines downstream execution logic; missing this causes fulfillment workflows to fail.
- **Termination section reference** is a pointer, not a summary, because legal teams need direct navigation to the exact clause.
---
## Accuracy tips
1.import { ExtendClient, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* Process a Master Service Agreement: parse to markdown, extract key fields.
* @param filePath - Path to local MSA PDF file
*/
export async function processMasterServiceAgreement(filePath: string) {
// Convert local file to data URL for SDK
const fileBuffer = fs.readFileSync(filePath);
const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;
console.log(`[MSA Pipeline] Processing: ${filePath}`);
// ============================================================================
// STEP 1: Parse MSA to markdown with agentic OCR
// ============================================================================
console.log("[1/2] Parsing MSA document to markdown...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: true,
},
},
},
chunkingStrategy: {
type: "document",
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed: ${parseRun.status}`);
}
// Collect markdown from all chunks
const markdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(
`[1/2] ✓ Parsed ${parseRun.output.chunks.length} chunk(s), ${markdown.length} characters`
);
// ============================================================================
// STEP 2: Extract MSA fields into structured JSON
// ============================================================================
console.log("[2/2] Extracting MSA contract fields...");
// Define Zod schema for MSA fields
const msaSchema = z.object({
agreement_type: z
.string()
.nullable()
.describe(
"Type of agreement, typically 'Master Service Agreement' or 'MSA'"
),
effective_date: extendDate()
.nullable()
.describe(
"Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins"
),
party_one_name: z
.string()
.nullable()
.describe(
"First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')"
),
party_one_jurisdiction: z
.string()
.nullable()
.describe(
"First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution"
),
party_two_name: z
.string()
.nullable()
.describe(
"Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')"
),
initial_term_start_date: extendDate()
.nullable()
.describe(
"Initial contract term start date in YYYY-MM-DD format; when service delivery begins"
),
initial_term_end_date: extendDate()
.nullable()
.describe(
"Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed"
),
renewal_term_period_months: z
.string()
.nullable()
.describe(
"Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration"
),
renewal_notice_days: z
.string()
.nullable()
.describe(
"Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts"
),
scope_of_services: z
.string()
.nullable()
.describe(
"Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits"
),
work_order_requirement: z
.string()
.nullable()
.describe(
"Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')"
),
termination_section_reference: z
.string()
.nullable()
.describe(
"Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract"
),
});
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: msaSchema,
advancedOptions: {
reviewAgent: {
enabled: true,
},
advancedMultimodalEnabled: true,
},
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed: ${extractRun.status}`);
}
const extracted = extractRun.output.value;
console.log(`[2/2] ✓ Extracted ${Object.keys(extracted).length} fields`);
// ============================================================================
// Output results
// ============================================================================
console.log("\n--- MSA Processing Complete ---\n");
console.log("📄 PARSED MARKDOWN (first 500 chars):");
console.log(markdown.substring(0, 500) + (markdown.length > 500 ? "..." : ""));
console.log("\n📋 EXTRACTED FIELDS:");
console.log(JSON.stringify(extracted, null, 2));
return {
markdown,
extracted,
parseRun,
extractRun,
};
}
// Invoke if run directly
processMasterServiceAgreement(process.argv[2] || "msa.pdf").catch(console.error);import os
import base64
from extend_ai import Extend
from typing import Optional
client = Extend(token=os.environ["EXTEND_API_KEY"])
async def process_master_service_agreement(file_path: str):
"""
Process a Master Service Agreement: parse to markdown, extract key fields.
Args:
file_path: Path to local MSA PDF file
"""
# Convert local file to data URL for SDK
with open(file_path, "rb") as f:
file_buffer = f.read()
data_url = f"data:application/octet-stream;base64,{base64.b64encode(file_buffer).decode('utf-8')}"
print(f"[MSA Pipeline] Processing: {file_path}")
# ============================================================================
# STEP 1: Parse MSA to markdown with agentic OCR
# ============================================================================
print("[1/2] Parsing MSA document to markdown...")
parse_run = await client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"block_options": {
"text": {
"agentic": {
"enabled": True,
},
},
},
"chunking_strategy": {
"type": "document",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed: {parse_run.status}")
# Collect markdown from all chunks
markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
print(
f"[1/2] ✓ Parsed {len(parse_run.output.chunks)} chunk(s), {len(markdown)} characters"
)
# ============================================================================
# STEP 2: Extract MSA fields into structured JSON
# ============================================================================
print("[2/2] Extracting MSA contract fields...")
# Define schema for MSA fields
msa_schema = {
"type": "object",
"properties": {
"agreement_type": {
"type": ["string", "null"],
"description": "Type of agreement, typically 'Master Service Agreement' or 'MSA'",
},
"effective_date": {
"type": ["string", "null"],
"description": "Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins",
},
"party_one_name": {
"type": ["string", "null"],
"description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')",
},
"party_one_jurisdiction": {
"type": ["string", "null"],
"description": "First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution",
},
"party_two_name": {
"type": ["string", "null"],
"description": "Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')",
},
"initial_term_start_date": {
"type": ["string", "null"],
"description": "Initial contract term start date in YYYY-MM-DD format; when service delivery begins",
},
"initial_term_end_date": {
"type": ["string", "null"],
"description": "Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed",
},
"renewal_term_period_months": {
"type": ["string", "null"],
"description": "Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration",
},
"renewal_notice_days": {
"type": ["string", "null"],
"description": "Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts",
},
"scope_of_services": {
"type": ["string", "null"],
"description": "Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits",
},
"work_order_requirement": {
"type": ["string", "null"],
"description": "Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')",
},
"termination_section_reference": {
"type": ["string", "null"],
"description": "Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract",
},
},
}
extract_run = await client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": msa_schema,
"advanced_options": {
"review_agent": {
"enabled": True,
},
"advanced_multimodal_enabled": True,
},
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed: {extract_run.status}")
extracted = extract_run.output.value
print(f"[2/2] ✓ Extracted {len(extracted)} fields")
# ============================================================================
# Output results
# ============================================================================
print("\n--- MSA Processing Complete ---\n")
print("📄 PARSED MARKDOWN (first 500 chars):")
preview = markdown[:500] + ("..." if len(markdown) > 500 else "")
print(preview)
print("\n📋 EXTRACTED FIELDS:")
import json
print(json.dumps(extracted, indent=2))
return {
"markdown": markdown,
"extracted": extracted,
"parse_run": parse_run,
"extract_run": extract_run,
}
# Invoke if run directly
if __name__ == "__main__":
import sys
import asyncio
file_arg = sys.argv[1] if len(sys.argv) > 1 else "msa.pdf"
asyncio.run(process_master_service_agreement(file_arg))// Note: This code uses the Extend REST API directly (https://api.extend.ai) because
// Extend does not publish an official Java SDK. It calls the same endpoints and
// uses the same JSON shapes as the TypeScript reference. Uses only java.net.http.HttpClient
// and the built-in java.util.* classes — no external dependencies.
import java.io.*;
import java.net.http.*;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
import javax.json.*;
import javax.json.stream.JsonParser;
public class MasterServiceAgreementProcessor {
private static final String EXTEND_API_BASE = "https://api.extend.ai";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final HttpClient httpClient = HttpClient.newHttpClient();
/**
* Process a Master Service Agreement: parse to markdown, extract key fields.
* @param filePath - Path to local MSA PDF file
*/
public static Map<String, Object> processMasterServiceAgreement(String filePath)
throws IOException, InterruptedException {
// Convert local file to data URL
byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
String base64Content = Base64.getEncoder().encodeToString(fileBuffer);
String dataUrl = "data:application/octet-stream;base64," + base64Content;
System.out.println("[MSA Pipeline] Processing: " + filePath);
// ========================================================================
// STEP 1: Parse MSA to markdown with agentic OCR
// ========================================================================
System.out.println("[1/2] Parsing MSA document to markdown...");
String parsePayload = buildParsePayload(dataUrl);
String parseRunId = createAndPollParseRun(parsePayload);
Map<String, Object> parseResult = getParseRunResult(parseRunId);
if (!"PROCESSED".equals(parseResult.get("status"))) {
throw new RuntimeException("Parse failed: " + parseResult.get("status"));
}
List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseResult.get("chunks");
StringBuilder markdownBuilder = new StringBuilder();
for (Map<String, Object> chunk : chunks) {
if (markdownBuilder.length() > 0) {
markdownBuilder.append("\n\n");
}
markdownBuilder.append(chunk.get("content"));
}
String markdown = markdownBuilder.toString();
System.out.println("[1/2] ✓ Parsed " + chunks.size() + " chunk(s), " +
markdown.length() + " characters");
// ========================================================================
// STEP 2: Extract MSA fields into structured JSON
// ========================================================================
System.out.println("[2/2] Extracting MSA contract fields...");
String extractPayload = buildExtractPayload(dataUrl);
String extractRunId = createAndPollExtractRun(extractPayload);
Map<String, Object> extractResult = getExtractRunResult(extractRunId);
if (!"PROCESSED".equals(extractResult.get("status"))) {
throw new RuntimeException("Extraction failed: " + extractResult.get("status"));
}
Map<String, Object> extracted = (Map<String, Object>) extractResult.get("value");
System.out.println("[2/2] ✓ Extracted " + extracted.size() + " fields");
// ========================================================================
// Output results
// ========================================================================
System.out.println("\n--- MSA Processing Complete ---\n");
System.out.println("📄 PARSED MARKDOWN (first 500 chars):");
String preview = markdown.substring(0, Math.min(500, markdown.length()));
System.out.println(preview + (markdown.length() > 500 ? "..." : ""));
System.out.println("\n📋 EXTRACTED FIELDS:");
System.out.println(prettyPrintJson(extracted));
Map<String, Object> result = new HashMap<>();
result.put("markdown", markdown);
result.put("extracted", extracted);
result.put("parseRun", parseResult);
result.put("extractRun", extractResult);
return result;
}
private static String buildParsePayload(String dataUrl) {
return "{"
+ "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
+ "\"config\": {"
+ " \"blockOptions\": {"
+ " \"text\": {\"agentic\": {\"enabled\": true}}"
+ " },"
+ " \"chunkingStrategy\": {\"type\": \"document\"}"
+ "}"
+ "}";
}
private static String buildExtractPayload(String dataUrl) {
return "{"
+ "\"file\": {\"url\": \"" + escapeJson(dataUrl) + "\"},"
+ "\"config\": {"
+ " \"schema\": {"
+ " \"type\": \"object\","
+ " \"properties\": {"
+ " \"agreement_type\": {\"type\": [\"string\", \"null\"], \"description\": \"Type of agreement, typically 'Master Service Agreement' or 'MSA'\"},"
+ " \"effective_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins\"},"
+ " \"party_one_name\": {\"type\": [\"string\", \"null\"], \"description\": \"First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')\"},"
+ " \"party_one_jurisdiction\": {\"type\": [\"string\", \"null\"], \"description\": \"First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution\"},"
+ " \"party_two_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Second party name and entity type; the other contracting party\"},"
+ " \"initial_term_start_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial contract term start date in YYYY-MM-DD format; when service delivery begins\"},"
+ " \"initial_term_end_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed\"},"
+ " \"renewal_term_period_months\": {\"type\": [\"string\", \"null\"], \"description\": \"Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration\"},"
+ " \"renewal_notice_days\": {\"type\": [\"string\", \"null\"], \"description\": \"Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts\"},"
+ " \"scope_of_services\": {\"type\": [\"string\", \"null\"], \"description\": \"Description of services, products, or deliverables to be provided; defines what the vendor must deliver\"},"
+ " \"work_order_requirement\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether work orders or purchase orders are required to execute services\"},"
+ " \"termination_section_reference\": {\"type\": [\"string\", \"null\"], \"description\": \"Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7')\""
+ " }"
+ " },"
+ " \"advancedOptions\": {"
+ " \"reviewAgent\": {\"enabled\": true},"
+ " \"advancedMultimodalEnabled\": true"
+ " }"
+ "}"
+ "}";
}
private static String createAndPollParseRun(String payload)
throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/parse-runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJsonResponse(response.body());
return (String) data.get("id");
}
private static String createAndPollExtractRun(String payload)
throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/extract-runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJsonResponse(response.body());
return (String) data.get("id");
}
private static Map<String, Object> getParseRunResult(String runId)
throws IOException, InterruptedException {
while (true) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/parse-runs/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJsonResponse(response.body());
String status = (String) data.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
Map<String, Object> output = (Map<String, Object>) data.get("output");
Map<String, Object> result = new HashMap<>(data);
result.put("chunks", output.get("chunks"));
return result;
}
Thread.sleep(2000);
}
}
private static Map<String, Object> getExtractRunResult(String runId)
throws IOException, InterruptedException {
while (true) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(EXTEND_API_BASE + "/extract-runs/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJsonResponse(response.body());
String status = (String) data.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return data;
}
Thread.sleep(2000);
}
}
private static Map<String, Object> parseJsonResponse(String jsonStr) {
// Simple JSON parser using built-in regex and string operations
Map<String, Object> result = new HashMap<>();
// Parse basic fields (simplified for demonstration)
if (jsonStr.contains("\"id\"")) {
String id = extractJsonString(jsonStr, "id");
if (id != null) result.put("id", id);
}
if (jsonStr.contains("\"status\"")) {
String status = extractJsonString(jsonStr, "status");
if (status != null) result.put("status", status);
}
if (jsonStr.contains("\"output\"")) {
result.put("output", parseNestedObject(jsonStr, "output"));
}
if (jsonStr.contains("\"value\"")) {
result.put("value", parseNestedObject(jsonStr, "value"));
}
return result;
}
private static String extractJsonString(String json, String key) {
String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"";
int idx = json.indexOf("\"" + key + "\"");
if (idx == -1) return null;
int colonIdx = json.indexOf(":", idx);
int quoteIdx = json.indexOf("\"", colonIdx);
int endQuoteIdx = json.indexOf("\"", quoteIdx + 1);
if (endQuoteIdx == -1) return null;
return json.substring(quoteIdx + 1, endQuoteIdx);
}
private static Map<String, Object> parseNestedObject(String json, String key) {
Map<String, Object> obj = new HashMap<>();
if ("output".equals(key)) {
List<Map<String, Object>> chunks = new ArrayList<>();
int chunkStart = json.indexOf("\"chunks\"");
if (chunkStart != -1) {
int arrayStart = json.indexOf("[", chunkStart);
int depth = 0;
int i = arrayStart;
while (i < json.length()) {
if (json.charAt(i) == '[') depth++;
if (json.charAt(i) == ']') {
depth--;
if (depth == 0) {
String chunkJson = json.substring(arrayStart + 1, i);
chunks = parseChunksArray(chunkJson);
break;
}
}
i++;
}
}
obj.put("chunks", chunks);
} else if ("value".equals(key)) {
int valueStart = json.indexOf("\"value\"");
if (valueStart != -1) {
int objectStart = json.indexOf("{", valueStart);
int depth = 0;
int i = objectStart;
while (i < json.length()) {
if (json.charAt(i) == '{') depth++;
if (json.charAt(i) == '}') {
depth--;
if (depth == 0) {
String valueJson = json.substring(objectStart, i + 1);
Map<String, Object> valueObj = parseValueObject(valueJson);
return valueObj;
}
}
i++;
}
}
}
return obj;
}
private static List<Map<String, Object>> parseChunksArray(String chunksJson) {
List<Map<String, Object>> chunks = new ArrayList<>();
int pos = 0;
while (pos < chunksJson.length()) {
int objStart = chunksJson.indexOf("{", pos);
if (objStart == -1) break;
int depth = 0;
int i = objStart;
while (i < chunksJson.length()) {
if (chunksJson.charAt(i) == '{') depth++;
if (chunksJson.charAt(i) == '}') {
depth--;
if (depth == 0) {
String objStr = chunksJson.substring(objStart, i + 1);
Map<String, Object> chunk = new HashMap<>();
String content = extractJsonString(objStr, "content");
if (content != null) {
chunk.put("content", content);
}
chunks.add(chunk);
pos = i + 1;
break;
}
}
i++;
}
if (i == chunksJson.length()) break;
}
return chunks;
}
private static Map<String, Object> parseValueObject(String valueJson) {
Map<String, Object> result = new HashMap<>();
String[] fields = {
"agreement_type", "effective_date", "party_one_name", "party_one_jurisdiction",
"party_two_name", "initial_term_start_date", "initial_term_end_date",
"renewal_term_period_months", "renewal_notice_days", "scope_of_services",
"work_order_requirement", "termination_section_reference"
};
for (String field : fields) {
String value = extractJsonString(valueJson, field);
result.put(field, value);
}
return result;
}
private static String escapeJson(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static String prettyPrintJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{\n");
int count = 0;
for (Map.Entry<String, Object> entry : map.entrySet()) {
sb.append(" \"").append(entry.getKey()).append("\": ");
Object value = entry.getValue();
if (value == null) {
sb.append("null");
} else if (value instanceof String) {
sb.append("\"").append(escapeJson((String) value)).append("\"");
} else {
sb.append(value);
}
if (++count < map.size()) {
sb.append(",");
}
sb.append("\n");
}
sb.append("}");
return sb.toString();
}
public static void main(String[] args) {
try {
String filePath = args.length > 0 ? args[0] : "msa.pdf";
processMasterServiceAgreement(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}
}// This code calls the Extend REST API directly because Extend has no official Go SDK yet.
// The API is a thin wrapper; this mirrors the same endpoints and request/response shapes.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
const extendAPIBase = "https://api.extend.ai"
var apiKey string
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
log.Fatal("EXTEND_API_KEY environment variable not set")
}
}
// ParseRunOutput holds the parsed document output.
type ParseRunOutput struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
}
// ParseRun represents the result of a parse operation.
type ParseRun struct {
Status string `json:"status"`
Output ParseRunOutput `json:"output"`
}
// ExtractRunOutput holds the extracted fields.
type ExtractRunOutput struct {
Value map[string]interface{} `json:"value"`
}
// ExtractRun represents the result of an extract operation.
type ExtractRun struct {
Status string `json:"status"`
Output ExtractRunOutput `json:"output"`
}
// MSAFields holds the structured MSA extraction result.
type MSAFields struct {
AgreementType *string `json:"agreement_type"`
EffectiveDate *string `json:"effective_date"`
PartyOneName *string `json:"party_one_name"`
PartyOneJurisdiction *string `json:"party_one_jurisdiction"`
PartyTwoName *string `json:"party_two_name"`
InitialTermStartDate *string `json:"initial_term_start_date"`
InitialTermEndDate *string `json:"initial_term_end_date"`
RenewalTermPeriodMonths *string `json:"renewal_term_period_months"`
RenewalNoticeDays *string `json:"renewal_notice_days"`
ScopeOfServices *string `json:"scope_of_services"`
WorkOrderRequirement *string `json:"work_order_requirement"`
TerminationSectionReference *string `json:"termination_section_reference"`
}
// processMasterServiceAgreement processes an MSA file: parse to markdown, extract key fields.
func processMasterServiceAgreement(filePath string) (map[string]interface{}, error) {
// Read file and convert to base64 data URL
fileBytes, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64.StdEncoding.EncodeToString(fileBytes))
fmt.Printf("[MSA Pipeline] Processing: %s\n", filePath)
// ============================================================================
// STEP 1: Parse MSA to markdown with agentic OCR
// ============================================================================
fmt.Println("[1/2] Parsing MSA document to markdown...")
parseReq := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
"config": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]bool{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]string{
"type": "document",
},
},
}
parseRun, err := createAndPollParseRun(parseReq)
if err != nil {
return nil, err
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed: %s", parseRun.Status)
}
// Collect markdown from all chunks
var mdParts []string
for _, chunk := range parseRun.Output.Chunks {
mdParts = append(mdParts, chunk.Content)
}
markdown := strings.Join(mdParts, "\n\n")
fmt.Printf("[1/2] ✓ Parsed %d chunk(s), %d characters\n", len(parseRun.Output.Chunks), len(markdown))
// ============================================================================
// STEP 2: Extract MSA fields into structured JSON
// ============================================================================
fmt.Println("[2/2] Extracting MSA contract fields...")
// Define schema for MSA fields
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"agreement_type": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Type of agreement, typically 'Master Service Agreement' or 'MSA'",
},
"effective_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Agreement effective date in YYYY-MM-DD format; this is when the contract legally begins",
},
"party_one_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "First party name and entity type (e.g., 'Acme Corp, a Delaware corporation')",
},
"party_one_jurisdiction": map[string]interface{}{
"type": []string{"string", "null"},
"description": "First party jurisdiction of incorporation (e.g., 'Delaware', 'New York'); relevant for dispute resolution",
},
"party_two_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Second party name and entity type; the other contracting party (e.g., 'ServicePro Inc., a California corporation')",
},
"initial_term_start_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Initial contract term start date in YYYY-MM-DD format; when service delivery begins",
},
"initial_term_end_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Initial contract term end date in YYYY-MM-DD format; contract auto-expires on this date unless renewed",
},
"renewal_term_period_months": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Length of each automatic renewal term in months (e.g., '12', '24'); used to calculate next expiration",
},
"renewal_notice_days": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Number of days notice required before expiration to renew (e.g., '30', '90'); critical for renewal calendar alerts",
},
"scope_of_services": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Description of services, products, or deliverables to be provided; defines what the vendor must deliver and may reference attached exhibits",
},
"work_order_requirement": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether work orders or purchase orders are required to execute services; capture execution mechanism (e.g., 'All services require a signed SOW' or 'Work orders not required')",
},
"termination_section_reference": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Section reference for termination provisions (e.g., 'Section 8.2', 'Article 7'); points to the legal section defining how either party can end the contract",
},
},
}
extractReq := map[string]interface{}{
"file": map[string]string{
"url": dataURL,
},
"config": map[string]interface{}{
"schema": schema,
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]bool{
"enabled": true,
},
"advancedMultimodalEnabled": true,
},
},
}
extractRun, err := createAndPollExtractRun(extractReq)
if err != nil {
return nil, err
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed: %s", extractRun.Status)
}
extracted := extractRun.Output.Value
fmt.Printf("[2/2] ✓ Extracted %d fields\n", len(extracted))
// ============================================================================
// Output results
// ============================================================================
fmt.Println("\n--- MSA Processing Complete ---\n")
fmt.Println("📄 PARSED MARKDOWN (first 500 chars):")
if len(markdown) > 500 {
fmt.Println(markdown[:500] + "...")
} else {
fmt.Println(markdown)
}
fmt.Println("\n📋 EXTRACTED FIELDS:")
extractedJSON, _ := json.MarshalIndent(extracted, "", " ")
fmt.Println(string(extractedJSON))
return map[string]interface{}{
"markdown": markdown,
"extracted": extracted,
}, nil
}
// createAndPollParseRun creates a parse run and polls until completion.
func createAndPollParseRun(reqBody map[string]interface{}) (*ParseRun, error) {
body, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", extendAPIBase+"/v1/parseRuns", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("parse request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var run ParseRun
json.Unmarshal(respBody, &run)
// Poll until processed
for run.Status != "PROCESSED" && run.Status != "FAILED" {
time.Sleep(2 * time.Second)
req, _ := http.NewRequest("GET", extendAPIBase+"/v1/parseRuns", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, _ := client.Do(req)
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
json.Unmarshal(respBody, &run)
}
return &run, nil
}
// createAndPollExtractRun creates an extract run and polls until completion.
func createAndPollExtractRun(reqBody map[string]interface{}) (*ExtractRun, error) {
body, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", extendAPIBase+"/v1/extractRuns", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("extract request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var run ExtractRun
json.Unmarshal(respBody, &run)
// Poll until processed
for run.Status != "PROCESSED" && run.Status != "FAILED" {
time.Sleep(2 * time.Second)
req, _ := http.NewRequest("GET", extendAPIBase+"/v1/extractRuns", nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, _ := client.Do(req)
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
json.Unmarshal(respBody, &run)
}
return &run, nil
}
func main() {
filePath := "msa.pdf"
if len(os.Args) > 1 {
filePath = os.Args[1]
}
_, err := processMasterServiceAgreement(filePath)
if err != nil {
log.Fatal(err)
}
}// Deploy the "Master Service Agreement" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/contract-metadata-extraction.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// npx tsx provision.ts
//
// Generated by doc1 (template: contract-metadata-extraction).
import fs from "node:fs";
import path from "node:path";
const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }
const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "contract-metadata-extraction.json");
type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
: {};
function saveState() {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
async function api(method: string, pathName: string, body?: unknown) {
const res = await fetch(API + pathName, {
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
"x-extend-api-version": VERSION,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
return data;
}
// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
"name": "Master Service Agreement Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"agreement_type": {
"type": [
"string",
"null"
],
"description": "Type of agreement (e.g., Master Service Agreement)"
},
"effective_date": {
"type": [
"string",
"null"
],
"description": "Agreement effective date in YYYY-MM-DD format"
},
"party_one_name": {
"type": [
"string",
"null"
],
"description": "First party name and entity type"
},
"party_two_name": {
"type": [
"string",
"null"
],
"description": "Second party name and entity type"
},
"scope_of_services": {
"type": [
"string",
"null"
],
"description": "Description of services, products, or deliverables to be provided"
},
"renewal_notice_days": {
"type": [
"string",
"null"
],
"description": "Days notice required before expiration to renew"
},
"initial_term_end_date": {
"type": [
"string",
"null"
],
"description": "Initial term end date in YYYY-MM-DD format"
},
"party_one_jurisdiction": {
"type": [
"string",
"null"
],
"description": "First party jurisdiction of incorporation"
},
"work_order_requirement": {
"type": [
"string",
"null"
],
"description": "Whether work orders are required and execution requirements"
},
"initial_term_start_date": {
"type": [
"string",
"null"
],
"description": "Initial term start date in YYYY-MM-DD format"
},
"renewal_term_period_months": {
"type": [
"string",
"null"
],
"description": "Length of each renewal term in months"
},
"termination_section_reference": {
"type": [
"string",
"null"
],
"description": "Section reference for termination provisions"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
]
};
async function main() {
console.log(`Deploying "${WORKFLOW.name}"…`);
if (state.workflowId) {
console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
} else {
// Reuse an existing workflow with the same name if one exists (e.g. a
// previous run's state file was lost) instead of creating a duplicate.
try {
const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
const existing = items.find((x) => x.name === WORKFLOW.name);
if (existing?.id) {
state.workflowId = existing.id; saveState();
console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
}
} catch { /* lookup is best-effort; fall through to create */ }
if (!state.workflowId) {
const created = await api("POST", "/workflows", WORKFLOW);
const wfId = created.id ?? created.workflow?.id;
if (!wfId) throw new Error("Could not read created workflow id from response");
state.workflowId = wfId; saveState();
console.log(`+ created workflow (${wfId})`);
}
}
// Deploy the current draft as a new version so the workflow is runnable —
// best-effort: some accounts/plans may not require this explicit step.
await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});
console.log("\nDone. Run documents through it with:");
console.log(` POST ${API}/workflow_runs { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}
main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
#!/usr/bin/env python3
"""
Deploy the "Master Service Agreement" pipeline to YOUR Extend account.
The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
step carries its extractor/classifier/splitter config INLINE, so this is a
single API call. No processors to create or wire up beforehand.
Idempotent: the created workflow id is cached in .extend/contract-metadata-extraction.json,
so re-running updates the existing workflow instead of duplicating it.
Usage:
export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
python provision.py
Generated by doc1 (template: contract-metadata-extraction).
"""
import json
import os
import sys
from pathlib import Path
from typing import Any, Optional
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "contract-metadata-extraction.json"
def load_state() -> dict[str, Optional[str]]:
"""Load workflow state from disk."""
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {}
def save_state(state: dict[str, Any]) -> None:
"""Save workflow state to disk."""
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
# ── Workflow definition — extractor/classifier/splitter configs inline ──────
WORKFLOW = {
"name": "Master Service Agreement Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [{"step": "parse1"}],
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {"text": {"agentic": {"enabled": True}}},
"chunkingStrategy": {"type": "document"},
}
},
"next": [{"step": "extraction2"}],
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"agreement_type": {
"type": ["string", "null"],
"description": "Type of agreement (e.g., Master Service Agreement)",
},
"effective_date": {
"type": ["string", "null"],
"description": "Agreement effective date in YYYY-MM-DD format",
},
"party_one_name": {
"type": ["string", "null"],
"description": "First party name and entity type",
},
"party_two_name": {
"type": ["string", "null"],
"description": "Second party name and entity type",
},
"scope_of_services": {
"type": ["string", "null"],
"description": "Description of services, products, or deliverables to be provided",
},
"renewal_notice_days": {
"type": ["string", "null"],
"description": "Days notice required before expiration to renew",
},
"initial_term_end_date": {
"type": ["string", "null"],
"description": "Initial term end date in YYYY-MM-DD format",
},
"party_one_jurisdiction": {
"type": ["string", "null"],
"description": "First party jurisdiction of incorporation",
},
"work_order_requirement": {
"type": ["string", "null"],
"description": "Whether work orders are required and execution requirements",
},
"initial_term_start_date": {
"type": ["string", "null"],
"description": "Initial term start date in YYYY-MM-DD format",
},
"renewal_term_period_months": {
"type": ["string", "null"],
"description": "Length of each renewal term in months",
},
"termination_section_reference": {
"type": ["string", "null"],
"description": "Section reference for termination provisions",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {"enabled": True},
"advancedMultimodalEnabled": True,
},
}
},
},
],
}
def main() -> None:
"""Provision the workflow."""
client = Extend(token=API_KEY)
state = load_state()
print(f'Deploying "{WORKFLOW["name"]}…"')
if state.get("workflow_id"):
print(f'✓ workflow already provisioned ({state["workflow_id"]}) — updating steps')
client.workflows.update(
id=state["workflow_id"],
steps=WORKFLOW["steps"],
)
else:
# Reuse an existing workflow with the same name if one exists (e.g. a
# previous run's state file was lost) instead of creating a duplicate.
existing_id = None
try:
workflows_list = client.workflows.list(name=WORKFLOW["name"])
items = getattr(workflows_list, "data", None) or getattr(workflows_list, "items", None) or []
for workflow in items:
if getattr(workflow, "name", None) == WORKFLOW["name"]:
existing_id = getattr(workflow, "id", None)
break
if existing_id:
state["workflow_id"] = existing_id
save_state(state)
print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps')
client.workflows.update(
id=existing_id,
steps=WORKFLOW["steps"],
)
except Exception:
# lookup is best-effort; fall through to create
pass
if not state.get("workflow_id"):
created = client.workflows.create(**WORKFLOW)
wf_id = getattr(created, "id", None)
if not wf_id:
raise RuntimeError("Could not read created workflow id from response")
state["workflow_id"] = wf_id
save_state(state)
print(f"+ created workflow ({wf_id})")
# Deploy the current draft as a new version so the workflow is runnable —
# best-effort: some accounts/plans may not require this explicit step.
try:
client.workflows.create_version(id=state["workflow_id"])
except Exception:
pass
wf_id = state["workflow_id"]
api_base = "https://api.extend.ai"
print("\nDone. Run documents through it with:")
print(f' POST {api_base}/workflow_runs {{ "workflow": {{ "id": "{wf_id}" }}, "file": {{ "url": "https://…" }} }}')
print("Or open the workflow in the Extend dashboard to review and deploy it.")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)// Deploy the "Master Service Agreement" pipeline to YOUR Extend account.
// Uses the Extend REST API directly (no official Java SDK exists yet).
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/contract-metadata-extraction.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// javac Provision.java && java Provision
//
// Generated by doc1 (template: contract-metadata-extraction).
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class Provision {
private static final String API = "https://api.extend.ai";
private static final String VERSION = "2026-02-09";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final String STATE_DIR = ".extend";
private static final String STATE_FILE = "contract-metadata-extraction.json";
private static final HttpClient CLIENT = HttpClient.newHttpClient();
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
static class State {
String workflowId;
State(String workflowId) {
this.workflowId = workflowId;
}
}
private static State state;
public static void main(String[] args) {
try {
loadState();
String workflowName = "Master Service Agreement Processing Pipeline";
System.out.println("Deploying \"" + workflowName + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
api("POST", "/workflows/" + state.workflowId, buildStepsJson());
} else {
// Reuse an existing workflow with the same name if one exists
try {
String listResponse = api("GET", "/workflows?name=" + URLEncoder.encode(workflowName, StandardCharsets.UTF_8), null);
String workflowId = extractWorkflowIdFromList(listResponse, workflowName);
if (workflowId != null && !workflowId.isEmpty()) {
state.workflowId = workflowId;
saveState();
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + workflowId + ") — updating steps");
api("POST", "/workflows/" + workflowId, buildStepsJson());
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (state.workflowId == null || state.workflowId.isEmpty()) {
String createResponse = api("POST", "/workflows", buildWorkflowJson());
String workflowId = extractWorkflowId(createResponse);
if (workflowId == null || workflowId.isEmpty()) {
throw new RuntimeException("Could not read created workflow id from response");
}
state.workflowId = workflowId;
saveState();
System.out.println("+ created workflow (" + workflowId + ")");
}
}
// Deploy the current draft as a new version so the workflow is runnable
try {
api("POST", "/workflows/" + state.workflowId + "/versions", "{}");
} catch (Exception e) {
// best-effort; some accounts/plans may not require this explicit step
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
} catch (Exception e) {
System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
System.exit(1);
}
}
private static String api(String method, String pathName, String body) throws IOException, InterruptedException {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(API + pathName))
.method(method, body != null ? HttpRequest.BodyPublishers.ofString(body) : HttpRequest.BodyPublishers.noBody())
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
builder.header("Content-Type", "application/json");
}
HttpRequest request = builder.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String errorBody = response.body().length() > 300 ? response.body().substring(0, 300) : response.body();
throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorBody);
}
return response.body();
}
private static void loadState() throws IOException {
Path statePath = Path.of(STATE_DIR, STATE_FILE);
if (Files.exists(statePath)) {
String json = Files.readString(statePath);
String workflowId = extractFieldFromJson(json, "workflowId");
state = new State(workflowId);
} else {
state = new State(null);
}
}
private static void saveState() throws IOException {
Files.createDirectories(Path.of(STATE_DIR));
String json = "{\"workflowId\": \"" + escapeJson(state.workflowId) + "\"}";
Files.writeString(Path.of(STATE_DIR, STATE_FILE), json);
}
private static String buildWorkflowJson() {
return "{" +
"\"name\": \"Master Service Agreement Processing Pipeline\"," +
"\"steps\": " + buildStepsJson() +
"}";
}
private static String buildStepsJson() {
return "[" +
"{\"name\": \"startTrigger1\", \"type\": \"TRIGGER\", \"next\": [{\"step\": \"parse1\"}]}," +
"{\"name\": \"parse1\", \"type\": \"PARSE\", \"config\": {\"parseConfig\": {\"blockOptions\": {\"text\": {\"agentic\": {\"enabled\": true}}}, \"chunkingStrategy\": {\"type\": \"document\"}}}, \"next\": [{\"step\": \"extraction2\"}]}," +
"{\"name\": \"extraction2\", \"type\": \"EXTRACT\", \"config\": {\"extractorConfig\": {\"schema\": {\"type\": \"object\", \"properties\": {" +
"\"agreement_type\": {\"type\": [\"string\", \"null\"], \"description\": \"Type of agreement (e.g., Master Service Agreement)\"}," +
"\"effective_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Agreement effective date in YYYY-MM-DD format\"}," +
"\"party_one_name\": {\"type\": [\"string\", \"null\"], \"description\": \"First party name and entity type\"}," +
"\"party_two_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Second party name and entity type\"}," +
"\"scope_of_services\": {\"type\": [\"string\", \"null\"], \"description\": \"Description of services, products, or deliverables to be provided\"}," +
"\"renewal_notice_days\": {\"type\": [\"string\", \"null\"], \"description\": \"Days notice required before expiration to renew\"}," +
"\"initial_term_end_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial term end date in YYYY-MM-DD format\"}," +
"\"party_one_jurisdiction\": {\"type\": [\"string\", \"null\"], \"description\": \"First party jurisdiction of incorporation\"}," +
"\"work_order_requirement\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether work orders are required and execution requirements\"}," +
"\"initial_term_start_date\": {\"type\": [\"string\", \"null\"], \"description\": \"Initial term start date in YYYY-MM-DD format\"}," +
"\"renewal_term_period_months\": {\"type\": [\"string\", \"null\"], \"description\": \"Length of each renewal term in months\"}," +
"\"termination_section_reference\": {\"type\": [\"string\", \"null\"], \"description\": \"Section reference for termination provisions\"}" +
"}}, \"baseProcessor\": \"extraction_performance\", \"advancedOptions\": {\"reviewAgent\": {\"enabled\": true}, \"advancedMultimodalEnabled\": true}}}}" +
"]";
}
private static String extractWorkflowId(String json) {
String id = extractFieldFromJson(json, "id");
if (id == null || id.isEmpty()) {
id = extractNestedFieldFromJson(json, "workflow", "id");
}
return id;
}
private static String extractWorkflowIdFromList(String json, String name) {
// Simple JSON parsing for data/items array
String data = extractFieldFromJson(json, "data");
if (data == null || data.isEmpty()) {
data = extractFieldFromJson(json, "items");
}
if (data != null && !data.isEmpty() && data.startsWith("[")) {
int start = 0;
while ((start = data.indexOf("{", start)) != -1) {
int end = data.indexOf("}", start);
if (end != -1) {
String item = data.substring(start, end + 1);
String itemName = extractFieldFromJson(item, "name");
if (name.equals(itemName)) {
return extractFieldFromJson(item, "id");
}
start = end;
} else {
break;
}
}
}
return null;
}
private static String extractFieldFromJson(String json, String field) {
String key = "\"" + field + "\":";
int idx = json.indexOf(key);
if (idx == -1) return null;
int start = idx + key.length();
while (start < json.length() && Character.isWhitespace(json.charAt(start))) start++;
if (start >= json.length()) return null;
if (json.charAt(start) == '"') {
int end = start + 1;
while (end < json.length() && json.charAt(end) != '"') {
if (json.charAt(end) == '\\') end++;
end++;
}
if (end < json.length()) {
return json.substring(start + 1, end);
}
}
return null;
}
private static String extractNestedFieldFromJson(String json, String outerField, String innerField) {
String outerKey = "\"" + outerField + "\":";
int idx = json.indexOf(outerKey);
if (idx == -1) return null;
int start = idx + outerKey.length();
while (start < json.length() && Character.isWhitespace(json.charAt(start))) start++;
if (start >= json.length() || json.charAt(start) != '{') return null;
int depth = 1;
int objEnd = start + 1;
while (objEnd < json.length() && depth > 0) {
if (json.charAt(objEnd) == '{') depth++;
else if (json.charAt(objEnd) == '}') depth--;
objEnd++;
}
String nested = json.substring(start, objEnd);
return extractFieldFromJson(nested, innerField);
}
private static String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
type WorkflowListResponse struct {
Data []WorkflowItem `json:"data,omitempty"`
Items []WorkflowItem `json:"items,omitempty"`
}
type WorkflowItem struct {
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
}
type WorkflowResponse struct {
ID string `json:"id,omitempty"`
Workflow struct {
ID string `json:"id,omitempty"`
} `json:"workflow,omitempty"`
}
type SchemaProperty struct {
Type interface{} `json:"type"`
Description string `json:"description"`
}
type ExtractorSchema struct {
Type string `json:"type"`
Properties map[string]SchemaProperty `json:"properties"`
}
type ExtractorConfig struct {
Schema ExtractorSchema `json:"schema"`
BaseProcessor string `json:"baseProcessor"`
AdvancedOptions struct {
ReviewAgent struct {
Enabled bool `json:"enabled"`
} `json:"reviewAgent"`
AdvancedMultimodalEnabled bool `json:"advancedMultimodalEnabled"`
} `json:"advancedOptions"`
}
type ParseConfig struct {
BlockOptions struct {
Text struct {
Agentic struct {
Enabled bool `json:"enabled"`
} `json:"agentic"`
} `json:"text"`
} `json:"blockOptions"`
ChunkingStrategy struct {
Type string `json:"type"`
} `json:"chunkingStrategy"`
}
type StepConfig struct {
ParseConfig *ParseConfig `json:"parseConfig,omitempty"`
ExtractorConfig *ExtractorConfig `json:"extractorConfig,omitempty"`
}
type NextStep struct {
Step string `json:"step"`
}
type WorkflowStep struct {
Name string `json:"name"`
Type string `json:"type"`
Config *StepConfig `json:"config,omitempty"`
Next []NextStep `json:"next,omitempty"`
}
type WorkflowDefinition struct {
Name string `json:"name"`
Steps []WorkflowStep `json:"steps"`
}
type VersionRequest struct{}
var (
apiKey string
stateDir string
stateFile string
)
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get current directory: %v\n", err)
os.Exit(1)
}
stateDir = filepath.Join(cwd, ".extend")
stateFile = filepath.Join(stateDir, "contract-metadata-extraction.json")
}
func loadState() (State, error) {
var state State
data, err := os.ReadFile(stateFile)
if err != nil {
if os.IsNotExist(err) {
return state, nil
}
return state, err
}
err = json.Unmarshal(data, &state)
return state, err
}
func saveState(state State) error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (json.RawMessage, error) {
url := API + pathName
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
errMsg := string(respBody)
if len(errMsg) > 300 {
errMsg = errMsg[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, errMsg)
}
return respBody, nil
}
func buildWorkflow() WorkflowDefinition {
schema := ExtractorSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"agreement_type": {
Type: []string{"string", "null"},
Description: "Type of agreement (e.g., Master Service Agreement)",
},
"effective_date": {
Type: []string{"string", "null"},
Description: "Agreement effective date in YYYY-MM-DD format",
},
"party_one_name": {
Type: []string{"string", "null"},
Description: "First party name and entity type",
},
"party_two_name": {
Type: []string{"string", "null"},
Description: "Second party name and entity type",
},
"scope_of_services": {
Type: []string{"string", "null"},
Description: "Description of services, products, or deliverables to be provided",
},
"renewal_notice_days": {
Type: []string{"string", "null"},
Description: "Days notice required before expiration to renew",
},
"initial_term_end_date": {
Type: []string{"string", "null"},
Description: "Initial term end date in YYYY-MM-DD format",
},
"party_one_jurisdiction": {
Type: []string{"string", "null"},
Description: "First party jurisdiction of incorporation",
},
"work_order_requirement": {
Type: []string{"string", "null"},
Description: "Whether work orders are required and execution requirements",
},
"initial_term_start_date": {
Type: []string{"string", "null"},
Description: "Initial term start date in YYYY-MM-DD format",
},
"renewal_term_period_months": {
Type: []string{"string", "null"},
Description: "Length of each renewal term in months",
},
"termination_section_reference": {
Type: []string{"string", "null"},
Description: "Section reference for termination provisions",
},
},
}
extractorCfg := ExtractorConfig{
Schema: schema,
BaseProcessor: "extraction_performance",
}
extractorCfg.AdvancedOptions.ReviewAgent.Enabled = true
extractorCfg.AdvancedOptions.AdvancedMultimodalEnabled = true
parseCfg := ParseConfig{}
parseCfg.BlockOptions.Text.Agentic.Enabled = true
parseCfg.ChunkingStrategy.Type = "document"
steps := []WorkflowStep{
{
Name: "startTrigger1",
Type: "TRIGGER",
Next: []NextStep{
{Step: "parse1"},
},
},
{
Name: "parse1",
Type: "PARSE",
Config: &StepConfig{
ParseConfig: &parseCfg,
},
Next: []NextStep{
{Step: "extraction2"},
},
},
{
Name: "extraction2",
Type: "EXTRACT",
Config: &StepConfig{
ExtractorConfig: &extractorCfg,
},
},
}
return WorkflowDefinition{
Name: "Master Service Agreement Processing Pipeline",
Steps: steps,
}
}
func main() {
state, err := loadState()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load state: %v\n", err)
os.Exit(1)
}
workflow := buildWorkflow()
fmt.Printf("Deploying \"%s\"…\n", workflow.Name)
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
updateBody := map[string]interface{}{
"steps": workflow.Steps,
}
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), updateBody)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} else {
// Try to find existing workflow by name
query := url.QueryEscape(workflow.Name)
respData, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", query), nil)
found := false
if err == nil {
var listResp WorkflowListResponse
if err := json.Unmarshal(respData, &listResp); err == nil {
items := listResp.Data
if len(items) == 0 {
items = listResp.Items
}
for _, item := range items {
if item.Name == workflow.Name && item.ID != "" {
state.WorkflowID = item.ID
_ = saveState(state)
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, item.ID)
updateBody := map[string]interface{}{
"steps": workflow.Steps,
}
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", item.ID), updateBody)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
found = true
break
}
}
}
}
if !found {
respData, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
var wfResp WorkflowResponse
if err := json.Unmarshal(respData, &wfResp); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse workflow response: %v\n", err)
os.Exit(1)
}
wfID := wfResp.ID
if wfID == "" {
wfID = wfResp.Workflow.ID
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
if err := saveState(state); err != nil {
fmt.Fprintf(os.Stderr, "Failed to save state: %v\n", err)
os.Exit(1)
}
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version — best-effort
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), VersionRequest{})
fmt.Printf("\nDone. Run documents through it with:\n")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Printf("Or open the workflow in the Extend dashboard to review and deploy it.\n")
}A service contract establishes the foundational terms and conditions governing the relationship between a service provider and client. This template captures critical information including party identification, effective dates, term length, renewal conditions, work order procedures, and exhibits defining deliverables and service scope.