Skip to content

Extracting the Payment Schedule From a Multi-Year Pledge Agreement

9 min read · updated August 11, 2026

A pledge agreement contains a total and a table, and the table should add up to the total. It is the rare extraction where you can check your own output arithmetically — provided the check includes the payment made at signing and tolerates a schedule that does not divide evenly.

What is in a pledge and what is not

A pledge is a promise to pay, not a payment. Nothing has been received when it is signed, which is why a pledge document is not an acknowledgment and why the receipt schema does not fit it. The fields that matter are:

  • Total pledged amount, and its currency. Stated once, usually in both words and figures, and the two can disagree.
  • The installment schedule: a row per payment with a due date and an amount, sometimes with a payment method per row.
  • A payment accompanying the agreement, if any. Often in prose rather than in the table.
  • Restriction or designation — whether the gift is unrestricted, restricted to a named purpose, or an endowment. Free text, and the most valuable free text on the page.
  • Conditions: a matching requirement, a naming right, a contingency on a campaign reaching a threshold. A conditional installment is not the same object as an unconditional one and should carry a flag rather than being flattened into the amount.
  • Revocability and anonymity clauses, and the signature block with dates.

What is not in a pledge, and should not be invented into the schema: any payment status. Whether installment three was received lives in the ledger, not in the agreement. A schema with a paid field on each row invites the model to guess, and it will guess “false”, and that guess will overwrite a true record somewhere downstream.

The sum check, and why the naive version fails

The obvious assertion is that the installments sum to the total. Here is a synthetic schedule where it works:

Total pledged: 250,000.00

  2026-12-31    50,000.00
  2027-12-31    50,000.00
  2028-12-31    50,000.00
  2029-12-31    50,000.00
  2030-12-31    50,000.00
  ---------------------------
  sum           250,000.00     matches the stated total

Now the same check on a pledge that is drafted just as correctly and fails it:

Total pledged: 1,000,000.00
Received with this agreement: 100,000.00

  2027-06-30   150,000.00
  2028-06-30   150,000.00
  2029-06-30   150,000.00
  2030-06-30   150,000.00
  2031-06-30   150,000.00
  2032-06-30   150,000.00
  ---------------------------
  sum          900,000.00     stated total is 1,000,000.00  -> "off by 100,000"

Nothing is wrong with the document. The initial payment is in a sentence above the table, the table is the remaining schedule, and the correct identity is:

initial_payment + sum(installments) == total_pledged
100,000.00     + 900,000.00           == 1,000,000.00

This is the whole reason the extraction schema needs an explicit initial_payment field that defaults to zero rather than being absent. An absent field and a zero field validate identically in the happy case and diverge exactly here, where the model failed to find a sentence it should have found. Making the field required with an explicit null forces the difference between “the document says there was no payment at signing” and “nothing was read” to survive into the record, which is the same argument as handling a required field that is missing from the source.

Rounding: a tolerance with a reason

The second false failure is arithmetic rather than structural. Split $250,000 into three equal annual installments and no set of three equal cent-precise amounts exists: 250000 / 3 is 83,333.333…, and three payments of 83,333.33 sum to 249,999.99. Drafters resolve it by absorbing the remainder in the final installment:

  2026-12-31    83,333.33
  2027-12-31    83,333.33
  2028-12-31    83,333.34     <- absorbs the remainder
  ---------------------------
  sum          250,000.00

Some drafters instead leave all three at 83,333.33 and let the last payment be trued up in practice, in which case the extracted table genuinely sums a cent short and the document is still fine. An exact-equality assertion turns both into alerts. The tolerance that has a reason behind it is one cent per row:

tolerance = 0.01 * number_of_installment_rows
abs(initial_payment + sum(installments) - total_pledged) <= tolerance

A five-row schedule tolerates five cents. A discrepancy of $150,000 — a whole missing installment — blows past it immediately, which is the case you actually want to catch. Do not widen this to a percentage: a percentage tolerance on a million-dollar pledge quietly accepts a ten-thousand-dollar error, and the errors that occur in practice are missing rows and transposed digits, both of which are large.

Compare amounts as scaled integers or decimals, never as floats. Adding three IEEE doubles of 83333.33 does not give 249999.99 exactly, and the resulting phantom failures are indistinguishable from real ones until somebody prints the intermediate value.

Dates that are not dates and amounts that are conditional

Pledge tables frequently label rows relatively: “Year 1”, “Year 2”, or “on each anniversary of this agreement”. There is no date in the cell to extract. The correct output is not a guess — it is the relative expression plus the anchor needed to resolve it:

{
  "sequence": 2,
  "due": { "kind": "relative", "expression": "anniversary+2y", "anchor": "agreement_date" },
  "resolved_due_date": null,
  "amount": "150000.00"
}

Resolve it in code from agreement_date, and leave resolved_due_date null when the anchor itself was not found. The alternative — asking the model to compute the dates — puts calendar arithmetic inside a component that has no reason to be good at it, and produces confident wrong dates rather than nulls. The same principle drives the appeal-deadline handling on a warranty denial letter, where the trigger and the window are extractable and the deadline is not.

Conditional installments need the same treatment. “$150,000 in 2029, contingent upon the Foundation securing matching funds of not less than $150,000” is an amount with a predicate attached. Flatten it to 150000.00 and the schedule sums correctly while representing a commitment that may never exist. Carry conditional: true and the condition text, and run the sum check twice — once over all rows against the stated total, once over unconditional rows only for anything that feeds a revenue forecast.

Amendments, and the table that crosses a page

Two structural failures account for most of the rest.

The first is the page break. A ten-year schedule does not fit on the signature page, and the continuation on page two usually repeats the column header. A row-wise reader either drops the first continuation row or ingests the repeated header as a row with a null amount. The defence is a sequence check independent of the sum check: installment sequence numbers, or due dates, must form an unbroken increasing series. A schedule that jumps from 2029 to 2031 has lost a row even if nobody notices that the total is short. This is the same class of problem as a table whose columns do not align between pages, and it is worth reading that page rather than solving it again here.

The second is the amendment. Pledges get restructured — a donor asks to stretch five years into eight, and an amendment is signed that restates the remaining schedule. The amendment looks almost exactly like the original document, including a total, which means an extraction pipeline that treats every arriving PDF as an independent pledge will book the same commitment twice. Capture document_kind (original or amendment), any reference to the agreement being amended, and the effective date, and let the reconciliation happen against the donor record. If the amendment restates the full total rather than only the remaining balance — both drafting styles exist — the sum check on the amendment passes in isolation and the double-count is invisible to it. Only the document-kind field catches that one, which is why it belongs in the schema even though it is never printed as a labelled field on the page.