> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyformat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Contract analysis

> Extract key terms and clauses from a contract, then check them with validation rules

**Nodes used:** [Parse](/guides/nodes/parse), [Extract](/guides/nodes/extract), [Validate](/guides/nodes/validate)

This example reads a contract, extracts its key terms and the clauses it contains, and then runs a set of rules over those terms. Deterministic rules catch a missing date or a short notice period for free; one AI rule judges whether the indemnification clause protects you.

## The workflow

<CodeGroup>
  ```json API theme={null}
  {
    "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" }
    ]
  }
  ```

  ```python Python theme={null}
  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()
  )
  ```

  ```typescript TypeScript theme={null}
  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();
  ```
</CodeGroup>

## 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 poll `GET /v3/runs/{run_id}/` until `status` is `processed`. `error` and `cancelled` are terminal too, so stop on those.

<CodeGroup>
  ```bash curl theme={null}
  # 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
  ```

  ```python Python theme={null}
  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")
  ```

  ```typescript TypeScript theme={null}

  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");
  ```
</CodeGroup>

## Response

The `results` 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"`.

```json theme={null}
{
  "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
          }
        }
      }
    ]
  }
}
```

Rule outcomes (`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](/guides/nodes/if-else) node and send a [Slack alert](/guides/nodes/slack-alert) or an [email alert](/guides/nodes/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 `required` check on `liability_cap` is the cheapest way to flag an uncapped contract.
* `severity` is a label. A failed `error` rule never blocks the run; it tells the reviewer where to look first.
* Declare `termination_notice_days` as `integer`. A `range` rule and calendar math both work on it directly.
* The `multi_select` for `key_clauses` gives you presence checks. Split its value on `", "` to get the option names. Add `string` fields for the clauses whose wording you need to read.

## Next steps

<CardGroup cols={2}>
  <Card title="Validate node" icon="check-double" href="/guides/nodes/validate">
    Every check type, expressions and severities
  </Card>

  <Card title="Run document packet" icon="play" href="/api-reference-v3/document-packets/run">
    Run or re-run a packet you uploaded earlier
  </Card>
</CardGroup>
