Free kit / Contract OCR API Pack

Contract OCR API: Clean JSON Out of Messy PDFs

Template based OCR breaks the moment a field moves an inch. You get the vision prompt, the enforced schemas and the validation layer I use to turn contracts and filings into strict JSON.

16failure signals, each with a lookup location
9worksheets you fill in on your own stack
7sections, in the order that makes the diagnosis

Why generalist models fail on dense legal PDFs, and the evaluation that settled it, is in Contract OCR API: Extract Structured Legal Data with Claude 4.7. Related: the Workflow Audit Kit finds where the documents are getting stuck before you build anything.

01

Before you start

This pack is the extraction layer I put in front of any document pipeline that has to produce structured records instead of a pile of text. The vision model reads the page in context. The schema decides what is allowed to leave. The validator decides what is allowed to write. You get the prompt, the schemas and the Python call that runs them. The thresholds and the retry wording sit beside it.

Template based OCR treats a page as a fixed map of coordinates, with every field pinned to one spot and every label to another. It works on one clean scan and falls apart on everything else, from a retainer with new letterhead to a lease with a stapled addendum to an ID photo taken at an angle. A vision-language model reads the page the way a person does, through layout, tables, handwriting and shifted blocks. The schema is what keeps that read honest.

Ground rules

Never let the model write directly into a system of record. Extraction produces a candidate payload. Validation decides if the payload is safe. Confidence decides if a human sees it. Only then does anything write. Keep the original document beside every extracted object so any field can be traced back to a page.

02

Section 1 · Why templates fail and context reads hold

Template OCR fails in specific, repeatable ways. Name the failure, and you know which layer to replace. Work the tables below against your current extractor before you change anything.

Table 1A · Template failure modes

Failure modeWhat it looks likeWhy the template breaksContext read behaviour
Field driftExample: notice clause sits two lines lower after a letterhead changeAnchor box no longer overlaps the valueModel reads the clause from its heading
Layout variants
Multi-page tables
Handwritten annotations

Table 1B · Document inventory (fill this in)

List every document type you extract. If a type appears monthly or more, it ships a schema. If it appears yearly, route it to the generic contract schema and review.

Document typeMonthly volumeFields downstream needsCurrent extractorKnown break pattern
Example: engagement / retainer40parties, fee, term, notice days, jurisdictionZapier + fixed templatesfee block drifts on new letterhead

What a context read does

LayerWhat it decidesPass conditionCommon failure
Page levelDocument type, page count, whether the scan is legible at allOne type from your enum, every page accounted for, no unreadable flagAn exhibit pack arrives as one PDF and the model classifies by the cover sheet
Region levelWhich blocks are parties, consideration, term, notice, governing law, signatureEach required region located or explicitly absent, with page numberDefinitions mistaken for the operative clause
Field levelThe typed value plus the source quote that proves itValue, type, evidence quote and page on every required fieldA number copied from a schedule instead of the operative clause
What context does not give you

A licence to accept whatever comes back. The model can still invent a date, misread a currency, or pick the wrong of two parties. That is why the schema, the validator and the escalation queue are not optional extras. They are the product.

03

Section 2 · The extraction prompt and its output rules

One prompt template, parameterised by document type and target schema. The rules are hard on purpose. A prompt that allows prose allows silent parse failures downstream.

Extraction prompt template

ROLE You extract structured fields from legal documents. You never summarize, never infer, never invent. INPUT document_type: {{engagement | nda | service_agreement | lease | court_filing | identity | contract}} matter_hint: {{case_type if known}} target_schema: {{schema_name}} page_count_hint: {{expected pages, or null}} RULES 1. Return JSON only. No prose before or after. 2. Wrap output exactly as: <response><json>{...}</json></response> 3. Read numbers as they appear in the document. Spell-out numbers convert to digits: "thirty-six thousand six hundred five" becomes 36605. If the digits and the words disagree, set numeric_conflict: true and use neither value. 4. Dates become YYYY-MM-DD. If only a partial date exists, set date_precision to year, month or day. 5. Jurisdiction is the stated one. If absent or ambiguous, return null. Do not guess from the party names. 6. Every field in the required list must be present. Use null for genuinely absent values, never an empty string. 7. Quote source text for each extracted value under "evidence". If you cannot locate the source text, the field is null. 8. Party fields capture the full legal name as written. Roles are separate fields. Do not merge a company and its signatory into one string. 9. Currency is ISO 4217. If the document states a symbol with no currency code, infer the code only when the symbol is unambiguous for the stated jurisdiction. Otherwise currency: null and amount: null. 10. Never average, round or normalise a figure the document states. Transcribe, then let code normalise. 11. If two sections disagree on the same fact, keep both under "conflicts" and set the primary field to null. 12. If the document is unreadable, return {"status":"unreadable","reason":"..."}. OUTPUT JSON object conforming to the target schema.

