Cookbook: Contracts
Cookbook: contracts
Section titled “Cookbook: contracts”Contracts are long, structured documents where the granularity matters as much as the values — you usually want clauses you can reference back to a page and bbox, not a flat dictionary.
The recipe
Section titled “The recipe”Long-context legal text is where Anthropic models do well, so the default Worker reaches for claude:
from scriva import Orchestrator, Workerfrom scriva.prompts import Prompt
worker = ( Worker.anthropic("claude-opus-4-7") .prompt(Prompt.contract) .cache(".scriva_cache") .score(method="rendering"))
recipe = ( Orchestrator() .orient() .deskew() .split.boxes(detector="legal-blocks") # heading-aware block segmentation .classify(roles={"heading": "header", "body": "data"}) .recognize(worker) .reconstruct.document(sections=True) .export.json("nda.json"))
result = recipe("nda.pdf")recipe.to_dict()["fields"] reads the typed contract: title,
parties, clauses with page index and bbox, signature blocks.
What you get back
Section titled “What you get back”class Contract(BaseModel): title: str document_type: Literal["NDA", "MSA", "SOW", "Lease", "Employment", "Other"] parties: list[Party] # role + name + address effective_date: date expiry_date: date | None term_description: str | None governing_law: str | None venue: str | None clauses: list[Clause] signatures: list[SignatureBlock] notes: str | None
class Clause(BaseModel): number: str | None # e.g. "3.2" heading: str | None body: str page_index: int bbox: BBox
class SignatureBlock(BaseModel): party_name: str | None signatory_name: str | None signatory_title: str | None signed: bool # detected mark, not a verification signed_on: date | None page_index: int bbox: BBoxClause.body is a stable identifier you can hash for diffing across
revisions; bbox lets a UI jump back to the source.
Customising
Section titled “Customising”Only the headlines
Section titled “Only the headlines”When you do not need clause bodies — just the metadata at the top of the document — narrow the reconstruct schema:
class ContractHeader(BaseModel): title: str parties: list[Party] effective_date: date governing_law: str | None
header_recipe = recipe.replace("reconstruct", reconstruct.kv(schema=ContractHeader))header = header_recipe("nda.pdf").to_dict()["fields"]Faster, cheaper, and (counterintuitively) more accurate — narrower schemas pressure the prompt to focus.
Templated contracts
Section titled “Templated contracts”For contracts that are filled-in copies of a known template, the fastest path is to compute the visual diff and only OCR the changed regions:
from scriva.preprocess import diff_to_template
recipe = ( Orchestrator() .then(diff_to_template("templates/master-agreement.pdf")) .split.from_layout("changed.json") # regions emitted by diff_to_template .recognize(worker) .reconstruct.document() .export.json("changed.json"))result = recipe("acme-signed.pdf")diff_to_template rasterises both PDFs at the same DPI, takes the
pixel difference, and emits regions for the changed boxes — typically
1–5% of the original document. Combined with worker.cache(...) this
brings a 40-page MSA below $0.05.
Multi-language clauses
Section titled “Multi-language clauses”Bilingual contracts (common in JP, KR, CN procurement) usually have
matching clause pairs. Worker.with_language(...) pins a language
hint, or set a recipe default:
recipe = recipe.options(default_language="ja")For per-clause language detection, the default Worker already sets
recognition.language (when the engine exposes LANGUAGE_DETECTION);
read it from Clause.locale in the reconstructed record.
Signature detection without verification
Section titled “Signature detection without verification”SignatureBlock.signed is a heuristic — Worker.signature() flags a
region with ink in the signature area as True. It is not a
signature verifier; it exists so a downstream UI can flag “this looks
signed” vs “this looks blank” without rasterising the page itself.
recipe = recipe.recognize.by_kind({ "signature": Worker.signature(), "text": worker,})For real signature verification (Adobe Sign, DocuSign), parse the
PDF’s embedded signature dictionary instead — see
scriva.pdf.signatures:
from scriva.pdf.signatures import extract_signaturessigs = extract_signatures("acme-signed.pdf")That uses the PDF’s CMS layer, not OCR — completely separate code path.
Production notes
Section titled “Production notes”Long contracts and streaming
Section titled “Long contracts and streaming”40+ page contracts blow past a single VLM context. The reconstruct step chunks at section boundaries (heading-detected) and stitches the results; you can also stream:
async for page_result in recipe.stream("msa.pdf"): publish_clauses(page_result.fields.clauses) # incremental UI updatesSee Cookbook › Long PDFs for the streaming patterns.
Comparing revisions
Section titled “Comparing revisions”v1 = recipe("contract-v1.pdf")v2 = recipe("contract-v2.pdf")
from scriva.diff import diff_contractsreport = diff_contracts(v1, v2)report.added # new clausesreport.removed # dropped clausesreport.changed # (v1_clause, v2_clause) pairs with rewritesreport.to_markdown("redline.md")scriva.diff.diff_contracts is a thin wrapper over difflib keyed
on clause numbers + headings — enough for a redline, not enough for
a substitute for human review.
Retention & PII
Section titled “Retention & PII”Contracts contain names, addresses, sometimes salary or banking info. Follow the ID cookbook redaction pattern when your Worker is a third-party API.
HITL on signature blocks
Section titled “HITL on signature blocks”The cheap pattern for contract review: auto-accept clause bodies, gate every signature block:
recipe = recipe.review.hitl( when=lambda r: r.region.role == "signature" or (r.confidence or 0) < 0.7, sidecar="review.json",)See Orchestrator › Review for the two-phase variant for web UIs.
What to read next
Section titled “What to read next”- Worker — engines, decorators, composition.
- Cookbook › Long PDFs — chunking and streaming.
- Evaluation — measure clause-coverage on a corpus.