1. Purpose
A Unit is a measurement label — the dimension in which an Item’s quantity, a Resource’s rate, a Recipe’s Output Unit, a Reference Rate, or a Worksheet Variable is expressed. Units are drawn from a managed library that admins can extend; every Item, Resource, Recipe Output, Reference Rate, and many Variables must reference a Unit.
oxFlow does not auto-convert between Units in v1 (BR-111). When a Resource’s Unit differs from the host Item’s Unit, the estimator performs the conversion explicitly inside the Worksheet (as a Variable or Calculation Block). Unit mismatches are flagged by Anomaly Review (BR-112) for review before Commercials.
Analogy (glossary cooking model): the Unit is the measure — cup, gram, pinch — what you’re counting in. Recipes don’t auto-convert from cups to grams; the cook does it.
2. Attributes
| Attribute | Type | Required | Default | Notes |
|---|---|---|---|---|
id | UUID | ✅ | generated | System-managed |
symbol | string (short text) | ✅ | — | Short display symbol — what appears in tables and rates (e.g., m, m², m³, hr, day, LS) |
display_name | string (text) | ✅ | — | Full human-readable name (e.g., “metre”, “square metre”, “cubic metre”, “hour”, “day”, “lump sum”) |
is_built_in | boolean | ✅ | false | True for the seeded standard set; false for admin-added custom units. Built-in units cannot be deleted |
category | enum | ❌ | null | Optional grouping for the Unit picker UI (e.g., Length / Area / Volume / Time / Mass / Count / Currency / Other) |
description | long text | ❌ | null | Admin notes — when to use this Unit, conversion guidance, regional convention |
created_at / updated_at | timestamp | ✅ | system | Audit |
created_by / updated_by | User ref | ✅ | system | Audit. created_by = system for built-ins; admin User for custom additions |
Derived attributes (computed, not stored) — see §8.
Note on conversions: Units do not carry conversion factors in v1. Auto-conversion is intentionally deferred (see §10 Open items). The category attribute is for UI grouping only — it does not enable cross-Unit math.
3. Built-in Unit set
The seeded standard set covers common construction-estimating Units. Admin can extend (BR-113) but cannot delete built-ins.
| Symbol | Display name | Category | Notes |
|---|---|---|---|
m | metre | Length | |
m² | square metre | Area | |
m³ | cubic metre | Volume | |
lm | linear metre | Length | Often used for piles, beams |
mm | millimetre | Length | Spec-detail; rarely used as Item Unit |
kg | kilogram | Mass | |
t | tonne | Mass | |
hr | hour | Time | |
day | day | Time | |
wk | week | Time | |
mth | month | Time | |
ea | each | Count | Generic count unit |
no | number | Count | Synonym for ea; both kept for estimator preference |
LS | lump sum | Currency-equivalent | Used for any non-quantity-driven Item or Resource (BR-110) |
km | kilometre | Length | Cartage / travel modifiers |
Notes:
LSis a normal Unit — nothing special structurally. It just means “no unit-rate math; the rate IS the total” (BR-110).eavsno— both are kept in the built-in library because Oxcon estimators historically use them interchangeably; merging would force migration of legacy data. See migration-benchmark.md §3.10 for mapping.
4. Lifecycle States
Units have no lifecycle state machine. Once created they persist; built-in Units cannot be deleted. Custom Units can be soft-deleted (archived) via admin — archived Units remain readable on historical entities that used them but are not selectable for new entries (mirrors Categorization soft-delete pattern, BR-120a).
5. Relationships
Inbound (things referring to Unit)
| From | Cardinality | Notes |
|---|---|---|
| Item | Item M:1 Unit | Required (BR-010) |
| Resource | Resource M:1 Unit | Required (BR-020) |
| Recipe | Recipe M:1 Unit (as output_unit) | Required (BR-030) |
| Input Parameter | Input Parameter M:1 Unit | Required (every Input Parameter has a Unit) |
| Variable | Variable M:0..1 Unit | Optional; estimator may declare a Unit on a Variable for clarity |
| Reference Rate | Reference Rate M:1 Unit | Required (implicit in the Reference Rate attribute list; BR-144 defines Reference Rates as admin-defined benchmarks, non-versioned) |
| Program Task | Program Task — implicit Unit (day for duration) | Not stored as FK; duration Unit is conventionally day |
Outbound (things Unit references)
Units have no outbound references — they are pure leaf entities in the reference-data layer.
6. Validation / Invariants
Rules that must hold at all times:
- Symbol unique. No two Units may share the same
symbol(case-sensitive). Enforced at write time. - Display name required.
display_namecannot be null or empty. - Symbol required.
symbolcannot be null or empty. - Built-in protection. Units with
is_built_in = truecannot be deleted, archived, or have their symbol/display_name changed via the admin UI. Description edits allowed. - Soft-delete only. Custom Units use archive (soft-delete); existing entities referencing them retain the reference. Hard delete is not supported.
- Cannot delete with references. Even custom Units cannot be hard-deleted while referenced by any Item, Resource, Recipe, or Reference Rate (archive is the only path).
7. Derived / Computed Attributes
| Attribute | Derivation | Notes |
|---|---|---|
usage_count | Count of Items + Resources + Recipes (output) + Reference Rates referencing this Unit | Informational; helps admins decide whether a custom Unit is in use |
is_archivable | is_built_in = false AND usage_count = 0 | Only archivable if custom and unused |
8. Worked Examples
Example A — Built-in Unit (m³)
A standard volume Unit used across the estimating model:
Unit:
symbol: "m³"
display_name: "cubic metre"
is_built_in: true
category: Volume
description: "Standard SI volume unit. Used for concrete, earthworks, bulk materials."
Usage:
- Item "Concrete pour — pile caps" (quantity: 18 m³)
- Resource "Concrete supply 32MPa" (rate: $230/m³)
- Recipe "Concrete Pump" output_unit: m³ (alternative — could be `day`)
- Reference Rate "Reinforced concrete works" (range: $450–550/m³)
Derived:
usage_count = (varies by workspace; typically high for built-in volume Units)
is_archivable = false (built-in)
Example B — Admin-added custom Unit
Admin adds a project-specific Unit for an unusual measurement:
Unit (admin-created):
symbol: "truckload"
display_name: "truckload (8m³ tipper)"
is_built_in: false
category: Volume
description: "Project-specific. Used for spoil-removal estimates on Bridge Project. Each truckload = ~8 m³ loose."
created_by: admin@oxcon.local
Usage:
- Item "Spoil cartage to off-site disposal" (quantity: 250 truckload)
- Resource "Tipper truck — 8m³" (rate: $180/truckload)
Anomaly Review:
No flag — both Item and Resource use the same Unit. Production rate logic in
Worksheet handles the conversion to volume if needed (manual per BR-111).
Derived:
usage_count = 2
is_archivable = false (in use)
Example C — Unit mismatch flagged by Anomaly Review (BR-112)
Item uses m² but its Worksheet contains a Resource priced in hr with no time-to-area bridging:
Item "External cladding — install":
unit: m²
quantity: 320
Worksheet:
Worksheet Resource: "Cladder day rate" (Resource Unit: hr, snapshot_rate: $85/hr)
quantity: ? ← estimator entered 220 (hours)
No Variable bridging hours → m² (no Production Rate, no Calculation Block
converting time-to-area).
Anomaly Review (BR-112 heuristic):
⚠️ "Unit mismatch: Item priced in m² but Worksheet contains only hr-based
Resources with no time-to-area conversion. Confirm conversion logic."
Estimator action:
Add Variable: install_rate = 1.45 m²/hr (manual Production Rate)
Add Calculation: implied_area = qty × install_rate = 220 × 1.45 = 319 m²
Cross-check vs Item quantity (320 m²) — within 0.3%, accepted.