---
title: "Tutorial: Contract Review"
source: "https://docs.vertesiahq.com/processes/tutorial-contract-review"
markdown: "https://docs.vertesiahq.com/llms/processes/tutorial-contract-review.md"
---

# Tutorial: Contract Review

This tutorial builds a realistic process: **contract review**. It accepts a contract document, has an agent extract key terms, has another agent flag risky clauses, routes high-value or high-risk contracts to a legal reviewer via a human task, auto-approves the rest, and finally writes the result back to the store.

By the end, you'll have seen every node type working together and picked up the patterns that carry to other processes.

## What we're building

```
start
  └── extract_terms  (agent)
        └── flag_clauses  (agent)
              └── risk_route  (condition)
                    ├─ legal_review  (human_task)
                    │    ├─ store_output  (agent)   ← approve
                    │    ├─ rejected  (final)        ← reject
                    │    └─ flag_clauses  (loop back) ← request_edits
                    └─ auto_approve  (tool)
                         └── store_output  (agent)
                               └── done  (final)
```

Context fields (the state the process accumulates):

- `contract_doc_id` — input document reference.
- `parties`, `term_length`, `governing_law`, `total_value`, `auto_renewal` — written by `extract_terms`.
- `flagged_clauses`, `has_critical_flag` — written by `flag_clauses`.
- `legal_decision`, `legal_notes` — written by `legal_review`.
- `output_doc_id` — written by `store_output`.

## 1. Start from the schema

Every writable field must appear in `context.schema`. Start here.

```json
"context": {
    "schema": {
        "type": "object",
        "properties": {
            "contract_doc_id": {
                "type": "string",
                "format": "document",
                "editor": "document",
                "description": "Contract document to review."
            },
            "parties": { "type": "string" },
            "term_length": { "type": "string" },
            "governing_law": { "type": "string" },
            "total_value": { "type": "number", "description": "Total contract value in USD." },
            "auto_renewal": { "type": "boolean" },
            "flagged_clauses": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "clause_type": { "type": "string" },
                        "excerpt": { "type": "string" },
                        "risk_level": { "type": "string" },
                        "reason": { "type": "string" }
                    }
                }
            },
            "has_critical_flag": { "type": "boolean" },
            "legal_decision": { "type": "string" },
            "legal_notes": { "type": "string" },
            "output_doc_id": { "type": "string" }
        },
        "required": ["contract_doc_id"]
    },
    "initial": {
        "has_critical_flag": false,
        "auto_renewal": false,
        "flagged_clauses": []
    }
}
```

Two things to call out:

- **`contract_doc_id`** uses `format: "document"` **and** `editor: "document"`. Both are required: the first enables validation, the second makes the Start Run modal render a document picker instead of a raw text field.
- **`initial`** seeds a few fields so downstream guards don't have to reason about "unset vs false".

## 2. `extract_terms` — the first agent node

```json
"extract_terms": {
    "type": "agent",
    "human_description": "Reads the contract document and extracts the key terms (parties, term, law, value, auto-renewal) into process context.",
    "prompt": "You are a contract analyst. Extract the key terms from the contract at {{contract_doc_id}}. Only extract what is explicitly stated; if unknown use 0 for numeric fields and false for booleans.",
    "tools": ["fetch_document"],
    "writes": ["parties", "term_length", "governing_law", "total_value", "auto_renewal"],
    "transitions": [
        { "to": "flag_clauses", "trigger": "agent" }
    ]
}
```

What's happening:

- The engine builds a `result_schema` from `node.writes` ∩ `context.schema.properties`. Since there's exactly one agent-triggered transition, no `_next_node` is needed — the engine auto-advances to `flag_clauses`.
- The agent gets the `fetch_document` tool so it can actually read the contract. Skills (`learn_*`) remain available.
- The `{{contract_doc_id}}` placeholder is expanded against context before dispatch.

The agent's final response must be JSON matching the schema — something like:

```json
{ "parties": "ACCOR SA and Vertesia SAS", "term_length": "12 months", "governing_law": "French law", "total_value": 97500, "auto_renewal": false }
```

## 3. `flag_clauses` — another agent, same pattern

```json
"flag_clauses": {
    "type": "agent",
    "human_description": "Identifies risky clauses in the contract across four categories and flags critical risks.",
    "prompt": "You are a legal risk analyst. Fetch the contract at {{contract_doc_id}} and identify risky clauses in these categories: indemnification, unlimited liability, non-standard termination, IP assignment. For each flagged clause return an object with clause_type, excerpt, risk_level (low|medium|critical), and reason. Set has_critical_flag true if any clause is critical.",
    "tools": ["fetch_document", "search_documents"],
    "writes": ["flagged_clauses", "has_critical_flag"],
    "transitions": [
        { "to": "risk_route", "trigger": "agent" }
    ]
}
```

## 4. `risk_route` — a condition node

Route high-value or high-risk contracts to a human; everything else auto-approves.

```json
"risk_route": {
    "type": "condition",
    "human_description": "Routes contracts above $50K or with critical risk flags to legal review; everything else auto-approves.",
    "branches": [
        {
            "to": "legal_review",
            "when": {
                "or": [
                    { ">": [{ "var": "total_value" }, 50000] },
                    { "==": [{ "var": "has_critical_flag" }, true] }
                ]
            }
        },
        { "to": "auto_approve", "default": true }
    ]
}
```

Condition nodes evaluate `branches` top-to-bottom and require a `default: true` branch as a safety net.

## 5. `legal_review` — a human task