What each rule prevents

RuleFailure it stopsWhere it bites if you drop it
1 JSON onlyMarkdown fences and commentary around the objectEvery downstream parse step
2 response / json wrapperModel talking past a strict extractorRetry logic, logging, schema registry
3 digit vs word numbersInvented numeric conversionsFees, deposits, penalties, damages
4 date precisionFalse day-level confidence on a month-only dateNotice windows, limitation periods
5 jurisdiction as statedGuessed governing law from a party addressVenue, conflict checks, court routing
6 null not empty stringEmpty writes that look like real valuesCRM required fields, search indexes
7 evidence per fieldFields nobody can trace back to a pageReviewers, audits, corrections
8 party roles separateCompany and signatory collapsed into one nameDedupe, conflict check, service lists
9 currency explicitSilent USD assumption on a EUR documentEvery money field
10 transcribe firstModel rounding 36605 into 36600Fee schedules, escalators
11 conflicts arrayOne of two disagreeing values chosen at randomAmendments, riders, prior drafts
12 unreadable statusConfident junk from a blank or corrupt pageBatch jobs that would write garbage

Retry wording (use as written)

When the first pass fails envelope parse or schema validation, retry once with a reduced prompt. Do not argue with the model. Narrow the job.

RETRY The previous response did not validate against the target schema. Return JSON only, wrapped as <response><json>{...}</json></response>. Extract ONLY these fields: {{comma separated required field list}} If a field is not on the page, return null. If two sections disagree, set the field to null and list the quotations under "conflicts". Do not explain. Do not add fields.

Two attempts is the ceiling. A third attempt is superstition. After two failures the document goes to the reviewer queue with both raw responses attached.

04

Section 3 · Schemas that reject anything the model invents

There is one base envelope and six type-specific property sets. The envelope carries status, confidence, evidence and conflict state. The property sets carry the fields that differ by document. Validate the envelope first, then the property set for the type you requested. Anything not in the schema is dropped before it reaches a record.

Base envelope schema

{ "$id": "ocr/envelope.v1.json", "type": "object", "properties": { "document_type": {"enum": ["engagement","nda","service_agreement", "lease","court_filing","identity","contract"]}, "target_schema": {"type": "string"}, "schema_version": {"type": "string"}, "page_count": {"type": "integer", "minimum": 1}, "status": {"enum": ["ok", "partial", "unreadable", "conflict"]}, "confidence": {"type": "number", "minimum": 0, "maximum": 1}, "meets_threshold": {"type": "boolean"}, "numeric_conflict": {"type": "boolean"}, "date_precision": {"enum": ["day", "month", "year", null]}, "jurisdiction": {"type": ["string", "null"]}, "evidence": { "type": "object", "additionalProperties": { "type": "object", "properties": { "quote": {"type": "string"}, "page": {"type": "integer", "minimum": 1}}, "required": ["quote", "page"]} }, "conflicts": { "type": "array", "items": { "type": "object", "properties": { "field": {"type": "string"}, "values": {"type": "array", "items": {"type": "string"}}, "pages": {"type": "array", "items": {"type": "integer"}}}, "required": ["field", "values"]} }, "fields": {"type": "object"} }, "required": ["document_type","target_schema","schema_version","status", "confidence","meets_threshold","numeric_conflict", "evidence","fields"], "additionalProperties": false }

Three points. additionalProperties: false is the rejection valve for invented keys. meets_threshold lets your orchestrator branch on a boolean instead of parsing free text. evidence is keyed by field name so a reviewer opens one object and sees both the claim and the page.

Type property sets

Each block below is the fields object for one document type, plus its required list. Load the base envelope, then the set for the requested type. Null means absent. Empty string is a validation failure.

