The type system
Meridian's engine ships with no clinical types. Instead it ships a language for describing types — a small meta type-system — and three open registries that plugins fill at startup. Everything downstream (connection validity, runtime checking, editor tooltips) is derived from those descriptors. This is the same "empty core" principle that governs the rest of the platform (see Architecture).
This document explains the model and why it is shaped this way. For the exhaustive variant table, see the TypeRef reference; for the registry APIs, see Type-system registries.
TypeRef: describing a shape
Every port and every field declares its shape with a recursive TypeRef
(libs/shared/src/domain/types.ts). There are
eight variants:
primitive— a scalar (Decimal,String,Integer,Boolean,DateTime).coded— a code bound to a terminology (a value-set).quantity— a dimension-aware measurement, unit-checked against UCUM.object— a structured datatype or resource from the registry.record— a workflow-local structural object with inline field descriptors.ref— a reference to an entity.list— a homogeneous collection.any— a flow port whose type is inferred from what it is wired to.
The point of a closed set of variants over an open set of registered vocabularies:
the engine understands the eight ways a shape can be composed, while knowing nothing
about which concrete objects, code systems, or dimensions exist. A plugin adds
Observation or renal-stage; the engine already knows how to compare, validate, and
display anything expressed with them.
Three open registries
The vocabulary lives in three registries, all empty at boot:
OBJECT_TYPES— descriptors forobjecttypes (datatypes, clinical resources), filled byregisterType.VALUE_SETS— terminologies backingcodedtypes, filled byregisterValueSet. A value-set may beexternal: true: its concepts are not enumerated but resolved by a terminology server through theterminologyport.DIMENSIONS— physical dimensions backingquantitytypes (a canonical unit plus conversion factors), filled byregisterDimension. The common physical dimensions (mass, volume, time, concentrations…) are contributed by@posos/common.
A plugin populates these during loading, in dependency order, so a value-set or type is always registered before a node that references it.
Event payloads use the same vocabulary. A plugin declares the fields of an event with
TypeRefs in contributes.events; they must resolve in the plugin's dependsOn closure,
and the declaration and its example are checked when the plugin loads. An event received
by POST /api/events is checked against its contract according to the instance's
EVENT_CONTRACTS mode (Event ingestion API). See
Plugin manifest.
What the engine derives
From these descriptors alone, three capabilities fall out for free — no per-type code:
- Structural compatibility of connections — subtyping by fields, by value-set, and
by dimension (
type-system.ts). This is what lets the editor reject an incompatible wire before a run (see Two levels of validation). - Runtime value validation at every port boundary.
- Field metadata exported as JSON for the UX (tooltips, dropdowns, the palette).
Dates carry a checked format
Most string primitives (String, Code, Uri, Time, Duration) are checked only
for being strings. Date and DateTime also have their format checked, because a date
is the one scalar that systems downstream parse rather than display: a FHIR server
refuses a Communication.sent of "" or 20260611083000, and flow.wait computes its
resume instant from the value. Checking the format where the value is produced turns
that late refusal into a failure on the node that produced the bad value.
The rules follow what the values are used for:
DateTimeis the FHIRdateTime. FHIR adapters read and write these values asdateTimeelements. The grammar accepts the reduced precisionsYYYY,YYYY-MMandYYYY-MM-DD: a condition onset known only by year is legitimate clinical data, and refusing it would fail the run that reads it. A time must carry seconds and an offset (Zor±hh:mm), since a time without an offset means a different instant on each server.Dateis a full calendar day,YYYY-MM-DD. ADateport therefore never receives a partial date or a time.Date.parseis not the rule. It accepts free text such asMay 11, 2026, reads01/05/2026as January 5, and moves February 30 to March. The grammar is checked first; every accepted value remains readable byDate.parse.
An unknown date is expressed by absence, not by an empty string: a producer omits the port, and absence propagation skips whatever needed the date. The TypeRef reference lists the accepted and refused forms.
No clinical TypeScript in the core
The core hosts no clinical TypeScript interface. A plugin that wants local static typing
for its own behavior code generates it from its manifest (import type declarations,
erased at runtime — e.g. external-plugins/demo/generated/types.ts). The result is zero
coupling: the engine depends on the descriptors, never on a plugin's compiled types.
See Codegen internals for how
that projection works.