Skip to content

Extracting Structured Fields From a Purchase and Sale Agreement

10 min read · updated August 11, 2026

Every deadline in a purchase and sale agreement is a small computation: an offset, from a basis date defined elsewhere in the document, counted in a unit the contract specifies. Extract the contingencies as one block of text and you have preserved none of them.

Contingencies are objects, not a paragraph

A residential purchase agreement typically carries three or four conditions that let a party walk away or renegotiate: a financing contingency, an inspection or due-diligence contingency, an appraisal contingency, and frequently a sale-of-buyer’s-property contingency. They are printed near each other, often in one numbered article, which is why so many extractions produce a single contingencies string. That string is unusable. Each contingency is independently satisfied, independently waived and independently enforceable, and each has its own clock.

Give each one a record with at least: the type, whether it is present at all, the deadline expression as printed, the resolved date, what satisfies it, and what happens on expiry. That last field matters more than it looks, because contracts differ on whether silence at the deadline waives the contingency or terminates the agreement. The two are opposite outcomes from the same absence of action, and it is a determination made from the contract’s own words — so capture the words.

Deadlines are computed, not printed

Here is the mechanism. Very few of these deadlines appear as dates. They appear as offsets: “within ten (10) days after the Effective Date”, “no later than five business days prior to Closing”, “within 21 days of Seller’s acceptance”. To turn that into a date you need three things the document also defines, and each of them is its own extraction problem.

  • The basis date. “Effective Date” is a defined term, and a very common definition is the date of the last signature by which the agreement became fully executed — which is not the date printed at the top of the form, not the offer date and not the first signature. If signature dates are handwritten next to each block, the basis date is the maximum of them, and a misread digit in any one of them shifts every downstream deadline.
  • The counting unit. Calendar days and business days produce different dates, and a contract that uses both in adjacent clauses is normal. The definitions section usually specifies how days are counted, whether the first day is excluded, and what happens when a deadline lands on a weekend or holiday.
  • The direction. “Prior to Closing” counts backwards from a date that may itself be an offset from something else. That is a two-step computation, and if the closing date moves, every backward-counted deadline moves with it.

So the deadline field is not a date. It is a small structure that keeps the inputs beside the output, so a change to the basis date can be recomputed rather than re-extracted, and so a human can see why your date is what it is:

{
  "type": "inspection",
  "present": true,
  "text_as_printed": "within ten (10) days after the Effective Date",
  "basis": "effective_date",
  "offset": 10,
  "unit": "calendar_days",
  "direction": "after",
  "resolved_date": "2026-05-04",
  "resolution_inputs": { "effective_date": "2026-04-24",
                         "day_count_rule": "section 21, first day excluded" },
  "on_expiry_text": "Buyer shall be deemed to have waived this contingency",
  "source": { "page": 3, "clause": "8(b)" }
}

resolved_date is derived and should be labelled as derived. Deadline computation under a contract is a legal question — what the parties’ obligations are on a given day is not something an extraction pipeline decides. What the pipeline can do honestly is show the arithmetic and the clause it came from, so the person whose job it is can check it in seconds instead of re-reading the agreement.

Addenda, riders and struck text

Almost no executed agreement is only the base form. There are addenda, riders, counter-offers and handwritten changes with initials beside them, and later documents override earlier ones. Three failure modes come out of this and all of them are silent.

The first is struck text that still extracts. A clause crossed through with a single pen line is fully legible to OCR and to a vision model, and unless you specifically ask about strike-through the model will read it as live text. Ask for it explicitly as a per-clause property, and treat a struck clause with initials beside it as a deletion with provenance rather than dropping it — the fact that it was struck is itself information.

The second is ordering. If a financing contingency appears in the base form with a ten-day deadline and again in an addendum with fourteen, the answer is fourteen only because the addendum is later. Your extraction has to know the document order, which means capturing each source document’s own date and its declared relationship to the others, then resolving. Store the resolved value and the layer it came from; never silently overwrite.

The third is the counter-offer that replaces a term without restating the clause — a single line saying the purchase price is now a different figure, with everything else unchanged. A per-document extraction handles this correctly only if the merge step exists at all. If your pipeline extracts each PDF independently and hands the results downstream, the merge happens in somebody’s head, inconsistently.

Money fields that look alike

Four or five dollar amounts appear within a page of each other and they are routinely confused: the purchase price, the initial earnest-money deposit, an additional deposit due after the inspection period, the loan amount, and any seller credit toward closing costs. They are distinguished by the label, not by magnitude, and labels vary between form families.

  • Numerals and words disagree. These forms print amounts twice — “Four Hundred Ninety Thousand Dollars ($490,000.00)”. That redundancy is a free validator on top of the usual currency-amount validation rules: parse both, compare, and flag a mismatch instead of picking one.
  • Handwritten amounts in blanks. The deposit figure is frequently written in by hand on a printed line. It carries no redundancy and no separator discipline, so it is the field most worth routing to review.
  • Zero and blank differ. A blank seller-credit line means no credit was agreed; a written zero means the parties addressed it. Preserve the difference.

A schema that survives a dispute

The test to design against is not “did the fields come out”. It is: six weeks later somebody asks why the system said the inspection period ended on the fourth. A schema that answers that carries, for every extracted value, the source document, the page, the clause reference and the verbatim text; for every computed value, the inputs and the rule; and for every overridden value, the layer that overrode it and the layer it replaced.

That is more storage than a flat record and it is the difference between a pipeline that is trusted and one that is checked by hand anyway. It is also not something to build per document type: the general form is an extraction field audit trail, and a purchase agreement is simply the document where the absence of one is felt soonest. Pair it with the rest of the ordinary machinery — confidence at the field level rather than a document score, and regression tests over a fixed set of executed agreements so a prompt change that breaks strike-through detection is caught by a test rather than by a client. The document-specific part — the part this page is about — is that the deadline is a computation and the contingency is an object.