// 1. ENGAGEMENT / RETAINER "fields": { "client_name": {"type": ["string","null"]}, "firm_name": {"type": ["string","null"]}, "responsible_attorney": {"type": ["string","null"]}, "practice_area": {"type": ["string","null"]}, "fee_type": {"enum": ["hourly","flat","contingency", "retainer","hybrid",null]}, "fee_amount": {"type": ["number","null"], "minimum": 0}, "currency": {"type": ["string","null"], "maxLength": 3}, "billing_interval": {"enum": ["hour","month","matter","on_event",null]}, "retainer_amount": {"type": ["number","null"], "minimum": 0}, "effective_date": {"type": ["string","null"]}, "termination_notice_days": {"type": ["integer","null"], "minimum": 0}, "scope_summary": {"type": ["string","null"]}, "case_type": {"type": ["string","null"]}, "urgency": {"enum": ["low","normal","high","critical",null]} } "required_fields": ["client_name","firm_name","fee_type", "effective_date","jurisdiction"] // 2. NDA "fields": { "disclosing_party": {"type": ["string","null"]}, "receiving_party": {"type": ["string","null"]}, "mutual": {"type": ["boolean","null"]}, "effective_date": {"type": ["string","null"]}, "term_months": {"type": ["integer","null"], "minimum": 0}, "survival_months": {"type": ["integer","null"], "minimum": 0}, "confidential_info_scope": {"type": ["string","null"]}, "permitted_use": {"type": ["string","null"]}, "return_or_destroy_days": {"type": ["integer","null"], "minimum": 0}, "injunctive_relief": {"type": ["boolean","null"]}, "governing_law": {"type": ["string","null"]}, "non_solicit_months": {"type": ["integer","null"], "minimum": 0} } "required_fields": ["disclosing_party","receiving_party", "effective_date","jurisdiction"] // 3. SERVICE AGREEMENT / SOW "fields": { "provider_name": {"type": ["string","null"]}, "customer_name": {"type": ["string","null"]}, "service_description": {"type": ["string","null"]}, "start_date": {"type": ["string","null"]}, "end_date": {"type": ["string","null"]}, "auto_renew": {"type": ["boolean","null"]}, "renewal_notice_days": {"type": ["integer","null"], "minimum": 0}, "total_contract_value": {"type": ["number","null"], "minimum": 0}, "currency": {"type": ["string","null"], "maxLength": 3}, "payment_terms_days": {"type": ["integer","null"], "minimum": 0}, "sla_uptime_pct": {"type": ["number","null"], "maximum": 100}, "liability_cap_multiple": {"type": ["number","null"], "minimum": 0}, "termination_for_convenience_days": {"type": ["integer","null"], "minimum": 0} } "required_fields": ["provider_name","customer_name","start_date","jurisdiction"] // 4. LEASE "fields": { "landlord_name": {"type": ["string","null"]}, "tenant_name": {"type": ["string","null"]}, "property_address": {"type": ["string","null"]}, "lease_type": {"enum": ["commercial","residential", "sublease","ground",null]}, "commencement_date": {"type": ["string","null"]}, "expiration_date": {"type": ["string","null"]}, "base_rent_amount": {"type": ["number","null"], "minimum": 0}, "currency": {"type": ["string","null"], "maxLength": 3}, "rent_frequency": {"enum": ["month","quarter","year",null]}, "security_deposit": {"type": ["number","null"], "minimum": 0}, "escalation_pct": {"type": ["number","null"], "minimum": 0}, "renewal_options": {"type": ["integer","null"], "minimum": 0}, "use_clause": {"type": ["string","null"]}, "square_footage": {"type": ["number","null"], "minimum": 0} } "required_fields": ["landlord_name","tenant_name","property_address", "commencement_date","jurisdiction"] // 5. COURT FILING "fields": { "court_name": {"type": ["string","null"]}, "caption_plaintiff": {"type": ["string","null"]}, "caption_defendant": {"type": ["string","null"]}, "case_number": {"type": ["string","null"]}, "document_title": {"type": ["string","null"]}, "filing_date": {"type": ["string","null"]}, "filing_party": {"type": ["string","null"]}, "filing_type": {"enum": ["complaint","motion","brief","order", "notice","exhibit","declaration", "other",null]}, "hearing_date": {"type": ["string","null"]}, "response_deadline": {"type": ["string","null"]}, "certificate_of_service": {"type": ["boolean","null"]}, "exhibit_count": {"type": ["integer","null"], "minimum": 0} } "required_fields": ["court_name","case_number","document_title","jurisdiction"] // 6. IDENTITY DOCUMENT "fields": { "doc_kind": {"enum": ["passport","drivers_license", "national_id","residence_permit", "other",null]}, "full_name": {"type": ["string","null"]}, "given_names": {"type": ["string","null"]}, "family_name": {"type": ["string","null"]}, "document_number": {"type": ["string","null"]}, "issuing_country": {"type": ["string","null"]}, "issuing_authority": {"type": ["string","null"]}, "date_of_birth": {"type": ["string","null"]}, "issue_date": {"type": ["string","null"]}, "expiry_date": {"type": ["string","null"]}, "address_line": {"type": ["string","null"]}, "mrz_present": {"type": ["boolean","null"]} } "required_fields": ["doc_kind","full_name","document_number","expiry_date"]
Schema versioning

