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.

"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

"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.writescontext.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:

{ "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

"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.

"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.

"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.

"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.

"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

"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:

{
    "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. StoreProcessesNew 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 StoreProcess 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

Was this page helpful?