Documentation / Développement / Guides pratiques / Validate a workflow

Validate a workflow

meridian-plugin validate statically checks a workflow YAML against your plugin's declared nodes (plus its dependsOn closure) and the engine's core primitives (flow.*, transform.*) — no engine, no infrastructure, nothing is executed (libs/plugin-cli/src/commands/validate.ts). It reports unknown node types, unknown or mistyped ports, missing context, and required inputs left unconnected, and exits non-zero on error so it drops straight into CI.

This is the static half of validation only — it never loads a behavior module and never checks that a port's runtime value actually conforms. For that split, see Two-level validation; for what actually runs your behaviors against sample data, see Simulate a workflow.

It assumes a plugin directory with a plugin.yaml (see Scaffold a plugin) and a workflow YAML wired against its nodes (see Your first workflow and the workflow DSL for the grammar itself). You do not need gen-types or build first: the validator reads contributes straight out of plugin.yaml — types, value-sets, dimensions, context kinds, declared events and node metadata — never the generated/ output or a compiled dist/.

Run the validator

  1. From the plugin's directory, run the CLI directly or the validate npm script the scaffold wires up (libs/plugin-cli/src/commands/scaffold.ts):

    npm run validate workflows/example.workflow.yaml   # → meridian-plugin validate …
    

    With no --plugin flag, the plugin directory defaults to . — the current directory (libs/plugin-cli/src/cli.ts).

  2. Pass --plugin <dir> once per additional plugin directory the workflow's nodes come from. You only need this for plugins that aren't reachable through a dependsOn closure already: dependsOn entries are resolved automatically through node_modules, the same way gen-types resolves them (libs/plugin-cli/src/discover.ts, loadPluginClosure). Repeat the flag for each extra root; results are merged and de-duplicated by plugin name.

    Inside the platform repository, use the root pnpm plugin script (there is no globally installed meridian-plugin binary there) instead of a package-local npm run validate. For example, content/clinical-demo/workflows/bio-result.workflow.yaml wires a trigger and a few patient.*/human.* nodes from the @posos/clinical plugin to compute/sink nodes contributed by the external @posos/demo plugin — neither depends on the other, so both roots must be listed:

    pnpm plugin validate content/clinical-demo/workflows/bio-result.workflow.yaml \
      --plugin external-plugins/clinical \
      --plugin external-plugins/demo
    

    flow.guard, also used in that workflow, needs no --plugin at all — core primitives are baked into the validator's catalog before any plugin is registered (libs/shared/src/engine/catalog-metadata.ts).

Read the result

A valid workflow prints its name and exits 0:

✓ Réception résultat biologie: valid (ports exist, connection types compatible, context, required inputs).

An invalid one lists every error, one per line, and exits 1:

✗ Réception résultat biologie: 2 error(s):
  - Connexion trigger.result → ghost.criteria : nœud cible « ghost » inconnu.
  - Nœud « guard_creatinine » : entrée requise « criteria » non connectée.

The loaded plugins are checked too, with the host's load checks (plugin contract, manifest keys the schema does not know, dependsOn ranges against the versions found, declarations and their default filters, node projection, event: triggers, pinned addresses, bindings, sdk range; see Load checks). A problem is printed as - <plugin> › <name> (<kind>) : <message> under ✗ plugins : <n> problème(s) de chargement :, the workflow is checked as well, and the command exits 1. An unknown key of a plugin reached only through dependsOn, neither passed with --plugin nor the current directory when no --plugin is given, is printed under dépendances : <n> note(s) de clés inconnues : and leaves the exit code unchanged: the host loads that plugin without the key, and its author fixes it.

The checker's own diagnostic strings are emitted in French, regardless of your plugin's locale: or an instance's locale — this is internal tooling output from libs/engine-core/src/engine/validate.ts and graph.ts, not the runtime ctx.t translation layer described in the manifest reference's i18n section; don't confuse the two.

