Documentation / Développement / Référence / Plugin manifest

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: []

See Add vocabulary and types.

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 » when none is) and its bindings stay loaded without any trigger of the plugin. A plugin trigger may reference it with 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 (fromEvent receives the workflow's copy of the event, unchanged apart from its eventId, which is the run id); from is refused, and the declared outputs are kept as they are, even an empty list;
  • without behavior, outputs omitted or empty (outputs: [] reads as omitted): the outputs are the event's fields (same name, type and description), in declaration order;
  • without behavior, outputs declared: each output takes the value of the contract key named by from (default: the output's name) — a declared field, the idField of one of the event's context kinds, or occurredAt (typed DateTime). The output type resolves in the plugin's dependsOn closure like a field type. The source type must be assignable to the output type, with two exceptions: a ref output accepts a text source, projected to { id }, and an Any source (or a list of Any) feeds only an Any output, since the projection copies it unchecked. Fed by a context id, the ref entity must be that kind's entity. An output without description takes 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 : », and a run in flight on it of journal format 1 stays blocked until the plugin is loaded again (a run of format 2 reads its trigger from its journal, see below). The context is the { 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 as 2026 passes as "2026"; a one-element array does not);
  • Number/Decimal: a finite number or numeric text; Integer: the same, integer only;
  • Boolean: a boolean, or true/false text (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[].schema describes the value of a field in a subset of JSON Schema (draft-07): type (one type or a list, null included), 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, oneOf with discriminator (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) and x-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.
  • derivedPorts gives the node's ports. A rule produces one port, or one port per item of a config list (forEach). Its name, required (inputs only, default true) and description are literals or values read in the current item ({ from: path }) or in the config ({ fromConfig: path }), with an optional default. Its type is 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 include when: { 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. from addresses a forEach item; fromConfig addresses 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.
  • assist is host-side code. The module exports a NodeAssist (@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 marked x-suggest: <key>, taken from a sample) and preview(sample, config) (the result shown by the editor). With preview, the editor opens a workshop — sample, suggestions, test; example pre-fills it, requirePreview applies a configuration only after a successful preview, sampleHint/previewHint are shown beside the sample and the result. The API serves the module on POST /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 (contract ValidationChannelPort, @meridian/shared/domain/ports) that requires the channel's connection type. When a run suspends on a validation, the host calls publish(request) on the adapter bound to the connection the validation node designates (connection: { port: validation } on the node), else on ports.validation of the instance. request carries the run and validation ids, the title, the payload, and, when the node gives them, message, recipients, options (a choice validation), deadline and the event's subjects. The ref it returns is journaled and passed back to close(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 formats include validation-decision, the host's format for incoming decisions. Its receiver calls onMessage with one JSON decision per message (InboundDecision: id unique within the connection, runId, pendingId, decision accepted | rejected or choice, by, comment?); the inbox stores it, deduplicates it by id and 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 « » non déclaré par les plugins chargés. Charger le plugin qui le déclare. », or « binding ignoré : « » est l'adresse du déclencheur épinglé « ». Viser un événement déclaré. »), and its feed never starts: a connection that names it is unusable (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 « » non déclaré par les plugins chargés. … », or « binding ignoré : « » est l'adresse du déclencheur épinglé « ». … »).
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.

  • 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/shared sub-paths a plugin imports.
  • CLI — meridian-plugin commands (gen-types, build, validate, publish, i18n).
  • Declare an event — contributes.events and its trigger, step by step.
  • Instance manifest — how a deployment selects adapters, sources and bindings.
  • Bundled plugins — real manifests to read.
97 documents29 sectionssource : /docs · généré au build