```json
"legal_review": {
    "type": "human_task",
    "human_description": "Pauses the process for a legal reviewer to approve, reject, or request edits.",
    "writes": ["legal_decision", "legal_notes"],
    "task": {
        "title": "Legal Review Required: {{parties}}",
        "description": "A contract requires legal review. Extracted terms and flagged clauses are available in the process context. Please review and submit your decision.",
        "assignee": "group:legal",
        "fields": [
            { "name": "legal_decision", "type": "select", "required": true, "label": "Decision", "options": ["approve", "reject", "request_edits"] },
            { "name": "legal_notes", "type": "text", "required": false, "label": "Notes or Edit Instructions" }
        ]
    },
    "transitions": [
        { "to": "store_output", "guard": { "==": [{ "var": "legal_decision" }, "approve"] } },
        { "to": "rejected",     "guard": { "==": [{ "var": "legal_decision" }, "reject"] } },
        { "to": "flag_clauses", "guard": { "==": [{ "var": "legal_decision" }, "request_edits"] } }
    ]
}
```

Highlights:

- The `title` uses `{{parties}}` — expanded at task creation time and stored expanded on the task (so reviewers see "Legal Review Required: ACCOR SA and Vertesia SAS"). Re-runs that revisit this node produce a new task with fresh values.
- The `assignee` is `group:legal`. Anyone in the `legal` group will see this in their Task Inbox. Users can **Take Task** to claim it to themselves.
- The three guarded transitions model a standard review outcome. `request_edits` loops back to `flag_clauses` so the agent can re-analyze with the legal reviewer's notes in context.

## 6. `auto_approve` — a tool node

A trivial deterministic step: just writes a fixed value into context and moves on.

```json
"auto_approve": {
    "type": "tool",
    "human_description": "Marks the contract as auto-approved when it clears the risk route without needing human review.",
    "config": {
        "context_update": { "legal_decision": "auto_approved" }
    },
    "writes": ["legal_decision"],
    "transitions": [ { "to": "store_output" } ]
}
```

## 7. `store_output` — a final agent node

Writes a markdown summary back to the content store.

```json
"store_output": {
    "type": "agent",
    "human_description": "Produces a markdown summary of the review and stores it as a new document.",
    "prompt": "Create a markdown artifact named contract-review.md containing the contract review results, then persist it with create_document using source: \"artifact:files/contract-review.md\". Use this markdown: \n\n# Contract Review\n\n## Decision\n{{legal_decision}}\n\n## Extracted Terms\n- Parties: {{parties}}\n- Term: {{term_length}}\n- Governing Law: {{governing_law}}\n- Total Value: ${{total_value}}\n- Auto-Renewal: {{auto_renewal}}\n\n## Flagged Clauses\n{{flagged_clauses}}\n\n## Legal Notes\n{{legal_notes}}\n\nReturn the new document id as output_doc_id.",
    "tools": ["write_artifact", "create_document"],
    "writes": ["output_doc_id"],
    "transitions": [
        { "to": "done", "trigger": "agent" }
    ]
}
```

## 8. Terminals

```json
"rejected": { "type": "final", "human_description": "The contract was rejected during legal review." },
"done":      { "type": "final", "human_description": "The contract has been reviewed and the output document stored." }
```

Multiple `final` nodes are fine — and useful — because the run's ending node tells a human reader how it ended.

## Putting it together

The top-level shape of the definition:

```json
{
    "format_version": 1,
    "process": "contract_review",
    "description": "Reviews a contract document: extracts key terms, flags risky clauses, routes to human legal review or auto-approves, then stores the final output.",
    "initial": "extract_terms",
    "context": { /* schema + initial from step 1 */ },
    "nodes": {
        "extract_terms": { /* step 2 */ },
        "flag_clauses":  { /* step 3 */ },
        "risk_route":    { /* step 4 */ },
        "legal_review":  { /* step 5 */ },
        "auto_approve":  { /* step 6 */ },
        "store_output":  { /* step 7 */ },
        "rejected":      { /* step 8 */ },
        "done":          { /* step 8 */ }
    }
}
```

## Running it

In Vertesia Studio:

1. **Store** → **Processes** → **New Process** (or ask the Studio Assistant to create it from this spec).
2. Save as `status: "draft"`, then open it and click **Start Run**.
3. The Start Run modal renders `context.schema` as a form, with the document picker for `contract_doc_id`. Pick a contract and choose `programmatic` unless you want a supervisor conversation involved.
4. Watch the run from **Store** → **Process Runs**: the state chart highlights the active node, the Node Inspector shows each `context_diff`, and the Conversation tab streams the two agent conversations live.
5. When `legal_review` fires, the task lands in your inbox (filtered to you and `group:legal`). Submit a decision. The process resumes.
6. `store_output` creates the summary document. `done` terminates the run.

## What to customize

- **Swap the extraction/flagging logic** by editing the agent prompts. The result schema is always derived from `writes`, so tightening or loosening the schema is as simple as editing the writes list.
- **Change the routing threshold** by editing the `when` in `risk_route`. JSON Logic supports arithmetic, comparisons, array operations.
- **Add clause-by-clause analysis** by wrapping a child agent node in a `foreach` step over `flagged_clauses`.
- **Add more human gates** — e.g. a finance-approval step for very high-value contracts — as additional `human_task` nodes with `group:finance`.

## See also

- [Node types](/processes/node-types)
- [Agent nodes](/processes/agent-nodes)
- [Observability](/processes/observability)
- [Task Inbox](/processes/task-inbox)