Extracting Deliverables and Milestones From a Statement of Work
9 min read · updated August 11, 2026
The useful output of reading a statement of work is not a list of deliverables. It is a list of triples: what gets delivered, what event releases money, and how much. Extract the deliverables alone and you have a project plan nobody can invoice against.
What makes an SOW hard
The information you want is distributed across sections that were written by different people. Deliverables sit in a scope section as prose. Milestone dates sit in a schedule table. Payment amounts sit in a fees section, often as a separate table with its own row labels. The acceptance criteria that decide whether a milestone has actually been met sit in yet another section, and sometimes only in the master agreement.
Those tables do not share a key. The fees table says “Design Phase”; the schedule table says “Milestone 2 — Design”; the scope section calls it “the Solution Design Document”. Joining them is a matching problem across near-synonymous labels, and it is the part of the job that a single-pass extraction prompt quietly gets wrong by inventing a plausible alignment. It is much safer to extract each table independently, keep the source labels verbatim, and do the join as an explicit, reviewable step.
A milestone is three things, not one
A milestone record that can be used downstream has, at minimum, a deliverable, a trigger and an amount — and the trigger is the field most often lost. “Payable on delivery” and “payable on acceptance” look like the same clause and differ by weeks of working capital, because acceptance usually has its own machinery: a review period, a deemed-acceptance rule if the customer does not respond, and a rejection-and-cure loop.
{
"sow_id": "SOW-2025-0042",
"total_contract_value": { "amount": 480000.00, "currency": "USD" },
"milestones": [
{ "ref": "M2",
"deliverable": "Solution Design Document",
"trigger": "acceptance",
"acceptance_review_days": 10,
"deemed_accepted_if_silent": true,
"amount": { "amount": 96000.00, "currency": "USD" },
"due": { "kind": "relative", "offset_days": 45,
"anchor": "project_start" },
"source": { "table": "Schedule A", "row": 2 } }
]
}The deemed_accepted_if_silent flag is worth carrying explicitly because it inverts who has to act. With it, a customer who says nothing for ten business days has accepted and owes the money; without it, the vendor waits indefinitely. Both are common. Neither is inferable from the milestone table.
Keep the source table and row on every record. When the footing check below fails, the first question is always which table a number came from, and highlighting the source region in the review queue turns a thirty-minute reconciliation into a click. The general case for provenance and field-level review is covered in extraction confidence; here it earns its keep specifically because two tables disagree often.
The footing check, and when it fails correctly
The check is trivial to state: the milestone amounts should sum to the stated total contract value — a cross-field amount validation of the ordinary kind. It is worth running on every SOW because it catches a dropped table row, a page break in the middle of the fees table, and a mis-read digit, all of which are common and none of which the model will flag on its own.
Take a stated total contract value of $480,000.00 and this milestone schedule: M1 kickoff $72,000.00, M2 design sign-off $96,000.00, M3 build complete $144,000.00, M4 UAT complete $96,000.00, M5 final acceptance $48,000.00. The sum is $456,000.00, which is $24,000.00 short — exactly 5% of the total. That gap is not an extraction error. It is retainage, held back under a separate payment clause and released after final acceptance, and it is stated in prose rather than as a table row.
There are four reasons a correct extraction fails the footing check, and a pipeline should distinguish them rather than routing all four to the same review queue:
- Retainage or holdback. A percentage withheld from each milestone or from the total, released on a later event. The shortfall is a round percentage of the total, which is a usable signal: if
(tcv - sum) / tcvlands on 0.05 or 0.10 exactly, look for a holdback clause before assuming a missed row. - Time-and-materials components. An SOW can be part fixed-fee and part T&M with a not-to-exceed ceiling. The T&M portion has no milestone amount, so the fixed milestones legitimately sum to less than the ceiling. The record needs a
pricing_modelper line, not one per SOW. - Optional or unexercised phases. “Phase 3, at Customer’s option” is frequently listed in the schedule and excluded from the total. Carry an
optionalflag and run the check twice, with and without. - Expenses excluded from the total. Travel and pass-through expenses are usually outside the contract value and capped separately. A sum that exceeds the stated total is most often this, or a percentage row double-counted as currency.
Percentage rows deserve their own mention. Some SOWs express milestones as percentages of the total and some as amounts, and a few mix both in one table. Normalise to a canonical amount at extraction time and keep the original representation alongside it, because 20% of $480,000.00 is exactly $96,000.00 but 33.3% three times is not 100%, and a rounding residue of a few dollars is a genuine finding rather than noise.
Relative dates and the kickoff problem
Most SOW milestone dates are not dates. They are offsets: “within thirty (30) days of the Project Start Date”, “four (4) weeks following acceptance of the preceding deliverable”. The anchor is often defined elsewhere as the date of the kickoff meeting, which has not happened when the document is signed.
Storing a computed absolute date at extraction time bakes in an assumption that will be wrong. Store the offset, the unit, the anchor and whether the count is in calendar or business days, and resolve to an absolute date only when the anchor is known. Business-day counts need a holiday calendar and a jurisdiction, and the SOW will not tell you which one — that is a field the ingesting system supplies.
Chained anchors compound. When M3 is “six weeks after acceptance of M2” and M2 is “thirty days after project start”, the schedule is a dependency graph, and a single slipped acceptance moves everything downstream. Represent the anchor as a reference to another milestone rather than flattening it, or the extracted schedule will describe a plan that stopped being true in week two.
Failure modes worth testing for
The fees table spans a page break. Header row on page 4, three rows on page 5 under no header at all. Layout-naive reading drops the orphaned rows or attaches them to the wrong table. This is reading order, not prompting; see PDF parsing and columns that shift across pages.
Negative numbers in parentheses. A credit line written as (12,000.00) read as positive flips a subtraction into an addition and breaks the footing by twice the amount — a distinctive signature worth detecting directly.
Decimal commas. A European SOW writes 96.000,00. Parsed with US conventions that becomes 96.0, off by a factor of a thousand. Decide the locale from the document, not from the parser default.
Two totals. A “Total Contract Value” in the fees section and a “Total” row at the foot of the milestone table that excludes expenses. They disagree legitimately. Extract both, name them differently, and check against the one the payment clause references.
Change orders. An SOW amended twice has a contract value that appears in three documents. As with a master agreement, the answer is an amendment chain: the effective schedule is the original plus each change order applied in order, and the footing check should run against the amended total.