Add value-sets, dimensions, types and conversions
Before a node can declare a coded, quantity or object port, the terminology,
dimension or datatype it points at has to exist. This recipe walks the four
vocabulary contribution points that back those ports — value-sets, dimensions,
types and conversions — using one consistent example: an estimated GFR result.
Everything below lives under contributes: in your plugin.yaml. For the full field
list and semantics of every section see the plugin manifest reference;
for the shape of the type: blocks see the TypeRef reference.
If you don't have a plugin skeleton yet, scaffold one first.
Before you start
The host applies contributions in a fixed order: value-sets → context kinds → dimensions → types → conversions → events → nodes → adapters → agents → event sources → message bindings → event bindings → port sandboxes. So a value-set or dimension is always registered before the type that uses it, and a type before the declared event, node or conversion that references it. Author each section in the same order and forward references resolve on their own.
A malformed manifest is logged and skipped. A node whose port references an unknown
object type, entity or context kind, or a dimension outside the plugin's dependsOn
closure, is logged and skipped too; the rest of the plugin still loads, so keep the
identifiers below in sync. Value-set names on a node's ports are not checked at load: an
unknown value-set accepts any code. A declared event is checked at load: its context kinds,
and the object types, entities, value-sets and dimensions of its fields, must resolve in
the closure.
Add a value-set
A value-set is the terminology behind a coded type. Enumerate its concepts, or mark it
external: true when a terminology server resolves the codes at runtime.
contributes:
valueSets:
- id: renal-stage
system: urn:acme:renal-stage
doc: KDIGO chronic-kidney-disease stage.
concepts:
- { code: G1, display: "G1 — normal or high (≥ 90)" }
- { code: G2, display: "G2 — mildly decreased" }
- { code: G3, display: "G3 — moderately decreased" }
- { code: G3a, display: "G3a", parent: G3 } # optional hierarchy
idis the identifier a{ kind: coded, valueSet: <id> }type points at.systemis the code-system URI.docis required;concepts[].parentis optional and only records hierarchy.- Set
external: trueand leaveconcepts: []for a server-resolved terminology (SNOMED CT, LOINC, …). An enumerated value-set codegens to a literal union of its codes; an external one falls back tostring.
For the ops side — pointing an instance at a terminology server — see Configure terminology.
Add a dimension
A dimension gives quantity types their UCUM unit checking: a canonical unit plus the
factor from each accepted unit to it.
contributes:
dimensions:
- name: filtration-rate
label: Glomerular filtration rate
canonical: "mL/min/{1.73_m2}"
units:
- { symbol: "mL/min/{1.73_m2}", toCanonical: 1 }
nameis the identifier a{ kind: quantity, dimension: <name> }type points at.canonicalis the reference unit; everyunits[].toCanonicalis the multiplier that converts that unit's values into the canonical one (the canonical unit itself is1).- List every unit you want accepted on ports of this dimension.
- A factor is multiplicative: a unit that needs an offset to reach the canonical unit (a
temperature in
°Fnext toCel) cannot be declared.
The core declares no dimension. @posos/common contributes the common physical ones
(dimensionless, fraction, mass, volume, time, amount-of-substance,
mass-concentration, molar-concentration, pressure, temperature): to type a port
with one of them, declare @posos/common in your plugin's dependsOn rather than
contributing the dimension again: a dimension already registered cannot be redefined. A
dimension is resolved in the dependsOn closure, whatever the load order: without the
contributing plugin there, the node or the event that uses it is dropped at load
(« dimension « mass » inconnue dans la fermeture dependsOn. Déclarer la dimension ou ajouter à dependsOn le plugin qui la contribue. »).
Add a type
A type is an explicit descriptor: each field carries a TypeRef, a required flag and an
optional doc. layer is datatype (reusable building blocks) or resource (clinical
resources). This is where the value-set and dimension above get consumed.
contributes:
types:
- name: EgfrResult
doc: Estimated GFR (value + equation + stage).
layer: resource
fields:
value: { type: { kind: quantity, dimension: filtration-rate }, required: true }
equation: { type: { kind: primitive, name: String }, required: true }
stage: { type: { kind: coded, valueSet: renal-stage }, required: true }
nameis what an{ kind: object, name: <name> }port references.docis required on the type;fields.<field>.docis optional.- A type owned by a
dependsOnplugin is referenced by name and resolved across the dependency closure — codegen imports it from the owning package, you don't redeclare it.
See the TypeRef reference for every kind (primitive,
coded, quantity, object, ref, list, any), and
the type system for why datatypes and
resources are layered this way.
Add a conversion
A registered conversion powers the transform.convert node (and the editor's auto-insert
when two wired ports need bridging). from/to are type names — a registered type or a
primitive — and expr is a JEXL expression evaluated
over value, the input.
contributes:
conversions:
- from: EgfrResult
to: Decimal
label: eGFR → number
expr: value.value.value
Here the input is an EgfrResult, so value.value is its quantity field and
value.value.value is the numeric measure. Register one conversion per from/to pair
you want the platform to offer.
expr can also read now, the instant of the conversion (ISO 8601 UTC), to date the
value it builds. For the size of a list, write list|length: list.length reads the
list's first element. The
@posos/clinical conversion
uses both.
Verify
Validate the manifest — this catches an unresolved value-set/dimension/type reference before you ship:
meridian-plugin validateRegenerate the typed client so
EgfrResult, therenal-stagecode union and the rest surface in TypeScript — see Generate types:meridian-plugin generateBuild and bundle with
meridian-plugin build(esbuild), then publish to the registry when ready.
Next steps
- Reference the new types from typed ports in Implement a node behavior.
- Localize the labels and docs you added in Translate a plugin.
- Map these types onto FHIR resources: FHIR mapping (
apps/ehr-lab/docs/resources/reference/fhir-mapping.md). - Background on how the registries fit together: type system registries and the plugins concept.