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 byextract_terms.flagged_clauses,has_critical_flag— written byflag_clauses.legal_decision,legal_notes— written bylegal_review.output_doc_id— written bystore_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_idusesformat: "document"andeditor: "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.initialseeds 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_schemafromnode.writes∩context.schema.properties. Since there's exactly one agent-triggered transition, no_next_nodeis needed — the engine auto-advances toflag_clauses. - The agent gets the
fetch_documenttool 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.
5. legal_review — a human task
"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
titleuses{{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
assigneeisgroup:legal. Anyone in thelegalgroup 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_editsloops back toflag_clausesso 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:
- Store → Processes → New Process (or ask the Studio Assistant to create it from this spec).
- Save as
status: "draft", then open it and click Start Run. - The Start Run modal renders
context.schemaas a form, with the document picker forcontract_doc_id. Pick a contract and chooseprogrammaticunless you want a supervisor conversation involved. - 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. - When
legal_reviewfires, the task lands in your inbox (filtered to you andgroup:legal). Submit a decision. The process resumes. store_outputcreates the summary document.doneterminates 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
wheninrisk_route. JSON Logic supports arithmetic, comparisons, array operations. - Add clause-by-clause analysis by wrapping a child agent node in a
foreachstep overflagged_clauses. - Add more human gates — e.g. a finance-approval step for very high-value contracts — as additional
human_tasknodes withgroup:finance.
