Plugin manifest
A plugin is a versioned bundle that contributes to a fixed set of extension points:
value-sets, dimensions, context kinds, types, conversions, declared events, nodes,
agents, adapters, event sources, event bindings, message bindings and port sandboxes. Its plugin.yaml is validated by the zod schema in
libs/shared/src/plugin/manifest.ts.
This page documents that schema field by field; it is the authoritative source for the
manifest grammar. It does not cover authoring workflows — see the how-to guides linked
from each section.
The manifest is browser-safe grammar only. The loading logic (filesystem discovery,
dynamic import) lives in the API host (PluginHost); the manifest schema defines
structure and validation, nothing else.
Top-level fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
apiVersion |
literal meridian/plugin-v1 |
yes | — | The only accepted value. |
name |
string | yes | — | Unique package-style identifier (e.g. @scope/name). |
version |
string | yes | — | Plugin version. Immutable once published to a registry. |
description |
string | no | — | Human description. |
sdk |
string | no | — | Required range of the plugin contract, e.g. ^0.1.1. Verified at load against PLUGIN_CONTRACT_VERSION (currently 0.1.8) using the npm semver package; an incompatible plugin is refused, by the host and by meridian-plugin validate/build, with a load problem of kind contract (« contrat plugin incompatible — plugin ignoré »), and its pinned triggers reserve no address. Absent means no constraint. A manifest declares a range whose minimum is at least 0.1.1 when it uses declared events (contributes.events, a node event, an output from), at least 0.1.3 when an event declares a defaultSelector, and at least 0.1.4 when a node describes its reception (receive.mediaTypes, receive.example, a transport's defaultPort or hint, see Inbound protocol and mapping nodes): meridian-plugin validate and build fail otherwise, and the host loads it with the warning « plage sdk insuffisante ». One problem is reported, for the highest floor. A binding that produces another plugin's declared event does not count as using declared events. A manifest that uses resolve.pending on an event binding declares ^0.1.2, as @posos/fhir does. |
locale |
string | no | fr |
Source language of the manifest's labels, descriptions and docs. Other languages are overlay bundles i18n/<locale>.yaml. See Translate a plugin. |
dependsOn |
record { "<plugin>": "<semver range>" } |
no | — | Other plugins this one builds on, direct dependencies only. The plugin and its transitive dependsOn form its closure, which drives load order, cross-plugin type resolution, and where a trigger's event, an event's context kinds and its field types must resolve (dependsOnClosure, the same function in the host and the CLI). Each range is checked against the version of the dependency found, with npm semver semantics (^0.4.0 is >=0.4.0 <0.5.0; a prerelease matches only a range that names one): an invalid range, a version found outside the range, or an entry that names the plugin itself refuses the plugin; a dependency that is absent, or refused with a version in the range, leaves the plugin loaded without the contributions that cite it (see Load checks). * constrains nothing, prereleases included. Declare ^<major>.<minor>.0 of the series the plugin is tested with ("@posos/clinical": "^0.2.0"). |
contributes |
object | no | {} |
The fourteen contribution arrays below; each defaults to []. |
contributes sections
| Key | Element schema | Section |
|---|---|---|
valueSets |
value-set contribution | contributes.valueSets |
dimensions |
dimension contribution | contributes.dimensions |
contextKinds |
context-kind contribution | contributes.contextKinds |
types |
type contribution | contributes.types |
conversions |
conversion contribution | contributes.conversions |
events |
declared-event contribution | contributes.events |
nodes |
node contribution | contributes.nodes |
agents |
agent contribution | contributes.agents |
adapters |
adapter contribution | contributes.adapters |
eventSources |
event-source contribution | contributes.eventSources |
connectionTypes |
connection-type contribution | contributes.connectionTypes |
eventBindings |
event-binding contribution | contributes.eventBindings |
messageBindings |
EAI message-binding contribution | contributes.messageBindings |
portSandboxes |
port-sandbox contribution | contributes.portSandboxes |
Load order and resilience
The host applies contributions in a fixed order so that a symbol is always registered before anything that references it:
valueSets → contextKinds → dimensions → types → conversions → events → nodes → adapters → agents → eventSources → messageBindings → eventBindings → portSandboxes
An invalid 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 other contributions still load, and a plugin never blocks
startup. Value-set names on node ports are not checked at load. A
contribution whose module does not import is skipped the same way, with a load problem of
kind module; a node whose behavior module does not import is not registered. A key
that the schema does not know is dropped when the manifest is parsed: the host reports it
with a load problem of kind key and loads the plugin without it. Once every manifest is
parsed, the host checks each dependsOn range against the version of the dependency
found (kind dependency): a plugin whose range is invalid or does not accept that
version is not loaded, a plugin whose dependency is absent, or refused with a version in
the range, loads without what cites it. A plugin found in two roots loads from the first;
the other copy is ignored with the warning « plugin en double — copie ignorée ». Before
applying any manifest, the host collects the addresses of the pinned triggers of every
discovered manifest and of the core: an event declared with one of these types is refused,
whatever the load order. Once every plugin is applied, the host checks the bindings that
produce a declared event. Declared-event problems are logged under the
plugins component; see Load checks and
Plugin load warnings.
Plugins are discovered from the PLUGINS_PATH roots, or downloaded from a registry per the
instance manifest's plugins: map. See
Bundling and distribution and
Publish to the registry.
Manifest skeleton
apiVersion: meridian/plugin-v1
name: "@scope/name"
version: 0.1.0
sdk: "^0.1.1"
locale: fr
dependsOn:
"@posos/clinical": "^0.2.0"
description: What this plugin does.
contributes:
valueSets: [ … ]
dimensions: [ … ]
contextKinds: [ … ]
types: [ … ]
conversions: [ … ]
events: [ … ]
nodes: [ … ]
adapters: [ … ]
agents: [ … ]
eventSources: [ … ]
eventBindings: [ … ]
messageBindings: [ … ]
portSandboxes: [ … ]
TypeRef
Field and port types are described with a recursive TypeRef. The variants accepted by
the manifest schema:
kind |
Additional keys | Meaning |
|---|---|---|
primitive |
name |
One of String, Number, Decimal, Integer, Boolean, Date, DateTime, Time, Duration, Code, Uri. |
coded |
valueSet (string) |
A code bound to a contributed value-set. |
quantity |
dimension (string, optional) |
A dimension-aware measure. |
ref |
entity (string) |
A reference to the entity of a contributed context kind (e.g. Patient for the patient kind) — open, validated against the context-kind registry. |
object |
name (string) |
Another registered type, by name. |
record |
name (string), fields (record of { type, required? }) |
A workflow-local structural object. It does not enter the global type registry. |
list |
of (TypeRef) |
A homogeneous list. |
any |
— | A flow port; the type is inferred from wiring. |
For the full semantics and the TypeScript each variant generates, see the TypeRef reference.
contributes.valueSets
Terminologies backing coded types.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Value-set identifier referenced by coded types. |
system |
string | yes | Code-system URI/URN. |
doc |
string | yes | Description. |
concepts |
array of { code, display, parent? } |
yes | Enumerated concepts; parent (string) is an optional hierarchy link. Empty array when external. |
external |
boolean | no | When true, the value-set is not enumerated and is resolved by a terminology server. |
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: G3a, display: "G3a", parent: G3 }
- id: snomed-ct
system: http://snomed.info/sct
external: true
concepts: []
contributes.dimensions
Backing for quantity types (UCUM-style unit checking). The core declares no
dimension; @posos/common contributes the common physical ones (mass, volume,
time, the concentrations…), and a plugin whose ports use one of them declares
@posos/common in dependsOn.
A dimension is resolved in the plugin's dependsOn closure, like a type: a declared event
whose field, or a node whose port, uses a dimension from outside it is dropped at load,
whatever the load order (« dimension « mass » inconnue dans la fermeture dependsOn. Déclarer la dimension ou ajouter à dependsOn le plugin qui la contribue. »). A dimension already registered cannot be redefined:
the same definition from the same plugin is left in place, any other one fails the plugin's
load, like a type collision.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Dimension name referenced by quantity types. |
label |
string | yes | Display label. |
canonical |
string | yes | Canonical unit symbol. |
units |
array of { symbol, toCanonical } |
yes | symbol (string) and toCanonical (number) — the multiplicative factor to the canonical unit. A unit that needs an offset cannot be declared. |
dimensions:
- name: filtration-rate
label: Glomerular filtration rate
canonical: "mL/min/{1.73_m2}"
units:
- { symbol: "mL/min/{1.73_m2}", toCanonical: 1 }
contributes.contextKinds
The kinds of ambient context a workflow can carry (context: in a workflow spec,
context:/establishes: on nodes, keys of the ContextEnvelope). The registry
(libs/shared/src/domain/context-kinds.ts) is open and starts empty: the core
ships none; @posos/common contributes patient, encounter, order and
document. A kind ties three axes together: the context key, the id field an
external caller knows how to supply (what makes the kind composable by an
incoming webhook or an out-of-workflow agent invocation), and the entity name of
the associated ref TypeRef.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Context key (context:/establishes:/ContextEnvelope) — e.g. patient. |
idField |
string | yes | HTTP/event field carrying the entity id — e.g. patientId. |
entity |
string | no | Entity name of the associated ref TypeRef — e.g. Patient. |
description |
string | no | Description. |
contextKinds:
- { name: patient, idField: patientId, entity: Patient, description: The patient whose record is in context. }
Registering an already-registered kind is a collision and fails the contribution.
Kinds contributed by the dependsOn closure are referenced by name from
context:/establishes:. GET /api/catalog serves the registered kinds
(contextKinds), and validateSpec checks a workflow's context: against the
registry. The core trigger.webhook and trigger.workflow-called triggers
declare establishes: "*" — opportunistic: every registered kind whose
idField is present in the call becomes ambient context.
contributes.types
Datatypes and resources. Each field carries its own TypeRef.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Type name; referenced by object type-refs. |
doc |
string | yes | Description. |
layer |
enum datatype | resource |
yes | datatype = reusable building block; resource = domain resource. |
fields |
record { <field>: FieldDescriptor } |
yes | Field descriptors keyed by field name. |
FieldDescriptor:
| Field | Type | Required | Notes |
|---|---|---|---|
type |
TypeRef | yes | The field's type. |
required |
boolean | yes | Whether the field is mandatory. |
doc |
string | no | Field description. |
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 }
Types owned by a dependsOn plugin are referenced by name and resolved across the
dependency closure. See Generate types.
contributes.conversions
A declarative conversion powering the transform.convert node. expr is a JEXL
expression evaluated over value (the input) and now (the instant of the
conversion, an ISO 8601 UTC string).
| Field | Type | Required | Notes |
|---|---|---|---|
from |
string | yes | Source type name. |
to |
string | yes | Target type name. |
label |
string | yes | Display label. |
expr |
string | yes | JEXL expression evaluated over value and now; result is the converted output. A conversion whose expr does not compile, or reads another variable, an unknown transform or a function, is ignored at load with a warning. |
conversions:
- from: CreatinineClearance
to: Decimal
label: Clearance -> number
expr: value.value.value
- from: Prescription
to: Annotation
label: "Ordonnance → note (résumé)"
expr: "{ text: 'Ordonnance ' + value.identifier + ' : ' + (value.items|length) + ' ligne(s).', author: 'conversion', time: now }"
value.items|length is the size of the list: in JEXL, value.items.length
reads the first element of the list. See
JEXL expressions.
contributes.events
The contract of a domain event type: its context kinds and its typed fields. The
registry (libs/shared/src/domain/event-types.ts) is open and starts empty. A declared
event is received by the core trigger Événement reçu (trigger.event-received,
the event chosen in trigger.config.event): it is admitted by POST /api/events (it
starts the workflows configured on it, and answers 400 « Aucun workflow pour l'événement
event to rename or type its outputs. An
event is declared by one plugin, in its own manifest.
The event's producers (bindings, message bindings, callers of
POST /api/events) are expected to follow
the contract. Ingestion checks each event against it according to the instance's
EVENT_CONTRACTS mode: observe, the default, records the issues and lets the event
through, enforce refuses it, off checks nothing (see
Contract check). Under
observe or off, a required field mapped from a path absent from the record yields an
event without that field. The contract is read by:
- the trigger inspector, section Declared event (« Événement déclaré »);
- the Plugins page, section Declared events (« Événements déclarés ») of the declaring plugin's Events tab;
- the guided Field list of the trigger filter, and the event line of the AI assistant's digest;
- the example prefill (
exampleEvent), used by the Emit an event composer, the filter check and simulation.
For the steps, see Declare an event.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
type |
string | yes | — | Event type, ^[A-Z][A-Za-z0-9]{0,63}$ (PascalCase, which excludes the core's internal types such as webhook.received). |
description |
string | no | — | Translatable (events.<type>.description). |
context |
array of string or { kind, required? } |
no | [] |
Context kinds of the event, in order. A bare name is required; required defaults to true. Each kind must be registered by the plugin or its dependsOn closure. No duplicates. |
fields |
array of { name, type, required?, description? } |
no | [] |
Declared fields, in order. name matches ^[a-zA-Z][a-zA-Z0-9]{0,63}$ and is unique. type is a type name resolved like a type hint (String, DateTime, CodeableConcept, String[]) or a full TypeRef (needed for a coded type). required defaults to false. description is translatable (events.<type>.fields.<name>.description). |
example |
record { … } |
no | — | A conforming payload: fields, and the ids of the event's context kinds when useful (a simulation of a workflow that reads the stay needs its id). Pre-fills the console. |
defaultSelector |
string | no | — | The filter proposed by default to the workflows that receive the event, a jexl expression on event (same rules as trigger.selector), whose meaning only the plugin knows. See Default filter. Requires sdk ≥ 0.1.3. |
events:
- type: PatientAdmitted
description: Un patient est admis en hospitalisation, directement ou depuis les urgences.
context: [patient, encounter]
fields:
- { name: admittedAt, type: DateTime, description: Date et heure d'admission. }
- { name: unit, type: String, description: UF d'hébergement. }
- { name: medicalUnit, type: String, description: UF de responsabilité médicale. }
example: { encounterId: sejour-12, admittedAt: "2026-06-12T08:00:00Z", unit: "4036", medicalUnit: "4036" }
- type: MedicationPrescribed
context: [patient, { kind: encounter, required: false }]
fields:
- { name: code, type: String, required: true }
- { name: doseValue, type: Decimal }
Two events of the same shape share their field list with a YAML anchor (& / *).
Reserved names. A declared field cannot be called type, eventId, occurredAt
(envelope set by ingestion), connectionId, recordId (set or removed by EAI
delivery), done (the flow output of a trigger whose outputs are the event's fields), a
name starting with __ (ingestion commands), or the idField of one of the event's
context kinds (carried by context).
Types and addresses. A declared type cannot be the address of a pinned trigger
(pinnedRouting: true with that eventType) of any discovered plugin or of the core: the
declaration is refused, whatever the load order, and the address keeps its own routing
(an event of that type without __workflowFile is refused). A type is either an address
or a declared event. A pinned trigger reserves its address when its contribution
projects (a pinned trigger the projection drops reserves none) and its plugin accepts the
host's contract. The refusal is reported to the plugin that declares the event and to the
plugin that pins the address.
Validity. A present field is valid when it conforms to its type without conversion:
a Decimal/Number is a JSON number, an Integer an integer, a Boolean a JSON
boolean, a Date or DateTime a string in the checked format (Time is any string),
a code of an internal value set a member of that set, a String a string, a list an
array of valid elements. A value is absent when it is undefined, null or a string
that is empty once trimmed. [] is absent only in a field the contract declares as a
list; in a field of another type it is a present value, which that type refuses. The
require of an event or message binding applies the same rule (isAbsent,
libs/shared/src/engine/event-contract.ts): a name outside the contract has no declared
type, so [] counts as present for it. A context id is valid when it is
a non-empty string or a finite number; an empty one is absent. The contract is open: an
undeclared field is accepted and passed on as is. Messages built from the contract never
quote the received value.
GET /api/catalog serves the registry as eventTypes (descriptions translated), each
with its defaultSelector when it declares one.
Default filter
defaultSelector belongs to the event: it is proposed to every workflow that receives the
event, whatever its trigger. It is a prefill and a publication warning, never applied at
run time: routing evaluates only each workflow's own trigger.selector. A trigger's facts
(triggerFacts) read it in the contract of the event the trigger receives. The console
pre-fills it when an author picks a trigger on the event in the creation dialog or
switches a workflow's trigger to one; an imported file keeps its own filter. Publication
and rollback warn when a workflow's filter does not contain it: Événement « <type> » : le filtre par défaut est absent du sélecteur (<expression>). The AI assistant's digest shows
it as « filtre par défaut : … ».
At load, an invalid expression is removed from the event, which stays declared; a field
the filter reads outside the contract gets a note (see Load checks).
Example: @posos/common declares event.sender != "meridian" on CommunicationReceived:
a workflow whose selector keeps it does not start on the notifications Meridian writes to
the record with that sender.
Load checks
The host (loadPlugins, applyManifest, then a pass once every plugin is applied) and
meridian-plugin validate/build run the same functions: the contract check of a
manifest (pluginContractIssue), the comparison of the YAML document with the parsed
manifest, which finds the keys the schema drops (manifestKeyIssues,
libs/shared/src/plugin/manifest-keys.ts), the check of the dependsOn ranges against
the versions found (dependencyIssues, libs/shared/src/plugin/dependencies.ts), the registration of a manifest's events, which sets
aside a type already declared before any other check and then declares each event with
its default filter and its check against pinned addresses (registerPluginEvents,
declarePluginEvent, libs/engine-core/src/engine/plugin-events.ts), the projection of
a node contribution onto its descriptor, the kind of its problems and the pinned
addresses (contributedNodeMeta, nodeIssues, pinnedAddresses,
libs/shared/src/plugin/node-contribution.ts), the check of a contributed agent against
the rules of an agent (contributedAgent, libs/shared/src/plugin/agent-contribution.ts),
and the binding, sdk and port dimension checks of
libs/shared/src/plugin/declared-events.ts. Each problem has a kind, which gives its log
label, and an effect: the contribution is dropped, or it is kept and the problem is only
reported. A problem of kind filter is a note: it drops nothing and does not fail the
CLI. A node problem has kind trigger on a trigger and node on any other node.
| Check | Message (beginning) | Kind, log label | Effect |
|---|---|---|---|
sdk range that does not accept the host's plugin contract |
plage sdk « R » incompatible avec le contrat plugin 0.1.8 de l'hôte, plugin non chargé. Déclarer une plage sdk qui accepte 0.1.8. |
contract, « contrat plugin incompatible — plugin ignoré » |
plugin not loaded (host) or not registered (CLI), left out of every dependsOn closure; its pinned triggers reserve no address |
dependsOn range that does not accept the version of the dependency found |
« D » en 0.3.0, hors de la plage « ^0.2.0 », plugin non chargé. Charger une version de « D » dans « ^0.2.0 », ou déclarer "^0.3.0" si le plugin fonctionne avec 0.3.0. (the second proposal only for a version above the range; below it, Charger une version de « D » dans « ^0.2.0 ».) |
dependency, « dépendance signalée » |
plugin not loaded (host) or not registered (CLI), left out of every dependsOn closure; its pinned triggers reserve no address |
dependsOn entry that names the plugin itself, whatever its range |
« P » se cite dans dependsOn, plugin non chargé. Retirer cette entrée. |
dependency, « dépendance signalée » |
plugin not loaded (host) or not registered (CLI) |
dependsOn range that is not a semver range |
plage « latest » de « D » invalide, plugin non chargé. Déclarer une plage semver, par exemple "^0.2.0". (the example comes from the version found, Déclarer une plage semver. without one) |
dependency, « dépendance signalée » |
plugin not loaded or not registered |
Dependency not loaded (host), or found neither in node_modules nor in a sibling directory (CLI) |
dépendance « D » absente, les contributions qui la citent sont écartées. Charger « D » avec ce plugin. |
dependency, « dépendance signalée » |
plugin loaded; what cites the dependency is dropped with its own problem |
| Dependency refused for its contract or for one of its own entries, with a version in the range | dépendance « D » refusée au chargement, les contributions qui la citent sont écartées. Corriger le chargement de « D ». |
dependency, « dépendance signalée » |
plugin loaded; what cites the dependency is dropped with its own problem |
A key of the YAML document that the schema does not know, at any depth: the root, contributes, a contribution and the objects it holds (ports, config fields, event fields, concepts, map expressions…) |
clé inconnue, ignorée : la valeur de « description » est coupée à une virgule. Mettre cette valeur entre guillemets. for the rest of a value cut by an unquoted comma in a { … } mapping (a key without a value, or with a space in it, written right after a key with a text value, which the message names; the other keys without a value of the same cut may come between them); clé inconnue du contrat plugin 0.1.4, ignorée. Corriger ou retirer la clé. otherwise |
key, « clé ignorée » |
key ignored, plugin and contributions loaded without it |
An agent's input or output shorthand next to its list (inputs, outputs), or whose value is not a type name |
« input » exclu avec « inputs », clé ignorée. Retirer « input »., « input » : nom de type attendu, clé ignorée. Retirer « input ». |
key, « clé ignorée » |
shorthand ignored; agent loaded with its lists, or dropped without an input or an output list (kind node) |
| Type already declared by another plugin, or earlier in the same load | événement « E » déjà déclaré par « P ». Retirer l'une des deux déclarations. |
event, « événement signalé » |
second declaration dropped before any other check (its example and filter are not checked), the first stays |
Type name, field names, duplicated field or kind, reserved field name, the idField of one of the event's kinds used as a field |
type « X » invalide : …, champ « x » réservé à l'enveloppe de l'événement. …, champ « done » réservé au port de flux d'un déclencheur. …, champ « x » : identifiant d'une sorte de contexte de l'événement, … |
event, « événement signalé » |
event dropped |
Context kind, field type (object type, ref entity, value set, dimension) not resolved in the dependsOn closure |
sorte de contexte « k » inconnue dans la fermeture dependsOn. …, champ « x » : type « T » inconnu dans la fermeture dependsOn. …, champ « x » : dimension « mass » inconnue dans la fermeture dependsOn. … |
event, « événement signalé » |
event dropped |
| Type equal to the address of a pinned trigger of a discovered plugin or of the core | type « X » identique à l'adresse du déclencheur épinglé « T ». Renommer l'événement. |
event, « événement signalé » |
event dropped; the triggers that reference it fall with it (event unknown in the closure) |
| The same conflict, reported to each other plugin that pins the address | adresse « X » déclarée comme type d'événement par « P », événement refusé. Changer l'adresse ou renommer l'événement. |
trigger, « déclencheur signalé » |
pinned trigger kept |
example not conforming to the contract (context ids are not required there; a present one must be valid) |
exemple non conforme, retiré. … Corriger l'exemple. |
example, « exemple d'événement retiré » |
example removed, event kept |
defaultSelector that is not a valid filter (same checks as trigger.selector) |
filtre par défaut invalide, retiré. Colonne N : … Corriger « defaultSelector ». (the column when the expression problem has one) |
event, « événement signalé » |
filter removed, event kept |
defaultSelector that reads an event.<field> outside the contract: neither a declared field, nor a context id of the event, nor type, eventId, occurredAt, connectionId |
defaultSelector : event.x absent du contrat de « E ». Déclarer le champ dans l'événement ou corriger le filtre. |
filter, « filtre par défaut hors contrat » |
note: event and filter kept (the contract is open) |
event: trigger: event unknown in the closure, an excluded key, from on an input or with behavior, a from that names no contract key, an output type that does not fit |
événement « E » inconnu dans la fermeture dependsOn. …, « eventType » exclu avec « event », qui le fournit. …, sortie « x » : « y » absent du contrat de « E ». …, sortie « x » : type incompatible (A → B). … |
trigger, « déclencheur signalé » |
trigger dropped |
from on a node without event |
port « id.p » : « from » n'est admis que sur une sortie d'un déclencheur « event ». … |
trigger, « déclencheur signalé », or node, « nœud signalé » |
node dropped; a pinned trigger dropped this way reserves no address |
Trigger without pinnedRouting and without event, with or without eventType |
déclencheur non épinglé sans « event ». Déclarer l'événement et le référencer par « event ». |
trigger, « déclencheur signalé » |
trigger dropped |
pinnedRouting: true without eventType |
déclencheur épinglé sans « eventType ». Déclarer l'adresse par « eventType ». |
trigger, « déclencheur signalé » |
trigger dropped |
eventType, establishes, example or pinnedRouting on a node that is not a trigger |
« K » réservé à un déclencheur. Retirer « K ». |
node, « nœud signalé » |
node dropped |
receive on a node that is not a pinned trigger (an event: trigger, or a node of another kind) |
« receive » réservé à un déclencheur épinglé. Épingler le déclencheur ou retirer « receive ». |
trigger, « déclencheur signalé », or node, « nœud signalé » |
node dropped |
A host transport (http, directory) in receive.transports |
transport « http » fourni par l'hôte, ignoré. Retirer « http » de « receive.transports ». |
trigger, « déclencheur signalé » |
trigger kept, transport ignored: the host offers it to every protocol |
defaultSelector on a trigger with event |
« defaultSelector » se déclare sur l'événement (contributes.events), clé ignorée. Le déplacer sur l'événement « E ». |
trigger, « déclencheur signalé » |
trigger kept, key ignored |
defaultSelector on a pinned trigger |
« defaultSelector » non admis sur un déclencheur épinglé, clé ignorée. Retirer « defaultSelector ». |
trigger, « déclencheur signalé » |
trigger kept, key ignored |
defaultSelector on a node that is not a trigger |
« defaultSelector » non admis sur un nœud « K », clé ignorée. Retirer « defaultSelector ». (K: the node's kind) |
node, « nœud signalé » |
node kept, key ignored |
Node port (inputs, outputs, derivedPorts rules) on a dimension not resolved in the dependsOn closure, checked after the projection |
port « id.p » : dimension « mass » inconnue dans la fermeture dependsOn. Déclarer la dimension ou ajouter à dependsOn le plugin qui la contribue. |
trigger, « déclencheur signalé », or node, « nœud signalé » |
node dropped, without the problems of its projection |
Agent that breaks the rules of an agent (AgentSpec): an empty provider or model, no input or no output, a port without name or type |
agent « X » : modèle absent, agent ignoré. Renseigner « model »., agent « X » : aucune entrée, agent ignoré. Déclarer une entrée dans « inputs »., agent « X » : entrée 1 sans « type », agent ignoré. Renseigner « type ». (name: the agent's node type, agent.<slug>) |
node, « nœud signalé » |
agent dropped, plugin loaded |
Event binding to a declared event: a map key that is not a contract key (a declared field, the idField of one of the event's kinds, occurredAt), a required field or required-kind id missing from map |
binding « b » : champ « x » absent du contrat de « E ». Déclarer le champ ou retirer la clé du map., binding « b » ne produit pas « k », requis par « E ». Ajouter « k » au map. |
binding, « binding hors contrat » |
binding kept |
Event or message binding to a declared event: a require name that is not a contract key |
binding « b » : champ « x » absent du contrat de « E ». Déclarer le champ ou retirer « x » de require. |
binding, « binding hors contrat » |
binding kept |
Manifest using declared events with an sdk range below 0.1.1, or none |
plage sdk « R » inférieure à 0.1.1, requise par les événements déclarés. Déclarer sdk: "^0.1.1"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded |
Manifest declaring an event defaultSelector with an sdk range below 0.1.3, or none |
plage sdk « R » inférieure à 0.1.3, requise par le filtre par défaut d'un événement. Déclarer sdk: "^0.1.3"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded, filter kept |
Manifest describing the reception of a protocol (receive.mediaTypes, receive.example, a transport's defaultPort or hint) with an sdk range below 0.1.4, or none |
plage sdk « R » inférieure à 0.1.4, requise par la description de la réception d'un protocole. Déclarer sdk: "^0.1.4"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded, description kept |
Manifest contributing an inbound connection type that declares connection, with an sdk range below 0.1.8, or none |
plage sdk « R » inférieure à 0.1.8, requise par la connexion sortante d'un type de connexion entrant. Déclarer sdk: "^0.1.8"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded |
Manifest requiring a node connection by port, or contributing a connection type with urlSchemes or exposeCredentials, with an sdk range below 0.1.7, or none |
plage sdk « R » inférieure à 0.1.7, requise par les connexions par port ou les types de connexion hors HTTP. Déclarer sdk: "^0.1.7"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded |
Manifest contributing a connection type or declaring connection on an adapter, an event source or a node, with an sdk range below 0.1.6, or none |
plage sdk « R » inférieure à 0.1.5, requise par les connexions. Déclarer sdk: "^0.1.5"., plage sdk absente, … |
sdk, « plage sdk insuffisante » |
plugin loaded |
Types, entities, kinds, dimensions and events must come from the plugin's dependsOn closure: one
registered by a plugin outside it gets the same « inconnu dans la fermeture dependsOn »
message as an unknown one, whatever the load order. A binding to a type that no loaded
plugin declares gets no contract check. Every binding of a loaded manifest is checked, whether or not the host keeps it
(a message binding without its reception protocol, for one).
The host logs each problem under the plugins component with the fields plugin,
kind, name and issue, at warn, and a note at info; see
Plugin load warnings.
Apart from contract and a dependency problem on a range, a problem never stops the plugin from loading, and a dropped
event or node shows as ignored at load (« ignoré au chargement ») on the Plugins
page. The repository's plugins load with none, which a test asserts. The host also
drops, and reports with the kind of the node (trigger or node), a node that its own
node checks refuse (an unknown object type or context kind in its ports, context or
establishes, derivedPorts next to inputs, outputs or dynamic, an invalid config
schema, a port dimension outside the dependsOn closure); of those node checks,
meridian-plugin runs only the dimension check, at the same point. A pinned trigger dropped by
these checks keeps the address its contribution reserves: a declared event of that type
is still refused, and this problem tells why the trigger it names is absent. A node
dropped by these checks gets no other node problem: an ignored defaultSelector is
reported only for a node the host registers.
A problem of kind key is found on the manifest's text, for every plugin whose contract
the host accepts, whatever happens to its contributions: a node dropped for another
reason also reports its unknown keys. A plugin refused for its contract reports only that
refusal, since its keys may belong to a newer contract. name is the path of the key:
keys joined by ., array indexes in brackets, and a key that is not an identifier as a
JSON string in brackets, for example contributes.nodes[3].config[0].option,
dependOn or contributes.nodes[0].outputs[1]["émis si l'événement le porte."]. The
values the schema keeps whole are open: example, query, an adapter's config, a
config field's schema, and the keys of dependsOn, types[].fields, map and
cases; the objects the schema describes inside them (a field descriptor, a map
expression) are checked. A map expression that matches none of its forms, through a
misspelled key ({ jsonpat: … }) or a value of the wrong type
({ path: subject.reference, ref: "yes" }, { jsonpath: 1 }), is read as an empty
literal: each of its keys is reported as unknown, the valid ones included, and every
record the binding maps with it is rejected (see
MapExpr). Check the key names and the types of the
values. An object shared through a YAML alias is compared at each
occurrence, since each place has its own schema, and each of its keys is reported once, at
the first occurrence that drops it. The message « virgule » relies on the key written just
before: a key without a value written in block style right after a key with a text value
gets it too. A derivedPorts rule is strict: an unknown key there makes the whole
manifest invalid (« manifeste invalide »).
A problem of kind dependency is named after the dependency. The ranges of every
manifest whose contract is accepted are checked against the versions found, before
the closures and the pinned addresses are computed: in the host, the discovered plugins,
the first copy of a name only; in the CLI, what loadPluginClosure found
(node_modules, then the sibling directories). The version found is that of the copy
discovered, whether it loads or is refused, for its contract or for its own ranges: the
verdict does not depend on what happens to the dependency. A plugin that cites itself in
dependsOn is refused whatever the range. A plugin whose range is
invalid, or does not accept the version found, is refused like a plugin refused for its
contract, and reports only what refuses it (its unknown keys and its sdk floor are
reported at parse, before). The message proposes to declare ^<version found> only when
that version is above the range; below it, only a newer version of the dependency fits.
A plugin whose dependency is absent, or refused with a version in the range, loads: the
contributions that cite the dependency fall with the closure problems above (« inconnu
dans la fermeture dependsOn »), the others load. A refusal does not cascade: a plugin
that depends on a refused plugin is not refused itself. The problems come in dependency
order: those of a dependency before those of the plugins that cite it. * (or an empty
string) accepts any version, a prerelease included, unlike npm; the repository's plugins
do not use it.
The host also reports, with the kind module (« module non chargé »), each contribution
whose module does not import — a file missing from dist/, a syntax error, a dependency
that does not resolve, an error thrown while the module evaluates. The message gives the
module, the cause and the contribution, for example
module « ./simulation/sandbox.js » non chargé (Cannot find module '…'), sandbox du port « patient » ignoré. Corriger le module ou reconstruire le plugin.;
name is the module path as the manifest writes it. The contribution is ignored
and the rest of the plugin loads. A node whose behavior module does not import is not
registered, and gets no other problem. A node whose assist or receive module does not
import is registered without its configuration assistance or its reception protocol. An
adapter, event source, message binding or port sandbox is not loaded. meridian-plugin validate resolves no module, and build stops on a module it cannot find.
meridian-plugin validate prints each problem as
- <plugin> › <name> (<kind>) : <message> and exits 1; an unknown key of a plugin
reached only through dependsOn, neither given with --plugin nor the current
directory, is a note instead
(dépendances : <n> note(s) de clés inconnues :). meridian-plugin build
prints the problems of the built plugin and stops before writing dist/.
meridian-plugin publish reads dist/plugin.yaml again and, when its sdk range
accepts the CLI's plugin contract, refuses to publish a manifest that the schema of that
contract rejects, that carries an unknown key, or whose dependsOn has an invalid range
or names a dependency whose versions published on the target registry are all outside
its range; a dependency with no published version is not checked, and a note says so.
meridian-plugin deps lists each dependency of a plugin with its range, the version
found, its origin and its state, and build and deps print, as notes, the problems
that refuse a dependency marked refusée.
validate and build print a note the same way, under plugins : <n> note(s) d'événements déclarés : (validate) or
<name> : <n> note(s) d'événements déclarés : (build), without changing the exit code
or stopping the build. The CLI sees only the plugins it loads (the plugin directories it is given and their dependsOn
closure): a binding to an event declared outside that set, and a declared type equal to
the address of a plugin outside it, are checked by the host only. Within that set, an
address conflict is reported to both plugins, so build fails for the plugin it builds
whether that plugin declares the event or pins the address.
See
CLI.
contributes.nodes
A node declares typed ports and (optionally) points at a behavior factory module.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id |
string | yes | — | Unique node id. |
kind |
enum | yes | — | One of trigger, source, sink, compute, gateway, human-task, agent, transform. |
category |
string | yes | — | Palette grouping in the editor. |
label |
string | yes | — | Display label. |
description |
string | yes | — | Description. |
context |
array of string | no | [] |
Ambient context kinds the node requires. OPEN: each value must resolve in the dependsOn closure's contextKinds — checked at load, and an unknown kind skips the node with a warning (same rule as an unknown object type). |
establishes |
array of string | no | [] |
For a pinned trigger: the context kinds it establishes. Same open resolution rule as context. Provided by the event on a trigger with event. Refused on a node that is not a trigger. |
event |
string | no | — | For a trigger: the declared event it references. Excludes eventType, establishes, example, pinnedRouting: true, dynamic and derivedPorts. See Declarative triggers. A trigger without pinnedRouting needs event; without it, with or without eventType, it is dropped at load. |
eventType |
string | no | — | For a pinned trigger: its address, the event type the entry mechanism emits (a webhook call, a schedule tick, an EAI reception). Its payload is described by the trigger's outputs, not by a declared event, and no declared event can take this type. Refused on a trigger without pinnedRouting and on a node that is not a trigger. |
pinnedRouting |
boolean | no | — | Own address: each event of this type goes only to the workflows its address names (a webhook URL, the parent's call, the scheduled file, the workflows bound to an EAI connection), never to every workflow that receives the type. Requires eventType, the address. The run id is the event's eventId. POST /api/events refuses (400) an event without __workflowFile whose type only such triggers claim. Required for shared inbound EAI connections. Refused on a node that is not a trigger. |
defaultSelector |
string | no | — | Not admitted on a node: the default filter is declared on the event (events[].defaultSelector). The node loads without it, with a problem: on a trigger with event, move it to the event; on a pinned trigger or another node, remove it (see Load checks). |
example |
record { … } |
no | — | For a pinned trigger: an example payload pre-filled in the console. Provided by the event on a trigger with event. Refused on a node that is not a trigger. |
inputs |
array of PortDef | no | [] |
Input ports. |
outputs |
array of PortDef | no | [] |
Output ports. |
config |
array of ConfigFieldDef | no | — | Configuration fields. |
agentTool |
boolean | no | — | Opt-in: let an exposed agent (api.enabled) list this node as a tool. Only meaningful for a sink, which is otherwise excluded because it writes to an external system with no workflow author arbitrating the call. Eligibility for every other kind is derived from the meta itself (kind, dynamic, context) and checked when the agent is registered. |
dynamic |
enum | no | — | One of break, make, convert, switch, map, guard: engine primitives whose ports the core derives from the config. A plugin node with its own config-derived ports uses derivedPorts instead. |
derivedPorts |
{ inputs?, outputs? } |
no | — | Port rules evaluated on the node's config; they replace inputs/outputs (left empty) and exclude dynamic. See Declared configuration and ports. |
behavior |
{ module, export } |
no | — | The behavior factory module (see below). Optional for dynamic or purely structural nodes. |
receive |
{ protocol, label, transports?, mediaTypes?, example?, module, export? } |
no | — | Inbound protocol contribution for a pinned trigger. The module exports an InboundProtocol; host HTTP and directory transports remain implicit. See Inbound protocol and mapping nodes. |
connection |
{ type, required? } or { port, required? } |
no | — | Connection the behavior uses, designated by each instance of the node (nodes.<id>.connection in the workflow spec, RunArgs.connectionId at run time). type: the host passes ctx.connection(connectionId?), which resolves the designated connection, else the default connection of type, at each call. port: the node accepts any connection whose type an adapter of the port requires, and ctx.portFor(port, connectionId?) returns that adapter bound to the connection, else the port bound by the instance. See Connection types. |
assist |
{ module, export?, example?, requirePreview?, sampleHint?, previewHint? } |
no | — | Host-side configuration assistance: the module exports a NodeAssist (checks, suggestions, preview). See Declared configuration and ports. |
PortDef (inputs / outputs):
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Port name. |
type |
TypeRef | yes | Port type. |
required |
boolean | no | Whether the input must be connected. |
description |
string | no | Description. |
from |
string | no | Output of a declarative trigger only: the contract key that feeds it (default: the output's name). Refused anywhere else, and removed before the port reaches the engine. |
ConfigFieldDef (config):
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Config key. |
type |
TypeRef | yes | Config value type. |
description |
string | no | Description. |
typeHint |
boolean | no | A structural field (type hint, contract): never exposable as a port nor wired. |
required |
boolean | no | The field must be set; checked by static validation. |
schema |
JSON Schema subset | no | Shape of the value, checked by static validation and rendered by the console. See Declared configuration and ports. |
options |
array of string | no | Closed vocabulary of a non-coded field: the console renders a picker instead of a free-text box. Purely a UX affordance — enforcement still belongs to your behavior. Do not use it on a coded field: its codes are already enumerated by its value set, which the console expands on its own (with each concept's display as the label). |
A field whose values are known in advance should never be free text. Prefer a coded
type bound to a value set you contribute — you get validation, display labels, i18n and
the picker in one move — and fall back to options only for technical vocabularies that
are not clinical terminology (a mode, a unit, an auth scheme). An external value set
(SNOMED, via a terminology server) is not enumerable, so such a field stays free text.
A PortDef or ConfigFieldDef key outside these tables, like any key the schema does not
know elsewhere in the manifest, is ignored; the host and meridian-plugin validate/build/publish report it with its path (see Load checks). In
a flow mapping ({ … }), a comma ends an unquoted value and the rest becomes a key:
quote a value that contains a comma.
outputs:
- { name: deceased, type: { kind: primitive, name: Boolean }, description: "Indicateur de décès, émis si l'événement le porte." }
behavior:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
module |
string | yes | — | Path to the JS module exporting the behavior factory (resolved as .ts in dev or .js when compiled). |
export |
string | no | default |
Named export to use. |
The behavior module exports a factory (ctx) => NodeBehavior. The method the interpreter
calls depends on kind. For the full contract see the
behavior contract reference and
Implement a node behavior.
nodes:
- id: acme.egfr.ckd-epi-2021
kind: compute
category: Labs & calculations
label: eGFR (CKD-EPI 2021)
description: Estimates glomerular filtration rate.
context: []
inputs:
- { name: result, type: { kind: object, name: Observation }, required: true }
- { name: demographics, type: { kind: object, name: Demographics }, required: true }
outputs:
- { name: egfr, type: { kind: object, name: EgfrResult } }
behavior: { module: ./behaviors/egfr.ts, export: egfrCkdEpi2021 }
Declarative triggers
A trigger that references a declared event with event receives that event: the event
type, the context it establishes (every context kind of the event, required or optional,
in order), its example and its default filter come from the declaration, read in the
event registry each time they are needed (triggerFacts,
libs/shared/src/domain/trigger-facts.ts). The node does not write them. It is an
ordinary trigger for routing and validation. Every trigger without pinnedRouting
references a declared event this way.
outputs decide how it emits:
- with
behavior: the trigger's code projects the event as it does for any trigger (fromEventreceives the workflow's copy of the event, unchanged apart from itseventId, which is the run id);fromis refused, and the declared outputs are kept as they are, even an empty list; - without
behavior,outputsomitted or empty (outputs: []reads as omitted): the outputs are the event's fields (same name, type and description), in declaration order; - without
behavior,outputsdeclared: each output takes the value of the contract key named byfrom(default: the output's name) — a declared field, theidFieldof one of the event's context kinds, oroccurredAt(typedDateTime). The output type resolves in the plugin'sdependsOnclosure like a field type. The source type must be assignable to the output type, with two exceptions: arefoutput accepts a text source, projected to{ id }, and anAnysource (or a list ofAny) feeds only anAnyoutput, since the projection copies it unchecked. Fed by a context id, therefentity must be that kind's entity. An output withoutdescriptiontakes the field's.
Without behavior, the core provides fromEvent (declaredEventBehavior,
libs/engine-core/src/engine/declared-trigger.ts). It reads the event's contract in the
registry at each call. The host registers the trigger only when its event is declared in
the plugin's dependsOn closure: when the plugin that declares the event is no longer
loaded, the trigger is dropped at load with it, its events are refused at ingestion, a
new run of a workflow on it fails with « Trigger invalide : { id }
of each context kind whose id the event carries, in declaration order; each output is
emitted when its source value is present and readable, and not emitted otherwise:
- text (
String,Time, a code, a context id): a string without its edge whitespace, or a finite number written as text (9001→"9001"); Date/DateTime: the same text, then the format check (a numeric year such as2026passes as"2026"; a one-element array does not);Number/Decimal: a finite number or numeric text;Integer: the same, integer only;Boolean: a boolean, ortrue/falsetext (trimmed, case ignored);- a code of an internal value set: a member of that set;
- a list: each element read by its type, and the list is not emitted when one element is not read;
- an object type: the value as received when it conforms;
Any: the value as received.
No clock, no randomness.
A run keeps the projection it started with. In a run of
journal format 2, every
run started by a platform release that knows it, fromEvent runs once, in the durable
step trigger, with the checks of the outputs: a replay reads the context and the
outputs from the journal. Editing a declared event or a declarative trigger's outputs (a
field, a field type, a context kind, an outputs entry or a from) applies to the runs
that start after the new version is loaded. A run of format 1 calls
fromEvent again each time it resumes, and its outputs and context kinds are part of the
state it persists at its first step or wait: such an edit blocks a suspended or waiting
run of format 1 on a journal mismatch until the previous version is loaded again (see
Recover runs blocked by a missing plugin).
See ADR 0008.
The admission trigger of @posos/clinical (external-plugins/clinical/plugin.yaml)
declares its outputs; encounterRef takes the encounter id:
nodes:
- id: trigger.patient-admitted
kind: trigger
category: Déclencheurs
label: Patient admis
description: Un patient est admis en hospitalisation, directement ou depuis les urgences.
event: PatientAdmitted
outputs:
- { name: encounterRef, type: { kind: ref, entity: Encounter }, from: encounterId, description: Référence du séjour. }
- { name: admittedAt, type: { kind: primitive, name: DateTime }, description: Date/heure d'admission. }
- { name: unit, type: { kind: primitive, name: String }, description: UF d'hébergement. }
- { name: medicalUnit, type: { kind: primitive, name: String }, description: UF de responsabilité médicale. }
The same plugin's trigger.patient-discharged, trigger.patient-transferred and
trigger.discharge-prescription-edited (prescriptionRef ← prescriptionId) are
declarative too; its other triggers build resources (Observation,
MedicationStatement…) and keep a behavior. The plugin generated by
meridian-plugin new starts with one declarative trigger, example.received.
A trigger with event is refused (skipped with a warning) when the event is unknown in
its dependsOn closure, when it also sets one of the excluded keys, when a from is set
on an input, set with behavior or names no contract key, or when a source type does not
fit its output (see Load checks).
Declared configuration and ports
A node whose configuration has structure — a list of fields, questions with their possible answers — declares it in the manifest. The core evaluates these declarations identically in the editor, static validation and the interpreter, without knowing the node's domain.
config[].schemadescribes the value of a field in a subset of JSON Schema (draft-07):type(one type or a list,nullincluded),enum,const,pattern,minLength/maxLength,minimum/maximum,items,minItems/maxItems,properties,required,additionalProperties(a key → value table when it is a schema),minProperties/maxProperties,propertyNames,oneOfwithdiscriminator(variants chosen by a field),not,default,title,description. Three extensions:x-uniqueBy(a property unique across the items of a list),x-widget(textarea,typeName,identifier) andx-suggest(the key of the suggestions provided by the node's assist module). Unknown keywords are refused: schemas are compiled when the plugin loads, and an invalid one skips the node with a warning.derivedPortsgives the node's ports. A rule produces one port, or one port per item of a config list (forEach). Itsname,required(inputs only, defaulttrue) anddescriptionare literals or values read in the current item ({ from: path }) or in the config ({ fromConfig: path }), with an optionaldefault. Itstypeis a type name (hint grammar,Observation[]for a list), a literal TypeRef, a type name read in the config ({ from }/{ fromConfig }), a type chosen by a value ({ byValue, cases, default }), a list ({ listOf }), or a record whose fields come from a list ({ record: { name, forEach, field: { name, type, required } } }). A rule may includewhen: { value: { fromConfig: operation.kind }, in: [concat] }(SDK^0.1.5): it is included only when the resolved value strictly equals one of the nonempty list of strings, numbers or booleans.fromaddresses aforEachitem;fromConfigaddresses the whole config. An excluded rule resolves neither name nor type, and produces no port or error. A name that is invalid, duplicated or reserved (after,done) gives no port and is reported by static validation.assistis host-side code. The module exports aNodeAssist(@meridian/shared/plugin/assist) with three optional operations:validateConfig(config)(checks beyond the schema, run at save, at publication and in the editor once the schema holds),suggest(key, sample, config)(values for the fields markedx-suggest: <key>, taken from a sample) andpreview(sample, config)(the result shown by the editor). Withpreview, the editor opens a workshop — sample, suggestions, test;examplepre-fills it,requirePreviewapplies a configuration only after a successful preview,sampleHint/previewHintare shown beside the sample and the result. The API serves the module onPOST /api/nodes/:type/assist(validate,suggest,preview).
Schema title and description, literal port descriptions and the two hints are
translatable (keys config.<field>.schema<pointer>.<title|description>,
inputs.<name>.description, outputs.<name>.description,
assist.sampleHint, assist.previewHint).
The HL7 mapping node, abridged (external-plugins/hl7/plugin.yaml):
- id: hl7v2.map
kind: transform
config:
- name: name
type: { kind: primitive, name: String }
typeHint: true
required: true
schema: { type: string, title: Nom du type, pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$" }
- name: fields
type: { kind: any }
typeHint: true
required: true
schema:
type: array
minItems: 1
x-uniqueBy: name
items:
type: object
required: [name, path, type, required]
properties:
name: { type: string, title: Nom }
path: { type: string, title: Chemin, x-suggest: sourcePath }
type: { type: string, enum: [String, Integer, Number, Boolean] }
required: { type: boolean, default: true }
derivedPorts:
inputs:
- { name: content, type: String }
outputs:
- name: rows
type:
listOf:
record:
name: { fromConfig: name, default: Message }
forEach: fields
field: { name: { from: name }, type: { from: type }, required: { from: required, default: true } }
- { name: provenance, type: String }
assist: { module: ./lib/mapping.ts, export: assist, requirePreview: true }
Inbound protocol and mapping nodes
A raw-message trigger combines pinnedRouting: true, an eventType, a
connectionId config field, and receive. A node with receive that is not a
pinned trigger is dropped at load. The protocol module owns message
identity, optional validation profiles, and protocol transports, such as a TCP
listener. The host owns the durable inbox, HTTP and directory reception, connection
configuration, and workflow delivery. The manifest describes the reception: the
console's connection form and the OpenAPI description of an HTTP connection read it,
and name no protocol themselves.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
protocol |
string (^[a-z][a-z0-9-]{0,31}$) |
yes | — | Protocol id. One plugin per protocol: the loader ignores a second contribution of the same id. |
label |
string | yes | — | Display name of the protocol (connection form, traffic history, OpenAPI). |
transports |
array of string or { name, defaultPort?, hint? } |
no | [] |
Previous form of a protocol's own transport, still read: declare an inbound connection type instead (Connection types). Each name is a factory of InboundProtocol.transports; the host turns it into an inbound type reserved to the protocol, with the parameters host, port (default defaultPort) and encoding. A name is read as { name }. defaultPort (integer, 1024–65535, LISTENER_PORTS) is proposed to a new connection; hint is shown under the host and port fields of the connection form and translates under nodes.<id>.receive.transports.<name>.hint. A host transport (http, directory) is ignored with a problem: the host offers both to every protocol. |
mediaTypes |
array of type/subtype strings, at least one |
no | text/plain, application/octet-stream (DEFAULT_MEDIA_TYPES) |
Media types of the body of an HTTP connection, in the OpenAPI description (GET /api/openapi.json). The host decodes the body with the connection's encoding whatever its Content-Type. |
example |
string | no | — | A raw message, the example of each text media type in the OpenAPI description. |
module |
string | yes | — | Module exporting the InboundProtocol. |
export |
string | no | default |
Named export to use. |
mediaTypes, example, and a transport's defaultPort or hint need sdk ≥ 0.1.4
(RECEIVE_PRESENTATION_MIN_SDK): a lower range is reported with « plage sdk
insuffisante ». A host of contract 0.1.3 refuses a plugin in ^0.1.4 with a contract
problem, or reports « manifeste invalide » when a transport is written as an object,
which its schema does not accept.
nodes:
- id: trigger.acme-received
kind: trigger
category: ACME
label: ACME message received
description: Receives one ACME message from the selected connection.
pinnedRouting: true
eventType: AcmeReceived
config:
- { name: connectionId, type: { kind: primitive, name: String } }
receive:
protocol: acme
label: ACME
transports:
- name: acme-tcp
defaultPort: 2600
hint: The receiver acknowledges each message once it is stored.
mediaTypes: [application/x-acme, text/plain]
example: "ACME|1|HELLO"
module: ./inbound.ts
export: acmeProtocol
InboundProtocol.identify(content) returns the message id used for
connection-scoped deduplication. validationProfiles advertises selectable
profiles ({ id, label, sha256 }; the label names the profile in the console, the
OpenAPI description and a refusal), and validate(content, profile) returns valid or
invalid with validator, schema fingerprint, and safe issue diagnostics. It throws only
when validation cannot run. Identical retransmissions retain their original receipt
decision.
A message-mapping node (hl7v2.map, pn13.map) declares its contract — version,
output name, optional group path, fields — with config schemas, its ports
(content: String → rows: List<Record>, provenance: String) with
derivedPorts, and its assistance with assist: protocol checks, source paths
of a sample, and the same mapping as at runtime for the preview. See
Declared configuration and ports and
Map an HL7 or PN13 message.
contributes.adapters
Binds an implementation to a hexagonal port. A factory (config, deps?) => PortImpl;
deps.connection is passed when the adapter declares connection. The port
bundle is fully open: any port name is addressable, and the port's contract lives with
the plugin that owns it (the SDK types only agent and terminology — see
Port contracts).
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
port |
string | yes | — | The port implemented. |
name |
string | yes | — | Selection identifier used by the instance manifest. |
module |
string | yes | — | JS module exporting the factory. |
export |
string | no | default |
Named export to use. |
config |
record { … } |
no | — | Default/static config. |
systems |
array of string | no | — | For a terminology adapter: code systems it can handle (informative). |
connection |
{ type, required? } |
no | — | Connection the adapter uses: the factory receives { connection }, resolving ports.<port>.connection (else the default connection of type) at each call. See Connection types. |
record |
{ contextKind } |
no | — | Declares the adapter the record port of a context kind: its implementation satisfies SubjectRecordPort (SDK) for that kind's entities. This is what wires the host integrations — GET /api/records/:kind/:id and validation publication on suspension — without the host knowing the domain. |
Adapters are selected by the deployment's instance manifest (e.g.
ports.patient = { adapter: fhir, config }); a port that nothing selects does not exist.
See Port contracts,
Implement a port adapter and the
instance manifest reference.
adapters:
- { port: patient, name: fhir, module: ./adapters/patient.ts, record: { contextKind: patient } }
- { port: terminology, name: hermes, module: ./adapters/hermes.ts, systems: [snomed-ct] }
- { port: patient, name: in-memory, module: ./adapters/in-memory.ts, export: patient, record: { contextKind: patient } }
contributes.agents
An agent is projected into a node agent.<slug> whose ports are typed from its inputs
and outputs. The rules of an agent are those of an instance agent (AgentSpec): a
non-empty provider and model, at least one input and one output. They are checked at
load on each projected agent, by the host and by meridian-plugin validate/build: an
agent that breaks one is dropped alone, with a problem of kind node named after its node
type (« agent « X » : modèle absent, agent ignoré. Renseigner « model ». »), and the rest of
the plugin loads; validate and build exit 1. No default model is applied to an agent.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Agent name. |
description |
string | no | — | Description. |
provider |
string | yes, non-empty | — | Model provider. Conventionally one of anthropic, openai, gemini, mistral, azure-openai, local; the schema accepts any string. |
model |
string | yes, non-empty | — | Model identifier. |
prompt |
string | yes | — | System prompt. |
inputs |
array of AgentPort | at least 1, or input |
— | Named input ports. |
outputs |
array of AgentPort | at least 1, or output |
— | Named output ports. |
input |
string | no | — | Shorthand for a single input port: a type name, read as inputs: [{ name: input, type: <input> }] when inputs is absent; ignored and reported otherwise. |
output |
string | no | — | Shorthand for a single output port: a type name, read as outputs: [{ name: output, type: <output> }] when outputs is absent; ignored and reported otherwise. |
tools |
array of string | no | [] |
Node ids the model may call; a terminal submit_result tool validates the final answer against the outputs. |
maxIterations |
integer | no | — | Between 1 and 50. |
AgentPort (inputs / outputs):
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Port name. |
type |
string | yes | A type name (not a TypeRef). |
list |
boolean | no | Whether the port is a list. |
required |
boolean | no | Whether the port is required. |
description |
string | no | Description. |
Shorthand: a scalar input: <TypeName> / output: <TypeName> is rewritten to a
single named port (inputs: [{ name: "input", type: … }],
outputs: [{ name: "output", type: … }]) when the list is absent, as @posos/geriatrie
writes its agents. Next to its list, or with a value that is not a type name, the
shorthand is ignored and reported as a load problem of kind key
(« input » exclu avec « inputs », clé ignorée. Retirer « input ».,
« input » : nom de type attendu, clé ignorée. Retirer « input ».); the agent loads with
its lists. An agent left without an input or an output list is dropped as above
(agent « X » : aucune entrée, agent ignoré. Déclarer une entrée dans « inputs ».).
agents:
- name: Renal triage
provider: anthropic
model: claude-sonnet-4-5
inputs: [ { name: observation, type: Observation } ]
outputs: [ { name: condition, type: Condition } ]
maxIterations: 6
tools: [patient.load-demographics, acme.egfr.ckd-epi-2021]
prompt: |
You are a nephrologist. From the creatinine result, estimate renal function
(use the tools) and propose a Condition if warranted.
The agent runner is itself a port adapter (provided by a plugin such as @posos/llm).
See Author an agent.
contributes.eventSources
A generic ingestion mechanism that pulls records from an external system (polling). The
host makes each source an inbound connection type whose format is the source itself: a
connection of the type (type: <name> in the instance manifest, or created in the
console) starts the source with its parameters, its outbound connection when the source
declares one, and one feed (opaque query) per binding enabled on the connection. The
source emits raw records into the reception inbox, and the binding of each record's feed
projects it at delivery; the source has no knowledge of domain events.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Source mechanism name, referenced by bindings. Also the name of the inbound connection type and of its format. |
module |
string | yes | — | JS module exporting the SourceFactory. |
export |
string | no | default |
Named export to use. |
label |
string | no | name |
Label of the format and of the transport in the console's inbound connection form. |
description |
string | no | — | Description. |
params |
array of { name, label, description?, type?, required?, options?, min?, max?, default?, connectionType? } |
no | — | Parameters of a connection of the source, with the shape of a connection type's parameters. The host checks a connection's values against them, applies the defaults, and passes the result to the factory as config; the console builds the connection form from them. Absent: the parameters are passed unchecked. |
connection |
{ type, required? } |
no | — | Connection the source uses. The host adds a parameter connection (connectionType: <type>) in front of the declared ones; opts.connection resolves the connection it names, else the default connection of type, at each call. connection is not part of config. |
eventSources:
- name: fhir-poll
module: ./sources/poll.ts
label: Interrogation FHIR
connection: { type: fhir-r4 }
params:
- { name: intervalSeconds, label: Intervalle (s), type: number, min: 1, default: 15 }
- { name: count, label: Ressources par page, type: integer, min: 1, default: 50 }
description: Polls a FHIR server and emits raw resources.
label and params need no sdk range: they did not change the plugin contract, and a host
that predates them loads the plugin without them (« clé ignorée »).
See Contribute an event source and binding and The open event model.
Connection types
contributes.connectionTypes describes how to reach a kind of external system. The
instance declares the connections — instances of a type — in its manifest
(connections) or in the console; an adapter, a source or a node that declares
connection: { type } receives a ConnectionAccessor (@meridian/shared/plugin/connections)
that resolves the designated connection, or the default connection of the type, into an
authenticated ConnectionHandle at each call. The plugin reads no secret and no
environment variable, except for a type that declares exposeCredentials. Requires sdk
≥ 0.1.6 (CONNECTIONS_MIN_SDK); urlSchemes, exposeCredentials and a node
connection by port require 0.1.7 (PORT_CONNECTIONS_MIN_SDK).
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string ^[a-z][a-z0-9-]{0,63}$ |
yes | — | Type name, unique across loaded plugins (a second plugin contributing the same name is ignored with a warning). |
label |
string | yes | — | Label shown in the console. |
description |
string | no | — | Description, shown in the connection form. |
direction |
outbound or inbound |
no | outbound |
outbound: a system Meridian calls. inbound: a transport that brings messages to the host's reception (see below). |
auth |
array of none, basic, header, bearer, oauth2-client-credentials, ambient |
outbound: yes (≥ 1) | [] |
Outbound: authentication schemes the system accepts; the host implements them, ambient adds nothing to the request, for a client that uses the process identity itself. Inbound: basic and/or header, the schemes that check a caller against a credential (empty: none). |
params |
array of { name, label, description?, type?, required?, options?, min?, max?, default?, connectionType? } |
no | [] |
Parameters of the type (outbound: beyond the host's base baseUrl, timeoutMs, headers, auth). type: string (default), integer, number, boolean; options: closed vocabulary; min/max: bounds of a number; default: value used when the connection sets none; connectionType: the parameter names an outbound connection of that type, and the inbound connection form shows it as a picker of those connections (empty: the default connection of the type). |
formats |
array of protocol ids (receive.protocol) |
no | all | Inbound: the formats the transport carries. |
receiver |
{ module, export? } |
inbound: yes | — | Inbound: module exporting an InboundReceiverFactory (@meridian/shared/plugin/inbound). |
connection |
{ type, required? } |
no | — | Inbound: outbound connection the receiver opens itself (a socket authenticated by a token). The host adds a connection parameter to the type (a picker of connections of type; empty: the default connection of the type) and passes InboundReceiverOptions.connection. Requires sdk ≥ 0.1.8. |
urlSchemes |
array of URL schemes | no | [https, http] |
Outbound: schemes accepted for baseUrl, e.g. [smtp, smtps] for a system that is not reached over HTTP. fetch of such a connection throws: its client reads baseUrl itself. |
exposeCredentials |
boolean | no | false |
Outbound: the plugin reads the fields of the connection's credential in clear (ConnectionHandle.credentials()), for a client that handles its own authentication (SMTP). The console shows it in the connection form. |
connectionTypes:
- name: posos-api
label: API Posos
auth: [ambient]
params:
- { name: audience, label: Audience IAP, required: true }
- { name: region, label: Région, options: [fr-FR, fr-BE], default: fr-FR }
adapters:
- { port: patient, name: fhir, module: ./adapters/patient.ts, connection: { type: fhir-r4 } }
nodes:
- id: acme.lookup
# …
connection: { type: posos-api }
The ConnectionHandle exposes id, type, label, revision (changes at each
modification, for a client cache), baseUrl, params (with the type's defaults),
fetch(path, init?) and authHeaders(), plus credentials() for a type that declares
exposeCredentials (the credential's fields; empty for none and ambient). fetch
appends a relative path to baseUrl, refuses an absolute URL of another origin, adds
the authentication and the connection's headers, applies its timeout and traces the call
(connection.fetch span); it throws for a baseUrl that is not http or https. A host
older than contract 0.1.6 refuses a plugin in ^0.1.6, one older than 0.1.7 a
plugin in ^0.1.7, and one older than 0.1.8 a plugin in ^0.1.8.
Connections that serve a port
A port with several providers (a notification channel, a decision model, an LLM) takes
one adapter per provider, each requiring its own connection type. A node declares
connection: { port }; each instance of the node designates a connection
(nodes.<id>.connection), and the host gives the behavior the adapter of the port that
requires the connection's type, bound to that connection:
# Plugin of a channel
connectionTypes:
- { name: slack, label: Espace Slack, auth: [bearer] }
adapters:
- { port: notification, name: slack, module: ./adapters/slack.ts, connection: { type: slack } }
# Plugin that owns the port
nodes:
- id: notify.send
# …
connection: { port: notification, required: false }
export const send: BehaviorFor<"notify.send"> = ({ ports, portFor }) => ({
run: async ({ inputs, connectionId }) => {
const channel = ((await portFor?.("notification", connectionId)) ?? ports.notification) as NotificationPort;
await channel.send({ /* … */ });
return {};
},
});
Without a designated connection, portFor returns the port bound by the instance
(ports.<port>). The host keeps one adapter instance per (port, connection, type); the
adapter's connection is still resolved at each call. When two adapters of the same port
require the same connection type, the first loaded serves the nodes, with the warning
« type de connexion déjà servi par un adapter de ce port ». In simulation, portFor
returns the port's sandbox.
Inbound connection types (transports)
An inbound type is a transport: a connection of the type pairs it with a format
(receive.protocol) and receives that format's messages into the host's inbox. The host
provides http (the connection's /api/ingress/<id> route) and directory (a watched
folder) to every format; a plugin contributes the others. When a connection is enabled,
the host calls the receiver factory with the connection's parameters (defaults
included) and an onMessage that resolves once the message is stored; the receiver
acknowledges according to its protocol and stops on stop().
connectionTypes:
- name: mllp
direction: inbound
label: Écoute MLLP
formats: [hl7v2]
params:
- { name: host, label: Adresse d'écoute, default: 127.0.0.1 }
- { name: port, label: Port, type: integer, required: true, min: 1024, max: 65535, default: 2575 }
- { name: encoding, label: Encodage, options: [utf-8, windows-1252, iso-8859-1], default: utf-8 }
receiver: { module: ./transports/mllp.ts, export: mllpReceiver }
meridian-plugin build bundles the receiver module like the other modules of the
manifest.
A transport that opens its own link (a socket authenticated by a token) declares
connection: { type }: the host adds a connection parameter to the type and passes the
resolved outbound connection as InboundReceiverOptions.connection (sdk ≥ 0.1.8).
Validation channels and incoming decisions
A human validation can be published to a messaging channel and decided there. Two halves, both contributed by the channel's plugin, none known to the host:
- Publication: an adapter of the port
validation(contractValidationChannelPort,@meridian/shared/domain/ports) that requires the channel's connection type. When a run suspends on a validation, the host callspublish(request)on the adapter bound to the connection the validation node designates (connection: { port: validation }on the node), else onports.validationof the instance.requestcarries the run and validation ids, the title, the payload, and, when the node gives them,message,recipients,options(a choice validation),deadlineand the event'ssubjects. Therefit returns is journaled and passed back toclose(closure, ref)when the validation ends (decision made here or elsewhere, expiry, withdrawal). A choice validation goes to the channel only: record ports (SubjectRecordPort) receive accept / reject validations. Requires journal format 2 (RUN_JOURNAL_FORMAT=2): format 1 has no closing step. - Decisions: an inbound connection type whose
formatsincludevalidation-decision, the host's format for incoming decisions. Its receiver callsonMessagewith one JSON decision per message (InboundDecision:idunique within the connection,runId,pendingId,decisionaccepted|rejectedorchoice,by,comment?); the inbox stores it, deduplicates it byidand applies it like a decision from the console. A refused decision (run ended, already decided, unknown choice) is logged and consumed; an unavailable engine makes the delivery retry.
connectionTypes:
- { name: chat, label: Messagerie, auth: [bearer] }
- { name: chat-app, label: Jeton d'application, auth: [bearer] }
- name: chat-socket
direction: inbound
label: Réponses de la messagerie
formats: [validation-decision]
connection: { type: chat-app }
receiver: { module: ./socket.ts }
adapters:
- { port: validation, name: chat, module: ./validation.ts, connection: { type: chat } }
contributes.eventBindings
The declarative, reusable link between a source feed and a domain action. Any plugin may
contribute bindings; an inbound connection of the source enables them and supplies the
source's parameters. A binding
declares either event (start a run) or resolve (resolve a pending human
validation) — exactly one of the two.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Unique binding id; equals the feed name on the mechanism. |
source |
string | yes | — | Target source mechanism (an eventSources name). |
query |
record { … } |
no | {} |
Source-specific query descriptor (e.g. { resource, params } for FHIR). |
event |
string | no | — | Issue A: the domain event type emitted. A binding emits without a designated workflow, so the type must be claimed by a loaded trigger without pinnedRouting — every declared event is, by the core trigger Événement reçu; otherwise the host drops the binding at load and logs the cause (« binding ignoré : événement « Binding inconnu pour ce protocole : <name>.). Only a declared type gets the contract check. |
map |
record { <field>: MapExpr } |
no | {} |
Issue A: projection of a raw record onto the event's fields. |
require |
array of string | no | [] |
Issue A: projected fields that must be present. A record whose listed fields are absent — missing, null, a string empty once trimmed, or [] for a field the contract declares as a list — becomes no event; it is counted rejected and logged at debug level (« enregistrement filtré (champs exigés vides) »). Under EVENT_CONTRACTS=enforce, the contract's required fields and required-kind ids are added to it. |
resolve |
{ run, pending?, decision, by? } |
no | — | Issue B: resolve a suspended run's human validation instead of starting a new run. |
description |
string | no | — | Description. |
resolve:
| Field | Type | Required | Notes |
|---|---|---|---|
run |
MapExpr | yes | Projects the run key. |
pending |
MapExpr | no | Projects the id of the decided validation (the pendingId published when the run suspended, pending.id of the run). When declared, the host applies the decision only if the run waits on that validation (otherwise 409, counted as a duplicate), and rejects a record that does not carry it. Requires sdk ^0.1.2. |
decision |
MapExpr | yes | Projects the decision (accepted | rejected). |
by |
MapExpr | no | Projects the author. |
Declare pending whenever the source can be read again after a decision was applied
(a since: epoch feed re-read at each startup, a restart overlap window) and a run can
wait on several validations in turn: without it, the decision of an earlier validation
resolves the one the run waits on now.
Validation rule (zod refine): a binding must declare event xor resolve.
Declaring both, or neither, is rejected.
eventBindings:
- name: fhir-creatinine
source: fhir-poll
event: BioResultReceived
query: { resource: Observation, params: { code: "http://loinc.org|2160-0" } }
map:
patientId: { path: subject.reference, ref: true }
encounterId: { path: encounter.reference, ref: true }
analyte: { const: creatinine }
value: valueQuantity.value
loincCode: { jsonpath: "$.code.coding[?(@.system=='http://loinc.org')].code" }
require: [patientId, encounterId, value]
When event names a declared event, its map and require are
checked against the contract: by the host once every plugin is loaded, and by
meridian-plugin validate and build when the declaring plugin is in the loaded
dependsOn closure. Every map key and every require name is a contract key (a
declared field, the idField of one of the event's kinds, or occurredAt), and every
required field and required-kind id is a map key. A mismatch is a load warning; the
binding still loads (see Load checks).
Producing a key does not guarantee a value: a map that reads a path absent from the record
yields an event without that field. Under EVENT_CONTRACTS=enforce the source filters on
the effective require — require plus the contract's required fields and required-kind
ids, computed once every plugin is loaded — so such a record never becomes an event;
under observe, the default, if it passes the declared require, it is counted
(meridian.events.contract_missing) and still emitted. List the contract's required keys
in require so that such a record starts no run in any mode.
The deployment supplies only an inbound connection of the source, with its parameters and the bindings it enables. See Connect an event source.
MapExpr
The projection grammar used by map and by resolve. Its evaluation lives in
libs/engine-core/src/engine/event-mapping.ts (JSONPath-based). Accepted forms:
| Form | Meaning |
|---|---|
"a.b.0.c" |
Dotted path (shorthand). |
{ path, ref?: true } |
Dotted path; ref: true strips a FHIR reference ("Patient/x" → "x"). |
{ jsonpath, all?: true, ref?: true } |
JSONPath (filters, wildcards); first match by default, all: true returns every match. |
{ const } |
Literal value. |
Defined in libs/shared/src/plugin/mapping.ts.
See the MapExpr reference for full semantics.
contributes.messageBindings
A message binding projects one raw EAI message into zero or more canonical
domain events. Operators enable bindings and configure their options per inbound
connection. This is separate from eventBindings, which project records emitted
by the source of a pulled inbound connection.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Binding id, lowercase and hyphenated. |
protocol |
string | yes | — | A loaded trigger's receive.protocol. |
label |
string | yes | — | Label shown in connection settings. |
event |
string | yes | — | Canonical event type emitted. A binding emits without a designated workflow: a binding whose type no loaded trigger without pinnedRouting claims (a type neither declared nor claimed by a dedicated trigger) is dropped at load, with the cause in the log (« binding ignoré : événement « |
description |
string | no | — | Description. |
require |
array of string | no | [] |
Projected fields that must be present before emission, by the same absence rule as an event binding's require. A payload whose listed fields are absent is not emitted. |
options |
array of { name, label, description?, default? } |
no | [] |
Per-connection string settings shown by the console. |
module |
string | yes | — | Module exporting the MessageBinding. |
export |
string | no | default |
Named export to use. |
messageBindings:
- name: acme-admission
protocol: acme
label: Admission → PatientAdmitted
event: PatientAdmitted
require: [patientId]
options:
- { name: patientAuthority, label: Patient identifier authority }
module: ./bindings/clinical.ts
export: admission
When event names a declared event, each require name must be a
contract key; the host checks it once every plugin is loaded (a protocol plugin often
loads before the plugin that declares the event) and warns otherwise. The check covers
require only; the projected payloads' conformity is covered by tests on fixtures.
Under EVENT_CONTRACTS=enforce, a payload is emitted only when it carries the effective
require — require plus the contract's required fields and required-kind ids — and
the emitted event goes through the
contract check like any
other; GET /api/connections/protocols serves the declared require.
MessageBinding.project(content, options, context) returns a list. An empty list means
the message does not match. A thrown error or a missing required field emits no
event for that binding; the receipt itself remains delivered and is not replayed.
The host supplies the event id, adds the connection id (connectionId) and, when the
payload has no occurredAt or a blank one, sets it to the message's reception time.
options holds the connection's values merged over the declared defaults.
context.timezone is the instance time zone (timezone in the instance manifest): the
zone of a source timestamp that carries no offset, unless an option of the connection
says otherwise. A date field in a projected event is a valid DateTime
(Date and DateTime formats) or is left
out; the target trigger checks it like any other output. A payload may carry the
reserved field recordId, a non-empty string naming the business fact it
projects: the host removes it from the event and derives the event id from it,
scoped by connection and event type, so the same fact received in several
messages or through several bindings of one connection is one event, which starts
one run per receiving workflow. Without recordId, the event id is specific to the
message and the binding. See
Connect an EAI.
contributes.portSandboxes
A simulation sandbox for a port the plugin owns — the engine ships no
business sandbox of its own. The module exports a factory
(seed, record) => portImpl: reads are served from the seed (shape defined
by the owning plugin, opaque to the engine), writes are captured through
record(op, targetId, payload) (never applied to a real system) while staying
re-readable (copy-on-write). A port with no contributed sandbox is simply absent
in simulation — a behavior that needs it fails with the clear
"Port « X » non branché" error, visible in the report.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
port |
string | yes | — | The port simulated. |
module |
string | yes | — | JS module exporting the sandbox factory. |
export |
string | no | default |
Named export to use. |
portSandboxes:
- { port: patient, module: ./simulation/sandbox.ts, export: patientSandbox }
@posos/clinical contributes the patient and prescription sandboxes,
@posos/notify the notification one. Seeds are supplied per port name in
POST /api/simulate (options.seeds), or read from a real record with
options.subject: { kind, id } (when the context kind has a record port, the
subject's record seeds that port). See
the simulation engine.
Related references
- TypeRef — the type grammar used across fields and ports.
- MapExpr — the binding projection grammar.
- Behavior contract — what a node behavior module exports.
- Port contracts — the interfaces adapters implement.
- SDK surface — the
@meridian/sharedsub-paths a plugin imports. - CLI —
meridian-plugincommands (gen-types,build,validate,publish,i18n). - Declare an event —
contributes.eventsand its trigger, step by step. - Instance manifest — how a deployment selects adapters, sources and bindings.
- Bundled plugins — real manifests to read.