Takeoff schema
The TakeoffReport is the structured contract between the client LLM and downstream
estimating tools. It's a Pydantic model, validated strictly by
generate_takeoff_report — bad reports come back as readable validation errors, not
silent garbage. Field names intentionally match the contractor-bid BOM format
(qty/uom/status/source/customFields) so exports drop straight into the
ContractorTakeoff suite (see Integrations).
Current schema_version: 1.
TakeoffReport
A complete takeoff for one plan set.
| Field | Type | Default | Description |
|---|---|---|---|
schema_version |
int |
1 |
Schema version |
project |
str |
required | Project name |
source_pdf |
str |
required | Plan set file the takeoff came from |
scope_profile |
str |
"fences-gates" |
Trade profile used |
generated_by |
str |
"conmcp" |
Tool identifier |
generated_at |
str or null |
null |
ISO timestamp, set at export if missing |
items |
list[TakeoffItem] |
[] |
The line items |
scope_notes |
list[str] |
[] |
Inclusions/clarifications |
risks |
list[str] |
[] |
Red flags a bidder must see |
TakeoffItem
One takeoff line item (a BOM row).
| Field | Type | Default | Description |
|---|---|---|---|
id |
str |
required | Stable item id, e.g. 'fence-cl-6ft' |
section |
str |
"general" |
Trade grouping, e.g. 'fences-gates' |
item |
str |
required | Short item code/name, e.g. 'CHAIN-LINK-6FT' |
description |
str |
required | Full description as an estimator would write it |
qty |
float (≥0) |
required | Quantity taken off |
uom |
str |
required | Unit of measure: LF, EA, SF, SY, CY, PAIR, TON, LS |
status |
str |
"Included" |
'Included', 'Excluded', or 'Flag <reason>' |
source |
str |
"" |
Sheet reference, e.g. 'C-101 p.2' |
location |
str |
"" |
Where on the project, e.g. 'north property line' |
assumptions |
list[str] |
[] |
Assumptions behind the quantity |
confidence |
float (0–1) |
0.5 |
Extraction confidence |
needs_review |
bool |
true |
Flag for human review before bidding |
custom_fields |
dict[str, str \| float \| bool] |
{} |
Trade-specific extras, e.g. post_spacing, fabric_gauge, gate_leaf_count |
Note the defaults are deliberately conservative: an item is needs_review: true at 50%
confidence unless the model explicitly says otherwise. Sight-unseen trust is not the
default.
UOM vocabulary
The shared unit vocabulary (single source of truth in domain/quantities.py):
LF · SF · SY · CY · EA · PAIR · TON · LS
UOMs are normalized to uppercase on validation. A unit outside this list does not
fail validation — it gets reported in flagged_uoms by generate_takeoff_report so a
human can decide whether it's legitimate (say, SETS) or a mistake before export.
Status values
status is validated: it must be exactly Included, exactly Excluded, or start with
Flag followed by a reason:
Included— in the bid scopeExcluded— documented out of scope (e.g."BY OTHERS"/ NIC items — recorded so the exclusion is explicit, not silent)Flag <reason>— needs a decision, e.g.Flag plan/schedule mismatch — RFI candidate
Export formats
generate_takeoff_report (and conmcp report on the CLI) renders a validated report
three ways.
JSON — contractor-bid compatible (to_bid_json)
The BOM line_items shape consumed by contractor-bid:
{
"schema_version": 1,
"project": {
"name": "Oakridge Commerce Park — Phase 2",
"scope_profile": "fences-gates"
},
"line_items": [
{
"id": "fence-cl-6ft",
"section": "fences-gates",
"item": "CHAIN-LINK-6FT-BW",
"description": "6' high galvanized chain link fence w/ 3-strand barbed wire",
"qty": 450.0,
"uom": "LF",
"status": "Included",
"source": "C-101 p.2",
"notes": "Posts 8'-0\" O.C. per detail 1/C-501",
"customFields": {
"post_spacing": 8,
"fabric_gauge": "9 GA",
"location": "north property line",
"confidence": 0.9,
"needs_review": false
}
}
],
"metadata": {
"extracted_by": "conmcp conmcp",
"extracted_at": "2026-07-10T14:30:00+00:00",
"source_pdf": "/Users/you/plans/sample_plan.pdf",
"scope_notes": ["Gate operator power and loops by electrical contractor (BY OTHERS)"],
"risks": ["Verify field dimensions prior to fabrication per general note 5"]
}
}
Mapping notes: item assumptions are joined with "; " into notes; location,
confidence, and needs_review travel inside customFields alongside any
trade-specific extras.
CSV — pma-standalone takeoff layout
One row per item, columns in this order:
Section, Item, Description, Qty, UOM, Status, Source, Location,
Confidence, NeedsReview, Assumptions, CustomFields
Confidenceis formatted to two decimals (0.90)NeedsReviewis lowercasetrue/falseAssumptionsare joined with"; "CustomFieldsis a JSON string (empty if there are none)
Markdown — human review
An Obsidian-friendly report: header metadata (source, scope profile, generated timestamp, item counts with how many need review), a line-item table with confidence and review flags, then Assumptions, Scope notes, and Risks / RFI candidates sections, plus a warning line if any non-standard UOMs were found. This is the format to read before the CSV or JSON leaves the building.