Every payload carries schema_version. Adding a field bumps the minor version, and you accept both shapes for one month. Removing or retyping a field bumps the major version and stops the old shape. Store the version on the extracted object so a later reader knows which rules produced it.

Table 3A · Property set checklist (fill this in)

Document typeRequired fields (comma list)Enum values your CRM acceptsConfidence floorSchema version
Engagement / retainerclient_name, firm_name, fee_type, effective_date, jurisdictionfee_type: hourly, flat, retainer0.881.0
NDA
Service agreement
Lease
Court filing
Identity document
05

Section 4 · Validation: numbers, dates, cross references

Validation runs in code. The prompt asks for honesty. The validator enforces it. Run every check below on every payload before anything writes.

Validation rules

  • Envelope check: the <response><json> wrapper parses, then the inner object parses as JSON. Reject and retry with the reduced prompt if not.
  • Type check: every field matches its declared type. Integer fields reject floats. Number fields reject strings with currency symbols still attached.
  • Required check: every field in the type's required list is present. Null is allowed only where the schema allows it. A required field that is null forces escalation.
  • Range check: amounts non-negative, notice days between 0 and your ceiling, confidence between 0 and 1, expiry dates in the past on identity documents.
  • Enum check: fee type, lease type, filing type, urgency and jurisdiction limited to values your CRM accepts. Anything else is a mapping failure and gets rejected.
  • Numeric consistency: if digits and words disagree, or the same figure appears twice with different values, set numeric_conflict and stop. Language models hallucinate numeric conversions. This is the most expensive class of error in a legal document.
  • Date shape and order: values match their date_precision. End date after start date. Expiry after issue. Hearing date not before filing date. Notice windows consistent with the termination clause.
  • Cross-reference parties: party names appear in the evidence map. Each party name is checked against your conflict list and existing contact records before any write.
  • Evidence present: each required field has a source quote and a page. A field with no evidence is treated as absent.
  • Unknown key check: any property outside the schema is dropped and logged. The model does not get to invent fields.

Cross-check matrix

CheckRuleOn failSeverity
Money: digits vs wordsBoth parse to the same integer amountSet numeric_conflict, drop amountBlock write
Money: fee vs scheduleOperative clause figure equals schedule figureRecord both under conflictsBlock write
Money: currency presentISO code on every amount fieldNull amount and currencyBlock write
Dates: precisionYear-only dates never written as day-levelDowngrade precision, flagWarn
Dates: orderingend after start, expiry after issue, hearing after filingNull both ends, escalateBlock write
Dates: notice windownotice days match the termination clauseRecord both under conflictsEscalate
Parties: evidenceEvery party name quotable on a pageNull the partyEscalate
Parties: role splitCompany and signatory are separate valuesMerge attempt rejectedBlock write
Parties: conflict listNo match against known conflict recordsStop pipeline, conflict reviewBlock write
Jurisdiction: statedPresent in text, not inferredNull, attorney reviewEscalate
Page accountingpage_count equals received pagesRequest missing pagesBlock write

Python: vision call, schema validation, routing

One module. Call the model with the document and the prompt. Parse the envelope. Validate against the base schema and the type property set. Run the cross-checks. Route on the result. This is the shape I ship.

