Background
Computed fields on resources — values derived at read time from other fields (e.g. isComplete, hasData) — are currently not expressible in the OpenAPI schema. The composition system added a derive: mechanism that declared and evaluated these per composition config, but that design was rejected: a computed property belongs on the resource itself, not on each composition that happens to assemble it.
The decision (see docs/architecture/cross-cutting/resource-composition.md Decision 7) is to move derived fields to resource schemas via an x-derived OpenAPI extension, making them available on the resource's own endpoints and naturally filterable via ?q= like any other field.
A working implementation of derived field computation (CEL expression evaluation, item-scope vs. collection-scope inference) exists in the composition assembler on commit 25bbcdeb of feat/declarative-resource-composition — the CEL evaluation logic is reusable; what changes is where the declarations live.
Design
x-derived is an extension on an OpenAPI schema property that declares a CEL expression to compute the field's value at read time:
components:
schemas:
ApplicationMember:
properties:
isComplete:
type: boolean
readOnly: true
x-derived: "has(firstName) && has(lastName) && has(dateOfBirth)"
The expression is evaluated in the context of the resource record. Scope follows the same pattern as composition derives: expressions that reference items are collection-scope (evaluated against the full result set); all others are item-scope (evaluated per record).
Reusable expressions
Common expressions (e.g. collection-completeness checks that apply to many resources) are declared once in packages/contracts/derives.yaml and referenced via $ref:
# derives.yaml
isComplete:
collection: "items.all(i, has(i.completedAt))"
item: "has(completedAt)"
# a resource schema
properties:
isComplete:
type: boolean
readOnly: true
x-derived:
$ref: './derives.yaml#/isComplete/collection'
This mirrors the derives: pattern used in the composition assembler, making expressions portable across resource schemas without duplication.
Known fields blocked on this feature
Task.slaDeadline
The workflow state machine references $object.slaDeadline in two places:
- Change detection:
$this.data.changes.exists(c, c.field == "slaDeadline") && $object.slaDeadline != null — triggers SLA timer scheduling when the deadline changes
- Timer
fireAt: fireAt: $object.slaDeadline (×2 — warning at -48h, breach at 0h)
Task.slaInfo is an array of SlaInfo entries (each with slaTypeCode, status, clockStartedAt, deadline). slaDeadline is the earliest active deadline across all entries — a scalar computed from the array.
Task.slaDeadline has been added to the schema as an engine-managed readOnly field (the engine computes and maintains it whenever slaInfo is updated). It is a candidate for x-derived when that feature lands — at that point the engine-managed declaration can be replaced with a CEL expression:
x-derived: "slaInfo.filter(e, e.status in ['active', 'warning']).map(e, e.deadline).min()"
Phases
- Design the
x-derived extension schema and document it in the x-extensions architecture doc; define the derives.yaml structure for reusable named expressions
- Add
x-derived support to the mock server resource handlers
- Add
x-derived validation to the resolve pipeline (validate CEL syntax, warn on unknown field references, resolve $ref expressions from derives.yaml)
- Add
x-derived fields to resource schemas where currently needed (starting with ApplicationMember)
- Update the TypeScript client generator to mark
x-derived fields as readonly computed properties
Validation
GET /applications/{id}/members returns computed fields declared with x-derived
?q=isComplete:false filters by the derived field value
- A
$ref-based expression in derives.yaml resolves and evaluates correctly
npm run validate catches malformed CEL expressions in x-derived
- Derived fields are marked
readOnly in the generated TypeScript client
Background
Computed fields on resources — values derived at read time from other fields (e.g.
isComplete,hasData) — are currently not expressible in the OpenAPI schema. The composition system added aderive:mechanism that declared and evaluated these per composition config, but that design was rejected: a computed property belongs on the resource itself, not on each composition that happens to assemble it.The decision (see
docs/architecture/cross-cutting/resource-composition.mdDecision 7) is to move derived fields to resource schemas via anx-derivedOpenAPI extension, making them available on the resource's own endpoints and naturally filterable via?q=like any other field.A working implementation of derived field computation (CEL expression evaluation, item-scope vs. collection-scope inference) exists in the composition assembler on commit
25bbcdeboffeat/declarative-resource-composition— the CEL evaluation logic is reusable; what changes is where the declarations live.Design
x-derivedis an extension on an OpenAPI schema property that declares a CEL expression to compute the field's value at read time:The expression is evaluated in the context of the resource record. Scope follows the same pattern as composition derives: expressions that reference
itemsare collection-scope (evaluated against the full result set); all others are item-scope (evaluated per record).Reusable expressions
Common expressions (e.g. collection-completeness checks that apply to many resources) are declared once in
packages/contracts/derives.yamland referenced via$ref:This mirrors the
derives:pattern used in the composition assembler, making expressions portable across resource schemas without duplication.Known fields blocked on this feature
Task.slaDeadlineThe workflow state machine references
$object.slaDeadlinein two places:$this.data.changes.exists(c, c.field == "slaDeadline") && $object.slaDeadline != null— triggers SLA timer scheduling when the deadline changesfireAt:fireAt: $object.slaDeadline(×2 — warning at -48h, breach at 0h)Task.slaInfois an array ofSlaInfoentries (each withslaTypeCode,status,clockStartedAt,deadline).slaDeadlineis the earliest active deadline across all entries — a scalar computed from the array.Task.slaDeadlinehas been added to the schema as an engine-managedreadOnlyfield (the engine computes and maintains it wheneverslaInfois updated). It is a candidate forx-derivedwhen that feature lands — at that point the engine-managed declaration can be replaced with a CEL expression:Phases
x-derivedextension schema and document it in the x-extensions architecture doc; define thederives.yamlstructure for reusable named expressionsx-derivedsupport to the mock server resource handlersx-derivedvalidation to the resolve pipeline (validate CEL syntax, warn on unknown field references, resolve$refexpressions fromderives.yaml)x-derivedfields to resource schemas where currently needed (starting withApplicationMember)x-derivedfields asreadonlycomputed propertiesValidation
GET /applications/{id}/membersreturns computed fields declared withx-derived?q=isComplete:falsefilters by the derived field value$ref-based expression inderives.yamlresolves and evaluates correctlynpm run validatecatches malformed CEL expressions inx-derivedreadOnlyin the generated TypeScript client