Documentation / Développement / Référence / Node behavior contract

Node behavior contract

The behavior contract is the API a plugin compiles against to implement the runtime logic of a node. It is defined in libs/shared/src/plugin/behavior.ts and shipped as part of the SDK surface (@meridian/shared). It is browser-safe: it declares only types, no engine code. Plugin autonomy requires that a plugin depend only on @meridian/shared; the contract lives there so a plugin can import it directly. It is re-exported from libs/engine-core/src/engine/catalog.ts for engine-side consumers.

This page documents the exported shapes and how the host invokes them. For the task of writing a behavior, see Implement a node behavior. For the ports reachable from a behavior, see Port contracts. For the wider published surface, see SDK surface.

Exported symbols

All symbols are exported from libs/shared/src/plugin/behavior.ts.

Symbol Kind Purpose
NodeBehaviorFactory<I, O> type (ctx) => NodeBehavior; the value a node module exports.
NodeBehavior<I, O, C> interface The node's runtime methods (run / propose / fromEvent).
PluginNodeContext interface Ambient context passed to the factory (ports, locale, t).
RunArgs<I, C> interface Argument bag for run and propose.
Proposal<O> interface Human-validation request produced by a human-task.
ValidationResolution interface The human decision resolving a human-task.

The factory

export type NodeBehaviorFactory<
  I = Record<string, unknown>,
  O = Record<string, unknown>,
> = (ctx: PluginNodeContext) => NodeBehavior<I, O>;

A node module exports one factory. The host calls it once per node type, at catalog construction time, passing a PluginNodeContext; the returned NodeBehavior is merged onto the node's static metadata. The factory is the place to capture ctx (ports, locale, t) in a closure so the returned methods can use it.

The generics I (inputs), O (outputs) and C (config, on NodeBehavior) carry the strong typing derived from the node descriptor by codegen. They all default to Record<string, unknown>, so a behavior written without type parameters remains valid. Generated typed signatures live in each plugin's generated/nodes.ts — see Generate types and TypeRef.

Instantiation

The factory is instantiated in makeCatalog (libs/engine-core/src/engine/catalog.ts):

pluginBehaviors[id]!({ ports, locale: i18n?.locale, t: i18n?.tFor(id) })
Field passed Source When absent
ports The adapter bundle wired for the instance. Always present.
locale i18n.locale (instance locale). undefined if the host supplies no i18n.
t i18n.tFor(id), bound to the contributing plugin's runtime bundle. undefined if the host supplies no i18n.

Engine primitives (the value.*, transform.*, flow.* nodes) are built in to makeCatalog and do not go through a factory; only plugin-contributed node types are instantiated this way.

NodeBehavior

export interface NodeBehavior<
  I = Record<string, unknown>,
  O = Record<string, unknown>,
  C = Record<string, unknown>,
> {
  run?: (args: RunArgs<I, C>) => Promise<O> | O;
  propose?: (args: RunArgs<I, C>) => Proposal<O> | null;
  fromEvent?: (event: Record<string, unknown>) => {
    context: ContextEnvelope;
    outputs: O;
  };
}

Which method a node must implement depends on its kind. All three are optional in the type, but the interpreter requires the one matching the kind and throws if it is missing.

Node kind Required method Role
source, sink, compute, gateway run Compute and return the node's outputs by port name.
human-task propose Build a validation request; onResolve / onTimeout produce outputs.
trigger fromEvent Establish the ambient context and initial outputs from the inbound event.

Node kinds are covered in Core primitives.

run

Returns the node's outputs keyed by output-port name. May be synchronous or return a promise. The interpreter runs it inside a durable step (ctx.step) and validates its outputs against the declared output ports (and coded value-sets) before recording them.

Omitting an output port from the returned object means nothing is emitted on that port, and any branch fed by that port stops. This is the mechanism behind guarded branching. See Branching and absence.

propose