Fix common failures

  • Unknown node or trigger type (type inconnu) — a typo in a node's type, or the owning plugin's directory is missing from --plugin and isn't reachable through dependsOn either. Add the directory or fix the id.

  • Unknown source/target node on a connection (nœud … inconnu) — the from/to side of a connection names a node id that isn't declared under nodes: (or isn't trigger). Check for a rename or a typo.

  • Unknown port, or unknown split-pin field (sortie/entrée … inexistante, champ … inconnu) — the port name, or the field addressed after the dot on a split port (node.port.field), doesn't exist on that node or type. Cross-check the node's ports in Plugin manifest or the type's fields in TypeRef.

  • Incompatible connection type (type incompatible) — the output type isn't assignable to the input type under structural subtyping. See TypeRef and The dataflow model. When two distinct types share a name, each label carries its origin: Quantity (type du cœur) → Quantity (@posos/common) is the core quantity kind wired into a node typed with the plugin's Quantity object; the two never connect.

  • Unknown type name in a type hint (type « … » inconnu dans « type ») — a type, valueType, itemType, resultType, from or to of a dynamic node names no registered type and no primitive, at the exact case. The message lists the names that match ignoring case; a core kind without a name is written as a TypeRef ({ kind: quantity }). See Core primitives.

  • Missing or unestablished context — a node's context entry isn't in the workflow's context: list, or the workflow lists a context key the trigger doesn't establish. Add it to context: or pick a trigger that establishes it.

  • Required input not connected — wire it, or set a fixed value under nodes.<id>.inputs.<port> instead (never both on the same port).

  • Graph structural errors — a cycle outside a loop's own return edge, a nested map, a node claimed by two loop bodies, or map.item feeding a node outside its body. These come from the same loop-body checks the interpreter enforces at runtime; see Loop with map for the model.

  • Invalid trigger filter (Sélecteur du déclencheur invalide, colonne <n> : …) — the workflow's trigger.selector does not compile, is empty, or reads no field of event. Fix the expression, or remove selector to receive every event of the type. See JEXL expressions. The load problems of the loaded plugins, printed under ✗ plugins : <n> problème(s) de chargement :, and their fixes:

  • « D » en 0.3.0, hors de la plage « ^0.2.0 », plugin non chargé. … (kind dependency) — the version of the dependency found is outside the plugin's dependsOn range. Load a version of the dependency in the range. When the version found is above the range, the message also proposes ^<version found>: declare it once the plugin is reviewed against that version, and bump the plugin's version. meridian-plugin deps lists each dependency and its state.

  • plage « latest » de « D » invalide, plugin non chargé. … — the range is not a semver range. Declare one, such as ^<major>.<minor>.0 of the version you test with.

  • « P » se cite dans dependsOn, plugin non chargé. … — the plugin names itself in dependsOn. Remove that entry.

  • dépendance « D » absente, … — the dependency is found neither in node_modules nor in a sibling directory; the line under the problem says where the CLI looked. Install it, place its directory next to the plugin's, or pass it with --plugin.

  • dépendance « D » refusée au chargement, … — the dependency, found with a version in the range, is refused for its contract or one of its own ranges; its own problem is listed before. Fix it first.

  • « eventType » exclu avec « event », qui le fournit. Retirer « eventType ». (same for establishes and example) — a trigger with event: also writes a key the event provides. Remove it from the node.

  • sortie « x » : « y » absent du contrat de « E ». … — a declarative trigger's output (or its from) names no contract key. Point from at a declared field, a context id or occurredAt, or declare the field on the event.

  • événement « E » inconnu dans la fermeture dependsOn. … — the trigger references an event that neither its plugin nor its dependsOn closure declares, or the declaring plugin was not loaded. Add that plugin to dependsOn (and to --plugin when it is not resolvable through node_modules), or fix the type.

  • plage sdk « … » inférieure à 0.1.1, requise par les événements déclarés. … or plage sdk absente, … — declare sdk: "^0.1.1" in the manifest (sdk: "^0.1.3" when the message names the default filter of an event, sdk: "^0.1.4" when it names the description of the reception of a protocol, sdk: "^0.1.5" when it names conditional port rules, sdk: "^0.1.6" when it names connections, sdk: "^0.1.7" when it names port connections or non-HTTP connection types, sdk: "^0.1.8" when it names the outbound connection of an inbound connection type).

  • transport « http » fourni par l'hôte, ignoré. Retirer « http » de « receive.transports ». (kind trigger) — a pinned trigger lists a host transport (http, directory) among its protocol's own transports. The host offers both to every protocol: remove the name.

  • filtre par défaut invalide, retiré. Colonne N : … Corriger « defaultSelector ». — an event's defaultSelector does not compile, is empty, or reads no field of event. The event stays declared without its filter. Fix the expression at the column given.

  • « defaultSelector » se déclare sur l'événement (contributes.events), clé ignorée. Le déplacer sur l'événement « E ». — a trigger with event carries defaultSelector. Move the filter to the event it receives.

  • « defaultSelector » non admis sur un déclencheur épinglé, clé ignorée. … or « defaultSelector » non admis sur un nœud « K », clé ignorée. … — a pinned trigger or a node that is not a trigger carries defaultSelector. Remove the key.

  • clé inconnue, ignorée : la valeur de « description » est coupée à une virgule. Mettre cette valeur entre guillemets. (kind key, named by the path of the key, such as contributes.nodes[0].outputs[1]["émis si l'événement le porte."]) — in a { … } mapping, an unquoted comma ends the value of the key the message names and the rest becomes a key. Quote the value.

  • clé inconnue du contrat plugin 0.1.8, ignorée. Corriger ou retirer la clé. (kind key, named by the path of the key, such as contributes.nodes[3].config[0].option) — the manifest carries a key its schema does not know, often a typo, or a key of a newer plugin contract. Fix or remove it.

  • « input » exclu avec « inputs », clé ignorée. Retirer « input ». or « input » : nom de type attendu, clé ignorée. Retirer « input ». (kind key, also output) — an agent writes the input shorthand next to its inputs list, or with a value that is not a type name. The agent keeps its list. Remove the shorthand.

  • binding « b » ne produit pas « encounterId », requis par « E ». … — an event binding's map does not cover a required field or required context id. Add the key to map (and to require, so records without it start no run).

  • binding « b » : champ « x » absent du contrat de « E ». Déclarer le champ ou retirer « x » de require. — the require of an event or message binding names a field the event does not declare. Declare the field in the event, or fix or remove the name.

  • déclencheur non épinglé sans « event ». Déclarer l'événement et le référencer par « event ». — a trigger without pinnedRouting names its type with eventType, or names none. Declare the event and replace eventType, establishes and example with event: E; see Declare an event.

  • type « X » identique à l'adresse du déclencheur épinglé « T ». Renommer l'événement. — a declared type is the address of a pinned trigger. Rename the event. The plugin that pins the address gets adresse « X » déclarée comme type d'événement par « P », événement refusé. …: change the address, or have the event renamed.

  • événement « E » déjà déclaré par « P ». Retirer l'une des deux déclarations. — another plugin, or the same manifest earlier, declares the type. Remove one of the declarations.

  • plage sdk « R » incompatible avec le contrat plugin 0.1.8 de l'hôte, plugin non chargé. … (kind contract) — the plugin's sdk range does not accept the host's contract: the host does not load it. Declare a range that accepts it.

Every check and its effect at load is listed under Load checks.

For the full mechanics behind every check (context propagation, split-pin resolution, loop-body inference), see Static validation engine.

Limitations

  • Agent nodes aren't projected. The CLI doesn't turn agents: manifest entries into node metadata, so a workflow using an agent.<slug> node reports it as an unknown node type (libs/plugin-cli/src/commands/validate.ts). See Author an agent and Use agents in a workflow.
  • It's static only. A green run confirms nothing about your behaviors, adapters, or live ports at runtime — only that the graph is well-typed and complete. Follow up with Simulate a workflow for a run against sample data, or Run and resolve for the real thing.

Add it to CI

Because the command exits non-zero on any error (libs/plugin-cli/src/cli.ts, main), add it as a plain step in your pipeline — no special integration required:

- run: npm run validate workflows/example.workflow.yaml

Run it once per workflow file you ship, alongside meridian-plugin i18n check (see Translate a plugin) and before building and publishing — a workflow that fails to validate has no reason to reach the registry.

97 documents6 sectionssource : /docs · généré au build