The workflow
{
"name": "Contract analysis",
"description": "Extract key terms and clauses, then check them",
"nodes": [
{ "id": "parse_1", "type": "parse" },
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{
"name": "contract_type",
"description": "The type of legal agreement",
"data_type": "enum",
"enum_options": [
{ "name": "nda", "description": "Non-disclosure agreement" },
{ "name": "service_agreement", "description": "Service or consulting agreement" },
{ "name": "employment", "description": "Employment contract" },
{ "name": "lease", "description": "Lease or rental agreement" },
{ "name": "licensing", "description": "Licensing agreement" }
]
},
{ "name": "effective_date", "description": "Date the contract takes effect", "data_type": "date" },
{ "name": "expiration_date", "description": "Date the contract expires or terminates", "data_type": "date" },
{ "name": "auto_renewal", "description": "Whether the contract automatically renews at expiration", "data_type": "boolean" },
{ "name": "governing_law", "description": "State or jurisdiction whose laws govern this contract", "data_type": "string" },
{ "name": "termination_notice_days", "description": "Number of days advance notice required to terminate", "data_type": "integer" },
{
"name": "key_clauses",
"description": "Standard contract clauses that are present in this agreement",
"data_type": "multi_select",
"enum_options": [
{ "name": "confidentiality", "description": "Confidentiality or NDA clause" },
{ "name": "non_compete", "description": "Non-compete restriction" },
{ "name": "indemnification", "description": "Indemnification or hold-harmless clause" },
{ "name": "limitation_of_liability", "description": "Cap on damages or liability" },
{ "name": "force_majeure", "description": "Force majeure or act-of-God clause" },
{ "name": "arbitration", "description": "Mandatory arbitration clause" },
{ "name": "intellectual_property", "description": "IP ownership or assignment clause" }
]
},
{ "name": "liability_cap", "description": "Maximum liability amount, if a cap is specified", "data_type": "float" }
]
}
},
{
"id": "validate_1",
"type": "validate",
"rules": [
{ "id": "has-effective-date", "kind": "deterministic", "severity": "error",
"check": { "type": "required", "field": "effective_date" } },
{ "id": "term-is-ordered", "kind": "deterministic", "severity": "error",
"check": { "type": "comparison", "left": "expiration_date", "op": ">=", "right": { "source": "field", "field": "effective_date" } } },
{ "id": "notice-at-least-90-days", "kind": "deterministic", "severity": "warning",
"check": { "type": "range", "field": "termination_notice_days", "min": 90 } },
{ "id": "liability-is-capped", "kind": "deterministic", "severity": "warning",
"check": { "type": "required", "field": "liability_cap" } },
{ "id": "indemnification-protects-us", "kind": "ai", "severity": "warning",
"description": "The indemnification clause protects the Client, not only the Provider." }
]
}
],
"edges": [
{ "source": "parse_1", "target": "extract_1" },
{ "source": "extract_1", "target": "validate_1" }
]
}
import os
from anyformat.sdk import Client
from anyformat.workflow import ComparisonCheck, RangeCheck, RequiredCheck, Schema, ValidationRule
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Contract analysis", "Extract key terms and clauses, then check them")
.parse()
.extract([
Schema.enum("contract_type", "The type of legal agreement", options=[
Schema.option("nda", "Non-disclosure agreement"),
Schema.option("service_agreement", "Service or consulting agreement"),
Schema.option("employment", "Employment contract"),
Schema.option("lease", "Lease or rental agreement"),
Schema.option("licensing", "Licensing agreement"),
]),
Schema.date("effective_date", "Date the contract takes effect"),
Schema.date("expiration_date", "Date the contract expires or terminates"),
Schema.boolean("auto_renewal", "Whether the contract automatically renews at expiration"),
Schema.string("governing_law", "State or jurisdiction whose laws govern this contract"),
Schema.integer("termination_notice_days", "Number of days advance notice required to terminate"),
Schema.multi_select("key_clauses", "Standard contract clauses that are present in this agreement", options=[
Schema.option("confidentiality", "Confidentiality or NDA clause"),
Schema.option("non_compete", "Non-compete restriction"),
Schema.option("indemnification", "Indemnification or hold-harmless clause"),
Schema.option("limitation_of_liability", "Cap on damages or liability"),
Schema.option("force_majeure", "Force majeure or act-of-God clause"),
Schema.option("arbitration", "Mandatory arbitration clause"),
Schema.option("intellectual_property", "IP ownership or assignment clause"),
]),
Schema.float("liability_cap", "Maximum liability amount, if a cap is specified"),
])
.validate(
ValidationRule(id="has-effective-date", kind="deterministic", severity="error",
check=RequiredCheck(type="required", field="effective_date")),
ValidationRule(id="term-is-ordered", kind="deterministic", severity="error",
check=ComparisonCheck(type="comparison", left="expiration_date", op=">=",
right={"source": "field", "field": "effective_date"})),
ValidationRule(id="notice-at-least-90-days", kind="deterministic", severity="warning",
check=RangeCheck(type="range", field="termination_notice_days", min=90)),
ValidationRule(id="liability-is-capped", kind="deterministic", severity="warning",
check=RequiredCheck(type="required", field="liability_cap")),
ValidationRule(id="indemnification-protects-us", severity="warning",
description="The indemnification clause protects the Client, not only the Provider."),
)
.create()
)
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const workflow = await af
.workflow("Contract analysis", "Extract key terms and clauses, then check them")
.parse()
.extract([
Schema.enum("contract_type", "The type of legal agreement", [
Schema.option("nda", "Non-disclosure agreement"),
Schema.option("service_agreement", "Service or consulting agreement"),
Schema.option("employment", "Employment contract"),
Schema.option("lease", "Lease or rental agreement"),
Schema.option("licensing", "Licensing agreement"),
]),
Schema.date("effective_date", "Date the contract takes effect"),
Schema.date("expiration_date", "Date the contract expires or terminates"),
Schema.boolean("auto_renewal", "Whether the contract automatically renews at expiration"),
Schema.string("governing_law", "State or jurisdiction whose laws govern this contract"),
Schema.integer("termination_notice_days", "Number of days advance notice required to terminate"),
Schema.multiSelect("key_clauses", "Standard contract clauses that are present in this agreement", [
Schema.option("confidentiality", "Confidentiality or NDA clause"),
Schema.option("non_compete", "Non-compete restriction"),
Schema.option("indemnification", "Indemnification or hold-harmless clause"),
Schema.option("limitation_of_liability", "Cap on damages or liability"),
Schema.option("force_majeure", "Force majeure or act-of-God clause"),
Schema.option("arbitration", "Mandatory arbitration clause"),
Schema.option("intellectual_property", "IP ownership or assignment clause"),
]),
Schema.float("liability_cap", "Maximum liability amount, if a cap is specified"),
])
.validate([
{ id: "has-effective-date", kind: "deterministic", severity: "error",
check: { type: "required", field: "effective_date" } },
{ id: "term-is-ordered", kind: "deterministic", severity: "error",
check: { type: "comparison", left: "expiration_date", op: ">=", right: { source: "field", field: "effective_date" } } },
{ id: "notice-at-least-90-days", kind: "deterministic", severity: "warning",
check: { type: "range", field: "termination_notice_days", min: 90 } },
{ id: "liability-is-capped", kind: "deterministic", severity: "warning",
check: { type: "required", field: "liability_cap" } },
{ id: "indemnification-protects-us", kind: "ai", severity: "warning",
description: "The indemnification clause protects the Client, not only the Provider." },
])
.create();
Upload now, run later, poll
Contracts often arrive in batches and get reviewed later. Upload each one as a document packet as it arrives, and start the run when you are ready. Then pollGET /v3/runs/{run_id}/ until status is processed. error and cancelled are terminal too, so stop on those.
# 1. Upload the contract as a packet (no run yet)
curl -X POST "https://api.anyformat.ai/v3/workflows/$WORKFLOW_ID/upload/" \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'files=@contract.pdf'
# → 201 { "document_packet_id": "<packet_id>", ... }
# 2. Run the packet when ready (a fresh run each call)
curl -X POST "https://api.anyformat.ai/v3/document-packets/$PACKET_ID/run/" \
-H "Authorization: Bearer $ANYFORMAT_API_KEY"
# → 202 { "run_id": "<run_id>", "status": "queued" }
# 3. Poll until "status" is one of processed / error / cancelled
until curl -s "https://api.anyformat.ai/v3/runs/$RUN_ID/" \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
| grep -qE '"status":"(processed|error|cancelled)"'; do
sleep 5
done
import time
upload = workflow.upload("contract.pdf") # packet only, no run
run = upload.run() # start it when ready
detail = client.get_run(run.id)
while detail.status not in ("processed", "error", "cancelled"):
time.sleep(5)
detail = client.get_run(run.id)
if detail.result is None:
raise RuntimeError(f"run {run.id} ended in {detail.status}")
result = detail.result # or: upload.run().wait() to block instead
print(result.fields["contract_type"].value, result.fields["governing_law"].value)
# A multi_select value is one comma-separated string of option names.
clauses = (result.fields["key_clauses"].value or "").split(", ")
if "indemnification" not in clauses:
print("WARNING: no indemnification clause")
import type { ExtractedField } from "@anyformat/sdk";
const file = new File([readFileSync("contract.pdf")], "contract.pdf", { type: "application/pdf" });
const upload = await workflow.upload(file); // packet only, no run
const run = await upload.run(); // start it when ready
const result = await run.wait(); // polls until a terminal status
console.log((result.field("contract_type") as ExtractedField<string> | undefined)?.value, (result.field("governing_law") as ExtractedField<string> | undefined)?.value);
// A multi_select value is one comma-separated string of option names.
const clauses = ((result.field("key_clauses") as ExtractedField<string> | undefined)?.value ?? "").split(", ");
if (!clauses.includes("indemnification")) console.warn("no indemnification clause");
Response
Theresults envelope of a processed run, trimmed. Every value is a string on the wire: a multi_select is one comma-separated string of option names, with one evidence snippet per clause, and a boolean is "True" or "False".
{
"status": "processed",
"results": {
"extractions": [
{
"split_name": null,
"partition": null,
"fields": {
"contract_type": {
"value": "service_agreement",
"confidence": 96.0,
"evidence": [{ "text": "MASTER SERVICES AGREEMENT", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
},
"auto_renewal": {
"value": "True",
"confidence": 96.0,
"evidence": [{ "text": "The Agreement automatically renews for successive one-year periods", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
},
"termination_notice_days": {
"value": "60",
"confidence": 97.0,
"evidence": [{ "text": "upon sixty (60) days prior written notice", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
},
"key_clauses": {
"value": "confidentiality, intellectual_property, limitation_of_liability, indemnification, force_majeure, arbitration",
"confidence": 94.0,
"evidence": [
{ "text": "3. CONFIDENTIALITY", "page_number": 1 },
{ "text": "4. INTELLECTUAL PROPERTY", "page_number": 1 },
{ "text": "5. LIMITATION OF LIABILITY", "page_number": 1 },
{ "text": "6. INDEMNIFICATION", "page_number": 1 },
{ "text": "7. FORCE MAJEURE", "page_number": 1 },
{ "text": "shall be finally settled by binding arbitration", "page_number": 1 }
],
"verification_status": "not_verified",
"value_override": null
},
"liability_cap": {
"value": "250000",
"confidence": 95.0,
"evidence": [{ "text": "shall exceed USD 250,000", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
}
}
}
]
}
}
pass, fail, inconclusive) appear in the Validation tab of the run in Studio, next to the extracted values. On this contract notice-at-least-90-days fails, because the notice period is 60 days. To act on a failure automatically, route the run with an If/Else node and send a Slack alert or an email alert.
Tips
- Use a deterministic rule for anything a calculator can check. It is instant, free and never flakes. Keep AI rules for judgement calls.
- A
requiredcheck onliability_capis the cheapest way to flag an uncapped contract. severityis a label. A failederrorrule never blocks the run; it tells the reviewer where to look first.- Declare
termination_notice_daysasinteger. Arangerule and calendar math both work on it directly. - The
multi_selectforkey_clausesgives you presence checks. Split its value on", "to get the option names. Addstringfields for the clauses whose wording you need to read.
Next steps
Validate node
Every check type, expressions and severities
Run document packet
Run or re-run a packet you uploaded earlier