For human-task nodes. Returns a Proposal describing the validation to request, or null to emit nothing (the node is marked skipped and no validation is created). When a Proposal is returned the interpreter suspends the run awaiting a human decision, then calls onResolve (on a decision) or onTimeout (on SLA expiry) to obtain the node's outputs. See Two-level validation and Add human validation.

fromEvent

For the trigger node. Receives the raw inbound event and returns the ambient clinical context (a ContextEnvelope) plus the trigger's initial outputs. Its outputs are validated like any node's. See The open event model.

RunArgs

export interface RunArgs<
  I = Record<string, unknown>,
  C = Record<string, unknown>,
> {
  inputs: I;
  config: C;
  context: ContextEnvelope;
}
Field Type Description
inputs I Resolved input-port values, keyed by port name.
config C The node instance's effective configuration.
context ContextEnvelope Ambient context for the run (open map of context kinds).

The same RunArgs value is passed to both run and propose.

ContextEnvelope

Defined in libs/shared/src/domain/types.ts: Record<string, { id: string } | undefined> — an open map, one key per context kind established at trigger time. The keys are the plugin-contributed context kinds (@posos/common contributes patient, encounter, order, document); every entry is optional, so guard the keys your behavior needs.

Proposal

export interface Proposal<O = Record<string, unknown>> {
  title: string;
  payload: Record<string, unknown>;
  deadlineSeconds?: number;
  onResolve: (resolution: ValidationResolution) => O;
  onTimeout?: () => O;
}
Field Type Description
title string Human-readable title of the validation request.
payload Record<string, unknown> Data shown to the validator.
deadlineSeconds number? SLA before escalation; absent means no deadline.
onResolve (resolution) => O Outputs produced once a human decides.
onTimeout () => O? Outputs produced if the deadline expires with no decision. If absent, {} is used.

ValidationResolution

export interface ValidationResolution {
  decision: "accepted" | "rejected";
  by: string;
  at: string;
  comment?: string;
}
Field Type Description
decision "accepted" | "rejected" The human decision.
by string Author of the decision.
at string Timestamp of the decision.
comment string? Optional free-text comment.

PluginNodeContext

export interface PluginNodeContext {
  ports: Ports;
  locale?: string;
  t?: (key: string, params?: Record<string, unknown>) => string;
}
Field Type Description
ports Ports The hexagonal port bundle (adapters). See Port contracts.
locale string? Instance locale — the language of persisted content (notifications, validation titles). See below.
t function? Runtime translator for this plugin's keys. See below.

ports

The Ports bundle (libs/shared/src/domain/ports.ts) is open and every port is optional: the SDK types only agent and terminology; business ports (patient, prescription, notification, …) are typed by their owning plugin's declare module augmentation and guarded at runtime with requirePort<T>(ports, "<name>"). Full method signatures are in Port contracts.

locale

The instance locale — the language of persisted content. The contract documents a fallback of "fr" when the host does not supply it; note that at instantiation the host passes i18n?.locale, which is undefined when no i18n is configured, so a behavior that formats persisted content should provide its own default. See Instance vs UI locale and Set the instance locale.

t

Translates a runtime key of the plugin (the runtime section of its i18n/<locale>.yaml bundles), resolved in the instance locale with {param} interpolation. Resolution falls back instance locale → plugin source locale → the key itself. t is undefined when the host provides no i18n, so callers should guard for its absence. See Translate a plugin.

Invocation summary

The interpreter (libs/engine-core/src/engine/interpreter.ts) drives the methods as follows.

Step Method Notes
Trigger fires fromEvent(event) Establishes context; outputs validated; missing fromEvent is an error.
human-task node propose(args) null marks the node skipped; otherwise the run suspends and resolves via onResolve / onTimeout.
Other nodes run(args) Executed in a durable step; outputs validated; missing run is an error.

The durability of run steps and the suspend/resume of propose are provided by the workflow context seam; see The workflow context seam and Interpreter internals.

75 documents16 sectionssource : /docs · généré au build