Plugin manifest
A plugin is a versioned bundle that contributes to a fixed set of extension points:
value-sets, dimensions, context kinds, types, conversions, nodes, agents, adapters,
event sources, event 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 SDK (@meridian/shared), e.g. ^0.1.0. Verified at load against PLUGIN_CONTRACT_VERSION using the npm semver package; an incompatible plugin is refused and logged. Absent means no constraint. |
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; drives load order and cross-plugin type resolution. The range value is not yet enforced. |
contributes |
object | no | {} |
The eleven 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 |
nodes |
node contribution | contributes.nodes |
agents |
agent contribution | contributes.agents |
adapters |
adapter contribution | contributes.adapters |
eventSources |
event-source contribution | contributes.eventSources |
eventBindings |
event-binding contribution | contributes.eventBindings |
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 → nodes → adapters → agents → eventSources → eventBindings → portSandboxes
An invalid manifest, or a node referencing an unknown type/value-set/dimension, is logged
and skipped; the other contributions still load and a plugin never blocks startup. Plugins
are discovered from a built-in directory plus every path in PLUGINS_PATH, 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.0"
locale: fr
dependsOn:
"@posos/clinical": "*"
description: What this plugin does.
contributes:
valueSets: [ … ]
dimensions: [ … ]
contextKinds: [ … ]
types: [ … ]
conversions: [ … ]
nodes: [ … ]
adapters: [ … ]
agents: [ … ]
eventSources: [ … ]
eventBindings: [ … ]
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. |
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).
| 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. |
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).
| 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; result is the converted output. |
conversions:
- from: CreatinineClearance
to: Decimal
label: Clearance -> number
expr: value.value.value
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 | — | Context kinds the node establishes (typically a trigger). Same open resolution rule as context. |
eventType |
string | no | — | For a trigger: the domain event type it accepts (must be a registered event; the API accepts it on POST /api/events). |
example |
record { … } |
no | — | For a trigger: an example payload pre-filled in the console. |
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. Marks an engine primitive whose ports derive from config; plugins do not define these. |
behavior |
{ module, export } |
no | — | The behavior factory module (see below). Optional for dynamic or purely structural nodes. |
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. |
ConfigFieldDef (config):
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Config key. |
type |
TypeRef | yes | Config value type. |
description |
string | no | Description. |
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.
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 }
contributes.adapters
Binds an implementation to a hexagonal port. A factory (config) => PortImpl. 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). |
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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Agent name. |
description |
string | no | — | Description. |
provider |
string | yes | — | Model provider. Conventionally one of anthropic, openai, gemini, mistral, azure-openai, local; the schema accepts any string. |
model |
string | yes | — | Model identifier. |
prompt |
string | yes | — | System prompt. |
inputs |
array of AgentPort | yes (min 1) | — | Named input ports. |
outputs |
array of AgentPort | yes (min 1) | — | Named output ports. |
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. |
Backward compatibility: a scalar input: <TypeName> / output: <TypeName> is
accepted and rewritten to a single named port (inputs: [{ name: "input", type: … }],
outputs: [{ name: "output", type: … }]).
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 (polling, an HL7 socket…). It receives a connection config and a list of feeds (opaque queries) and emits raw records; it has no knowledge of domain events.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string | yes | — | Source mechanism name (referenced by bindings). |
module |
string | yes | — | JS module exporting the SourceFactory. |
export |
string | no | default |
Named export to use. |
description |
string | no | — | Description. |
eventSources:
- { name: fhir-poll, module: ./sources/poll.ts, description: Polls a FHIR server and emits raw resources. }
See Contribute an event source and binding and The open event model.
contributes.eventBindings
The declarative, reusable link between a source feed and a domain action. Any plugin may
contribute bindings; the instance activates them and supplies connection config. 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 (must match a registered trigger). |
map |
record { <field>: MapExpr } |
no | {} |
Issue A: projection of a raw record onto the event's fields. |
resolve |
{ run, 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. |
decision |
MapExpr | yes | Projects the decision (accepted | rejected). |
by |
MapExpr | no | Projects the author. |
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 }
analyte: { const: creatinine }
value: valueQuantity.value
loincCode: { jsonpath: "$.code.coding[?(@.system=='http://loinc.org')].code" }
The deployment supplies only the connection config (and an optional binding allow-list) in its instance manifest. See Connect an event source.
MapExpr
The projection grammar used by map and by resolve. Its evaluation lives in the API
host (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.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 } (the record port of that context kind seeds the
corresponding 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). - Instance manifest — how a deployment selects adapters, sources and bindings.
- Bundled plugins — real manifests to read.