import base64, json, re import anthropic from jsonschema import Draft7Validator client = anthropic.Anthropic() ENVELOPE = json.load(open("schemas/ocr/envelope.v1.json")) SETS = json.load(open("schemas/ocr/property_sets.v1.json")) FLOORS = {"engagement": 0.88, "nda": 0.86, "service_agreement": 0.86, "lease": 0.87, "court_filing": 0.85, "identity": 0.92, "contract": 0.85} WRAPPER = re.compile(r"<response><json>(.*)</json></response>", re.S) JURISDICTION_REQUIRED = {"engagement", "nda", "service_agreement", "lease", "court_filing", "contract"} def doc_block(path): data = open(path, "rb").read() b64 = base64.b64encode(data).decode() if path.endswith(".pdf"): return {"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": b64}} return {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64}} def parse_envelope(text): m = WRAPPER.search(text or "") if not m: raise ValueError("envelope_missing") return json.loads(m.group(1)) def validate_payload(payload, doc_type): errs = [f"envelope:{e.message}" for e in Draft7Validator(ENVELOPE).iter_errors(payload)] fields = payload.get("fields") or {} props = SETS[doc_type]["fields"] wrapped = {"type": "object", "properties": props, "additionalProperties": False} errs += [f"fields:{e.message}" for e in Draft7Validator(wrapped).iter_errors(fields)] for name in SETS[doc_type]["required_fields"]: if name not in fields: errs.append(f"missing_required:{name}") elif fields.get(name) is None and name != "jurisdiction": errs.append(f"null_required:{name}") return errs def cross_checks(payload): errs = [] fields = payload.get("fields") or {} if payload.get("numeric_conflict"): errs.append("numeric_conflict") pairs = [("effective_date", "end_date"), ("effective_date", "expiration_date"), ("start_date", "end_date"), ("commencement_date", "expiration_date"), ("issue_date", "expiry_date"), ("filing_date", "hearing_date")] for a, b in pairs: if fields.get(a) and fields.get(b) and str(fields[b]) <= str(fields[a]): errs.append(f"date_order:{a}>={b}") for name, cell in (payload.get("evidence") or {}).items(): if not cell.get("quote") or not cell.get("page"): errs.append(f"evidence_incomplete:{name}") dt = payload.get("document_type") if dt in JURISDICTION_REQUIRED and not payload.get("jurisdiction"): errs.append("jurisdiction_missing") return errs def retry_prompt(doc_type): required = ", ".join(SETS[doc_type]["required_fields"]) return ("RETRY\nReturn JSON only, wrapped as " "<response><json>{...}</json></response>.\n" f"Extract ONLY these fields: {required}\n" "If a field is not on the page, return null. " "Do not explain. Do not add fields.") def extract(path, doc_type, schema_name): system = "You extract structured fields from legal documents." base_prompt = open("prompts/extraction.v1.txt").read().format( document_type=doc_type, matter_hint="null", target_schema=schema_name, page_count_hint="null") prompt, raw = base_prompt, "" for attempt in (1, 2): msg = client.messages.create( model="claude-sonnet-4-5", max_tokens=4096, system=system, messages=[{"role": "user", "content": [ doc_block(path), {"type": "text", "text": prompt}]}]) raw = msg.content[0].text try: payload = parse_envelope(raw) except (ValueError, json.JSONDecodeError): prompt = retry_prompt(doc_type) continue errs = validate_payload(payload, doc_type) + cross_checks(payload) floor = FLOORS[doc_type] ok = (payload.get("confidence", 0) >= floor and not errs and not payload.get("numeric_conflict")) payload["meets_threshold"] = ok if ok: return {"result": "accepted", "payload": payload, "attempt": attempt} prompt = retry_prompt(doc_type) reason = "two_failed_attempts" if errs else "below_confidence_floor" return {"result": "escalate", "payload": payload, "errors": errs, "raw_responses": [raw], "reason": reason}
Do not skip the validator because the model looked right.

A payload can be fluent, complete and wrong on the fee. The validator is the only layer that does not care how convincing the JSON is.

Table 4A · Validation worksheet (fill this in)

RuleWhere you implement itSample fail you caughtAction wired
Numeric consistencycross_checks() in extract.py36605 vs "thirty-six thousand six hundred"numeric_conflict, escalate
Date ordering
Currency present
Party evidence
Conflict list match
Required field null
Unknown key drop
06

Section 5 · Escalating to a human when confidence drops

A low confidence score is still information. The model is telling you it is unsure. The correct response is a reviewer with the payload and the source page.

Table 5A · Confidence thresholds and retry wording

