> ## Documentation Index
> Fetch the complete documentation index at: https://help.stralos.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Linked work-item quantity

> The calculators package contract for linking a work item's quantity to calculator readings, takeoff measurements, and rate-item rows

A work item's quantity can be **linked** to the sum of one or more sources, optionally scaled by a multiplier and shifted by an offset. `@stralos-co-nz/calculators` defines the `QuantityLinkV1` descriptor, the unit rule, and `resolveQuantityLink`, the one resolver api-estimate runs to write the stored quantity and feature-estimate runs to preview it. Sharing one function is what stops the preview and the stored figure drifting apart.

> [!NOTE]
>
> api-estimate persists `QuantityLinkV1` descriptors. Work items, build-up lines, and calculator input fields can each carry a takeoff-driven quantity link. See [Takeoff-driven links](#takeoff-driven-links) below for the three link targets and their rules. The package supplies shared resolution and validation for both hosts.

## `QuantityLinkV1`

```ts theme={null}
import type { QuantityLinkV1 } from "@stralos-co-nz/calculators";

type QuantityLinkV1 = {
  version: 1;
  sources: QuantityLinkSource[]; // 1 to 20, no duplicates
  multiplier?: number;           // 0 < m <= 1000, up to 4 dp. Absent means 1.
  offset?: number;               // 0 <= o < 10^10, up to 3 dp. Absent means 0.
  rounding?: QuantityRoundingOption; // 2.1.0: "Round up to" after multiplier and offset. Absent or "none" means none.
};
```

Each source is one of three kinds:

| `kind` | Fields | What it reads |
| - | - | - |
| `calculator` | `calculatorId`, `layerId`, `outputKey` | One output of one row of a stored calculator. `layerId` is a real layer id (`S1`, `pit-excavation`) or a measurement id (`m:o:lengthM`, `m:l:<layerId>:areaM2`); see [Calculator measurements](https://help.stralos.app/internal/calculator-measurements.md). `outputKey` is one of `lengthM`, `areaM2`, `inSituM3`, `looseM3`, `tonnes`, `count`. |
| `rateItem` | `rateItemId` | The sum of this work item's whole-item build-up rows for that rate item. |
| `takeoff` | `measurementId`, optional `tag` | The `quantity` of a live takeoff measurement, or — when `tag` is a takeoff-tag library id — one tag's subtotal of a Multi-segment or tagged Multi-line. Absent or `""` reads the measurement's total. A tag the measurement no longer has resolves `missing`; a retired tag is not missing. |

Two sources are duplicates when they have the same kind and ids. Ids are compared case-insensitively; a calculator `layerId` is compared exactly.

## Validation

`quantityLinkIssues(link)` checks structure only and returns `QuantityLinkIssue[]` (`code`, optional source `index`, `message`). An empty array means the descriptor is acceptable. It treats its input as untrusted, so a host can run it on a stored value as well as on a new one. Whether the named calculators, measurements, and rate items exist, and whether units match, is the host's check at write time.

A host validates every link write through `quantityLinkTargetIssues(link, target)`, which calls `quantityLinkIssues` and then applies the target's own restrictions. `target` is `"workItem"`, `"component"` (build-up line), or `"calculatorInput"`. See [Link targets](#link-targets).

| Code | Meaning |
| - | - |
| `version` | `version` is not `1`. |
| `sources_empty` | No sources. |
| `sources_too_many` | More than 20 sources. |
| `source_kind` | A source is not an object, or its `kind` is unknown. |
| `source_id` | A required id is missing or blank. |
| `layer_id_too_long` | A calculator `layerId` is over 64 characters (the `estimate_calculator_links.layer_id` budget). |
| `output_key` | A calculator `outputKey` is not one of the six keys. |
| `source_duplicate` | The same source appears twice. |
| `tag_id` | A takeoff `tag` field is neither absent, an empty string (the total), nor a non-blank string (a tag id). |
| `target_source_kind` | A source kind this target does not accept (see [Link targets](#link-targets)). |
| `takeoff_overlap` | The same measurement's total and one of its tag subtotals both appear in the link. The total already contains every tag, so the pair double-counts. |
| `tag_unsupported` | Deprecated from calculators 1.14.0 and never raised: every target accepts tag subtotals. The code stays in `QuantityLinkIssueCode` so exhaustive host mappings still compile; it is due for removal at the next major version. |
| `multiplier` | The multiplier is out of range or has more than 4 decimal places. |
| `offset` | The offset is negative, out of range, or has more than 3 decimal places. An offset only adds (a lap or an allowance); a negative one is refused. |
| `rounding` | 2.1.0. The `rounding` is not one of `QUANTITY_ROUNDING_OPTIONS`. |

## Link targets

A host validates each write through `quantityLinkTargetIssues(link, target)`, which combines structural checks with these target restrictions:

| Target | Accepted sources |
| - | - |
| `workItem` | `calculator`, `rateItem`, and `takeoff`, including tag subtotals. |
| `component` | `calculator` and `takeoff`, including tag subtotals. Rate-item sources are refused. |
| `calculatorInput` | `takeoff` only, including tag subtotals (from calculators 1.14.0). Other kinds raise `target_source_kind`. |

## Unit rule

`canonicalUnit(unit)` is the comparison fold used on both sides. `ML` is matched case-sensitively as `megalitre` first. Otherwise the unit is trimmed, lower-cased, `²` and `³` become `2` and `3`, `^` and full stops are removed, and inner whitespace is collapsed. The result is looked up in an alias table covering the unit spellings used by api-estimate, feature-estimate, and the calculators package, so `lm`, `lin m`, and `lineal metre` fold to `metre`, and `sqm`, `sq m`, and `m2` fold to `square_metre`. An unrecognised unit is returned as the folded token, so `Bag` and `bag ` compare equal. It is a comparison fold only; it does not change what the hosts store or display.

`quantityDimension(unit)` gives a unit's dimension, and `outputKeyDimension(outputKey)` gives a calculator output's:

| Canonical unit | Dimension | Output keys |
| - | - | - |
| `metre` | `length` | `lengthM` |
| `square_metre` | `area` | `areaM2` |
| `cubic_metre` | `volume` | `inSituM3`, `looseM3` |
| `tonne` | `mass` | `tonnes` |
| `each`, `number` | `count` | `count` |

Every other unit, including `kilometre`, `millimetre`, `hectare`, `kilogram`, `lot`, `item`, `lump_sum`, and `hour`, has no dimension and matches nothing, not even another dimensionless unit. Readings are in metres, square metres, cubic metres, and tonnes, so adding them to a kilometre item unconverted would be wrong by a factor of 1000.

* A **calculator** or **takeoff** source must have the same dimension as the work item's unit.
* A **rate-item** source counts only rows whose canonical unit equals the work item's canonical unit.

## Rate-item rows

A rate-item source counts only **whole-item** rows: rows whose stored quantity is the job total. A row priced per unit of the work item consumes the linked quantity, so counting it would make the resolver's input depend on its own output. Rows of other rate items are ignored. Rows of the named rate item that are not counted are returned in `excluded` with a reason:

| Reason | Condition |
| - | - |
| `in_activity` | The row has a `parentComponentId`. |
| `container` | `type` is `"activity"` and `rateItemId` is null. |
| `per_unit` | `quantityBasis` is `"per_unit"`. |
| `recipe` | The row has both `rateItemId` and `activityId` and no parent (a copied activity child whose quantity is still per unit; the picker reads **From an activity**). |
| `unit` | The row's canonical unit differs from the work item's. |
| `counted_via_calculator` | The row is fed by a calculator layer that a `calculator` source in the same descriptor already counts. A layer-scoped measurement (`m:l:<layerId>:…`) counts as its layer. `viaIndex` names that source. |

The first four reasons are exported as `wholeItemExclusion(row)` and `isWholeItemPricedRow(row)`, which must stay in step with api-estimate's and feature-estimate's whole-item rules.

## Resolver

```ts theme={null}
import { resolveQuantityLink } from "@stralos-co-nz/calculators";

const resolution = resolveQuantityLink(link, input);
```

`resolveQuantityLink` is pure and never throws. It is meant to run over stored descriptors during a whole-estimate save, so an invalid descriptor resolves stale with an `invalid` warning (and no source results) instead of failing an unrelated save.

### `QuantityLinkInput`

| Field | Type | Notes |
| - | - | - |
| `workItemUnit` | `string` | The work item's unit as stored. |
| `calculators` | `ReadonlyMap<string, QuantityLinkCalculatorInput \| "deleted">` | Keyed by calculator id. Each entry has `name`, `rows` (the engine's `CalculatorRowsResult`, run as the host's save runs it, with register allocations applied), and optional `type` (`"trench"`, `"pit"`, or `"volume"`) for a more specific unstated reason. |
| `takeoffs` | `ReadonlyMap<string, QuantityLinkTakeoffInput \| "deleted">` | Keyed by measurement id: live measurements on live sheets in this estimate, each with `label`, `quantity`, `unit`, and optional `byTag`. `byTag` is keyed by lower-cased tag UUID and carries each tag's `label` and `quantity`; it is present for Multi-segment (1.9.0) and tagged Multi-line (1.10.0), and absent otherwise. Build it with `takeoffTagQuantities` from `@stralos-co-nz/calculators`. |
| `components` | `readonly QuantityLinkComponentRow[]` | This work item's live build-up rows, including `calculatorLayer` (the calculator layer feeding the row, or `null`). |
| `rateItemNames` | `ReadonlyMap<string, string>` (optional) | Names for source labels, such as `AC14 (3 rows)`. A missing name reads `Rate item`. |

`QuantityLinkTakeoffInput.byTag` is a `ReadonlyMap<string, { label: string; quantity: number }>` keyed by lower-cased tag id. It is required for a Multi-segment's tag sources to resolve; any tag source on a measurement without `byTag` resolves `missing`. Build it from `takeoffTagQuantities(parts, breakdown)` (see [Takeoff parts](https://help.stralos.app/internal/takeoff-parts.md)). Retired tags remain in `byTag` (a retired tag is not missing, per Q12).

### `QuantityLinkResolution`

| Field | Notes |
| - | - |
| `state` | `"ok"` or `"stale"`. |
| `value` | The linked quantity at 3 dp, rounded up when the link carries a `rounding`. `null` when stale. When stale, a host is expected to keep the last stored quantity rather than write `null`. |
| `unrounded` | 2.1.0. `Σ × multiplier + offset` at 3 dp, before the rounding. Present only on an `ok` resolution of a link whose `rounding` is set and not `"none"`, so a link without rounding resolves to exactly its pre-2.1.0 shape. |
| `sum` | The source total at 3 dp before multiplier and offset. `null` unless every source resolved. |
| `sources` | One `QuantityLinkSourceResult` per source, in descriptor order, with `label`, `value`, `state`, a `reason` when not `ok`, an optional `cause` when not `ok` (see below), and a rate-item source's counted `rows`. |
| `excluded` | Rate-item rows not counted, with their reason. |
| `warnings` | Diagnostics (below). |

Output order is deterministic (sources in descriptor order, rows in `components` order), so a host can compare a new resolution with the stored one and write only on change.

### Rules

1. **Stale, never partial.** If any source is not `ok`, the whole link is stale: `value` and `sum` are `null`. Summing only the sources that resolved would silently under-count.
2. **Whole-item rows only.** See [Rate-item rows](#rate-item-rows).
3. **One rounding.** The sum (3 dp), multiplier (4 dp), and offset (3 dp) are held as scaled integers. The result is computed at 7 dp and rounded to 3 dp once, half up. For example, `571.650 × 1.05 = 600.2325`, which resolves to `600.233`.
4. **Round up last.** A link with a `rounding` rounds the rule 3 result up to its step. See [Round up to](#round-up-to).

### Source states

| State | When |
| - | - |
| `ok` | The source resolved to a value. |
| `missing` | The calculator or takeoff measurement is deleted or absent, or the calculator no longer has the named reading. |
| `unstated` | The calculator refuses its rows (an excavation split over more than 100%), the reading is a partitioned stratum with one row per destination, the calculator cannot state the requested output, or a figure is not a usable number. |
| `no_rows` | A rate-item source has no counted rows. Zero rows is stale, not zero, so removing the last priced row cannot silently zero the item. |
| `unit` | A calculator or takeoff source is in a different dimension from the work item's unit. |

### Stale causes (api-estimate)

When api-estimate stores a `quantity_link_result` or builds a 422 response, it adds a `cause` field to every non-ok source that gives a finer machine code than `state`. The `reason` string that accompanies it uses display units (for example, `m²` rather than `square_metre`) so the message is ready to show beside the source in the UI.

| `cause` | When |
| - | - |
| `calculator_deleted` | The calculator no longer exists. |
| `reading_removed` | The calculator exists but no longer has the named reading (layer). |
| `reading_unstated` | The reading exists but cannot state the requested output (e.g. a stratum with no density for a tonnage output). |
| `partition_refused` | The calculator's excavation is split over more than 100%, so it states no figures. |
| `layer_partitioned` | The reading is split across multiple destinations and states no single figure. |
| `measurement_deleted` | The takeoff measurement was deleted. |
| `tag_removed` | The measurement exists but the tag is no longer on it. |
| `no_rows_in_unit` | A rate-item source has no rows in the work item's unit. |
| `unit_mismatch` | A calculator or takeoff source is in a different dimension from the work item's unit. |
| `unusable_number` | The source resolved structurally but did not produce a usable number. |

### Warnings

| `kind` | When |
| - | - |
| `stacked_layers` | Counted rate-item rows come from two or more layers of one calculator. This is a warning only, because two courses on one surface can legitimately double the area. Carries `calculatorId` and `layerIds`. |
| `negative` | `sum × multiplier + offset` is below zero. A valid descriptor cannot reach this now that a negative offset is refused. The link is stale, but `sum` is still returned. |
| `too_large` | The result reaches `QUANTITY_LINK_QUANTITY_LIMIT` (10^10). The link is stale, but `sum` is still returned. |
| `invalid` | The descriptor fails `quantityLinkIssues`. Carries `issues`. |

## api-estimate behaviour

### Write-time 422 responses

When a link is written and a source fails the write-time check, the 422 body includes `sources[]`. Each entry has `index`, `label`, `state`, and, when api-estimate has assigned one, `cause` and `reason`. The `reason` string always speaks display units — `m²` not `square_metre` — so the client can show it directly beside the failing source.

The two link-specific 422 codes are:

| Code | Condition |
| - | - |
| `quantity_link_unit` | At least one source is in a different dimension from the target's unit. The message names the source and both units, e.g. "Road edge is in m, this item is m²." |
| `quantity_link_source_unresolvable` | At least one source cannot be read right now (deleted, missing reading, no rows, etc.). The message names the source, e.g. "No AC14 rows in m² remain." or "Trench B was deleted." |

### Calculator-owned row units

A build-up row fed by a project calculator (its `generatedKey` starts with `calc:`) has its unit set and reset by the calculator on every save. Changing the unit on such a row via `PUT /work-item-components/:id` or via the pricing-rule unit move returns:

* **Status**: `422 Unprocessable Entity`
* **Code**: `component_unit_calculator_owned`
* **Message**: names the calculator and the layer that owns the unit, e.g. "The unit of this row is set by Trench A · Layer 2. Choose another output in the calculator, or unlink the row from the calculator to set its unit yourself."
* **Body**: `{ origin: { calculatorId, calculatorName, layerId, layerLabel } }`

An unchanged unit (echo) or a canonical respelling of the same unit (e.g. `m3` → `cubic_metre`) passes without error. Use `POST /work-item-components/:id/unlink-calculator` to detach the row from the calculator and take ownership of its unit.

### Quantity-link candidates: row `origin`

`GET /estimates/:estimateId/work-items/:itemId/quantity-link/candidates` returns `rateItems[]`. Each `eligible` and `excluded` row now carries an `origin` field that describes where the row's quantity comes from:

| `kind` | Fields | Meaning |
| - | - | - |
| `calculator` | `calculatorId`, `calculatorName`, `layerId`, `layerLabel` | The row is generated by a project calculator, or its own quantity link names a calculator as its first source. |
| `takeoff` | `measurementId`, `measurementLabel` | The row has its own quantity link to a takeoff measurement (the first takeoff source). |
| `manual` | None | No calculator or takeoff origin could be resolved. This includes typed or activity-derived quantities and linked rows whose source lookup failed. |

## Round up to

From 2.1.0, `QuantityLinkV1.rounding` takes a `QuantityRoundingOption` (`QUANTITY_ROUNDING_OPTIONS`). The step is applied to `Σ × multiplier + offset`, always up; a value already on a step, zero, and a negative are unchanged. Absent or `"none"` resolves exactly as a pre-2.1.0 link.

When `quantityLinkRuns` splits a rounded link into per-run figures, the rounding uplift (`value − unrounded`) is added to the last run, so the runs still sum to the rounded quantity.

See [Estimate rounding](https://help.stralos.app/internal/estimate-rounding.md) for the option list, labels, `suggestQuantityRounding`, and how api-estimate applies it.

## Worked examples

The specification's worked examples are published on a separate subpath so fixture data never reaches a consumer bundle through the main entry point:

```ts theme={null}
import { quantityLinkWorkedExamples } from "@stralos-co-nz/calculators/quantity-link-examples";
```

Each call returns fresh `QuantityLinkWorkedExample` objects (`id`, `title`, `link`, `input`, `expected`) with new maps and arrays. Every `expected` value was calculated by hand from the specification, not captured from the resolver, so hosts can use them as parity tests. From 2.1.0 the set includes four rounding cases: `round-1`, `round-ends-in`, `round-after-multiplier`, and `round-on-step`.

## Constants

| Export | Value | Meaning |
| - | - | - |
| `QUANTITY_LINK_VERSION` | `1` | The only accepted version. |
| `QUANTITY_LINK_MAX_SOURCES` | `20` | Maximum sources per descriptor. |
| `QUANTITY_LINK_MULTIPLIER_MAX` | `1000` | Upper bound for the multiplier. |
| `QUANTITY_LINK_MULTIPLIER_SCALE` | `4` | Decimal places allowed on the multiplier. |
| `QUANTITY_LINK_OFFSET_SCALE` | `3` | Decimal places allowed on the offset. |
| `QUANTITY_LINK_QUANTITY_SCALE` | `3` | Decimal places for every source value, the sum, and the result. |
| `QUANTITY_LINK_QUANTITY_LIMIT` | `10_000_000_000` | Exclusive bound on the offset's magnitude and on the result. It matches the pricing package's per-operand ceiling. |
| `QUANTITY_LINK_LAYER_ID_MAX` | `64` | Maximum calculator `layerId` length. |

## Build-up line quantity links

A work item component (build-up line) can carry its own `quantity_link` on `estimate_work_item_components`. The descriptor is the same `QuantityLinkV1` shape, but the allowed source kinds are restricted:

* `calculator` and `takeoff` sources are accepted.
* `rateItem` sources are not accepted. A line reading a rate item's rows could end up reading itself, so that kind is excluded entirely.

### Eligibility

Not every build-up line can carry a link. A line is eligible when it meets both conditions:

* It is a whole-item priced row (`isWholeItemPricedRow`: not per-unit, not in an activity, not a copied activity child, not a container). A per-unit row consumes the work item's quantity, so feeding it a total would double-count.
* Its `generatedKey` is `null`. A calculator-generated or activity-generated row is rewritten by its owner on every save; the owner would overwrite the linked value.

`GET /estimates/:estimateId/components/:componentId/quantity-link/candidates` returns:

| Field | Description |
| - | - |
| `unit` | The line's stored unit. |
| `dimension` | The dimension of the unit (`length`, `area`, `volume`, `mass`, `count`), or `null` for dimensionless units. |
| `eligible` | Whether this line can carry a link. |
| `reason` | Ineligibility reason when `eligible` is `false`: `generated`, `per_unit`, `in_activity`, `recipe`, or `container`. |
| `calculators` | Calculator readings in the line's dimension, in the same shape as work-item candidates. Empty when `dimension` is `null`. |
| `takeoffs` | Takeoff measurements in the line's dimension, including multi-segment tag subtotals. Empty when `dimension` is `null`. |

### Resolution timing

Build-up line links are resolved at the head of every `recomputeEstimateTotals` run, before the work-item pre-pass reads the lines. This means a calculator save, reconcile, or delete moves the line's quantity in the same request — not only when takeoff is written.

Frozen estimates never re-resolve (SPEC D14). A stale source keeps the last stored quantity rather than zeroing it.

### Calculator deletion and source-removing saves

When a calculator is deleted with `?unlink=true`, or when `PUT /calculators/:id` would stop emitting a layer that a build-up line's link names, the response includes `unlinkedComponents` alongside `unlinked`:

```json theme={null}
{
  "unlinkedComponents": [
    {
      "componentId": "<uuid>",
      "quantity": 12.5,
      "label": "Item 2.1 Pipe bedding · Excavation"
    }
  ]
}
```

Each entry records the component that was unlinked and the quantity it kept.

The `409 calculator_in_use` and `409 calculator_source_removed` usage bodies also include a `components` array for build-up lines that name the calculator, alongside the existing `workItems` array.

### `linkedUseCount` on the calculator response

The `linkedUseCount` field returned by `GET /estimates/:id/calculators` counts how many live work items and build-up lines have a `quantity_link` naming the calculator. A work item or build-up line that reads several of the calculator's readings still counts as one. This count drives the "ƒ n uses" indicator in the Calculations list.

## Takeoff-driven links

Three targets in an estimate can carry a takeoff-driven link. All three are updated automatically by the takeoff-driven pass, which runs before estimate totals are recomputed. Frozen estimates are never re-resolved.

### Work-item quantity link

`estimate_work_items.quantity_link` stores a `QuantityLinkV1` descriptor. The server resolves the linked quantity and writes it into the item's `quantity` field. The takeoff source may be a measurement total or a Multi-segment or tagged Multi-line subtotal.

`PUT /estimates/:id/work-items/:itemId` accepts `quantityLink` (a `QuantityLinkV1` or `null`). Passing `null` unlinks and keeps the current quantity. A typed `quantity` that differs from the stored linked quantity is `422 quantity_linked` unless it equals the stored value (equal resend passes).

### Build-up line quantity links

Build-up lines accept calculator readings and takeoff totals or tag subtotals. See [Build-up line quantity links](#build-up-line-quantity-links) for eligibility and candidate endpoints.

### Calculator input links

`estimate_calculators.input_links` stores a `CalculatorInputLinksV1` (`{v:1, fields}`) where each field maps to a `QuantityLinkV1` of **takeoff sources only**: a measurement total, or one or more tag subtotals of a Multi-segment or tagged Multi-line. Tag subtotals are in metres, so only a length field can take one. Supported fields:

| Field | Calculator type | Config key written | Tag subtotals |
| - | - | - | - |
| `trench.length` | Trench | `length` | Yes |
| `pit.lengthM` | Pit | `lengthM` | Yes |
| `volume.lengthM` | Volume (L × W × D mode) | `lengthM` | Yes |
| `volume.areaM2` | Volume (Area × D mode) | `areaM2` | No |

`PUT /estimates/:estimateId/calculators/:id` accepts `inputLinks`. The resolved value is written directly into `config` so the engine and reconcile run unchanged. Errors:

* `422 calculator_input_link_kind` — a non-takeoff source kind.
* `422 quantity_link_unit`: a source in another dimension from the field, for example a tag subtotal on `volume.areaM2`.
* `422 quantity_link_takeoff_overlap`: a measurement's total and one of its own tag subtotals appear in the same field's link.
* `422 calculator_input_field_inapplicable` — the field does not apply to the calculator's type or configured mode.
* `422 calculator_input_linked` — a typed config value differs from the linked field's resolved value.
* `409 calculator_source_removed` — a config change (for example, a volume mode switch) would orphan a linked field. Pass `?unlink=true` to unlink that field and keep its value.

Candidates: `GET /estimates/:estimateId/calculators/:calculatorId/input-link/candidates?field=<field>` returns `field`, `unit`, `applies`, and `takeoffs`. `takeoffs` lists live measurements in the field's dimension, each with its `byTag` tag subtotals where it has them, so an area field never sees a tag subtotal.

### Takeoff usage

`GET /estimates/:estimateId/takeoff-measurements/usage` returns every live measurement's total and each of its tags, with all three user types listed under each source. Works on frozen estimates.

When a measurement PUT would remove a linked source, the server returns `409 takeoff_source_removed` with `usage` and `removed: {total, tags}` before writing anything. Pass `?unlink=true` to apply the edit and clear affected links atomically. From calculators 1.16.0, only a tag loses its source, when its last span or line is deleted or retagged, so `removed.total` is always `false`. Deleting a span or line that leaves its tag elsewhere, a move, an insert, or a non-last retag re-resolves every linked quantity instead (work items, build-up lines, and calculator inputs).

`DELETE /estimates/:estimateId/takeoff-measurements/:id` and `DELETE /estimates/:estimateId/takeoff-map-sheets/:sheetId` both return `409 measurement_in_use` when any work item, build-up line, or calculator field links to the measurement, listing `workItems[]`, `components[]`, and `calculatorFields[]`. Pass `?unlink=true` to unlink every user and complete the delete in one request. The 200 response includes `unlinkedComponents` and `unlinkedCalculatorFields` alongside the existing `unlinkedWorkItems`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.