Document typeFloorAttemptsRetry emphasisEscalate to
Engagement / retainer0.882fee_type, fee_amount, notice daysParalegal queue, SLA 2h
NDA0.862parties, term, survivalParalegal queue, SLA 4h
Service agreement0.862TCV, payment terms, liability capOps queue, SLA 4h
Lease0.872address, dates, rent and escalationParalegal queue, SLA 4h
Court filing0.852case number, dates, captionAttorney review, SLA 1h
Identity document0.921no second attempt on identity dataCompliance queue, same day

Floors are starting points. Raise a floor when a document type starts producing accepted payloads that reviewers keep correcting. Lower it only with evidence that reviewers are re-reading correct output.

Escalation triggers

TriggerWhy it mattersRoute to
Confidence below the type floorThe model is telling you it is unsureReviewer queue, SLA timer on
Required field returned nullCRM write would land incompleteReviewer queue
numeric_conflict: trueMoney figures are not a place to guessReviewer queue, block downstream
Conflicting dates across sectionsNotice periods and limitations depend on themReviewer queue
Jurisdiction absent or ambiguousDrives routing, conflicts and courtAttorney review, never auto-assign
Scanned image quality poor or pages missingExtraction cannot be trusted at allRe-request document
Party name matches a conflict recordCannot proceed to matter or deal creationConflict review, pipeline stopped
Two failed extraction attemptsRetrying a third time is superstitionReviewer queue

Escalation queue record

{ "queue_item_id": "ocr_esc_01J2K8Q2", "document_id": "doc_9f3a_retn_2026_04", "document_type": "engagement", "correlation_id": "corr_ocr_01J2K8Q2", "idempotency_key": "ocr:engagement:doc_9f3a:attempt2", "reason": "below_confidence_floor", "confidence": 0.79, "floor": 0.88, "attempt_count": 2, "errors": ["null_required:fee_amount", "evidence_incomplete:client_name"], "payload": { "...": "candidate object as returned" }, "raw_responses": ["attempt1", "attempt2"], "source_pages": [1, 3, 4], "sla_minutes": 120, "assigned_role": "paralegal", "status": "open" }

Reviewer checklist

  • Open the source page beside the candidate payload. Work from the page itself.
  • Confirm every required field against the evidence quote before changing anything.
  • Correct by writing a corrected object. Never overwrite the model output. Both versions stay.
  • Record why the model was wrong: layout, illegible, ambiguous clause, bad page split.
  • If the same reason appears three times in a week, change the prompt or the schema. Do not keep correcting by hand.
  • Close the item with a decision: accept as corrected, reject and re-request, or block for attorney sign-off.
Queue rules

One role per document type. SLA on every open item, escalating when it ages. Rejection is a valid outcome and routes to a named person for a decision. Corrections append. Bulk close is disabled.

Related builds

If you want the pipeline built rather than assembled, that is the document processing agents service.

07

Section 6 · FAQ

Does this replace my current OCR tool?

It replaces the extraction and validation layer. If a tool already stores and versions documents, keep it. Point the tool at this call instead of its built-in template mapper.

Which model does the Python call use?

A vision-capable model API with zero data retention configured on your account. The sample uses an Anthropic messages call with a document block. Swap the client, keep the schema and the validator.

Why not ask the model harder?

Because a prompt cannot enforce a type. It can only request one. The schema rejects invented keys, the validator rejects bad values, and the queue catches the rest. Three layers, each one mechanical.

What about handwritten signatures and stamps?

Treat them as evidence that execution happened. Capture presence, date if legible, and the page. Do not ask the model to invent a signatory name from a flourish.

Can I add a seventh document type?

Yes. Copy a property set, list the required fields, pick a confidence floor from the closest neighbour, and version the schema. Table 3A is the worksheet for it.

What if the break is downstream of extraction?

Then it is a wiring problem. The free workflow diagnostic maps that path. This pack stays useful as the extraction layer underneath it.

Run the free workflow diagnostic, or read the case study, then decide. The diagnostic reads your current document path and tells you which layer is failing. The D'Alessio Law Group case study shows the document and intake pipeline end to end. When the pack is not enough, the same systems ship as client engagements through document processing agents.

08

Get the fillable workbook

Everything above is yours to read. The workbook adds the empty worksheets, so you fill them in on your own stack with your own owners, fields and failures. It opens in any browser, on any device, and it saves as you type.

When you want my read on your own stack, the document processing questionnaire takes ten minutes, and I reply with the first fix I would make and what it costs.