Documentation / Développement / Guides pratiques / Declare a node's configuration and ports

Declare a node's configuration and ports

Use this recipe when a node's configuration has structure — a list of fields to extract, questions with their possible answers — and its ports depend on it. The manifest carries everything the core needs: it validates the configuration, computes the ports in the editor, the static validation and the interpreter, and draws the editor's form, without any code of your plugin running there. The full grammar is in Declared configuration and ports.

1. Describe each configuration field

Give each structured field a schema, a subset of JSON Schema (draft-07). Mark it typeHint: true when it is a contract rather than a business parameter — then it can be neither exposed as a port nor wired — and required: true when it must be set.

config:
  - name: questions
    type: { kind: any }
    typeHint: true
    required: true
    schema:
      type: array
      title: Questions
      minItems: 1
      x-uniqueBy: name            # no two questions with the same name
      items:
        type: object
        required: [name, type, instructions]
        discriminator: { propertyName: type }
        oneOf:
          - title: Oui / non
            properties:
              name: { type: string, title: Nom, x-widget: identifier }
              type: { const: yesno, title: Type }
              instructions: { type: string, title: Énoncé, minLength: 1, x-widget: textarea }
            additionalProperties: false
          - title: Choix
            properties:
              name: { type: string, title: Nom, x-widget: identifier }
              type: { const: choice, title: Type }
              instructions: { type: string, title: Énoncé, minLength: 1, x-widget: textarea }
              options:
                type: object
                title: Options
                minProperties: 2
                additionalProperties: { type: [string, "null"] }
            required: [options]
            additionalProperties: false

This excerpt comes from the decision.ask node of @posos/decision (external-plugins/decision/plugin.yaml), a complete example with a score variant and derived inputs.

The editor renders it: a card per question, a selector for its variant, a table for the options, a text area for the wording. title and description label and explain each field; x-widget: typeName gives a type selector and x-widget: identifier restricts the input to a port-name alphabet.

When a config field is itself one object chosen among variants (discriminator + oneOf at the field's top level, as for the operation of a node that offers several), the node's card in the graph shows the title of the selected variant under the node label. Give each variant a short title naming the operation (Filtrer, Trier): without it, every card of that node type reads the same.

2. Derive the ports

Declare derivedPorts instead of inputs/outputs. One rule gives one port, or one port per item of a list with forEach:

derivedPorts:
  outputs:
    - forEach: questions
      name: { from: name }
      description: { from: instructions }
      type:
        byValue: { from: type }
        cases:
          yesno: Decimal
          choice: { kind: record, name: DecisionChoice, fields: { choice: { type: { kind: primitive, name: String }, required: true } } }
    - { name: model, type: String, description: Modèle qui a répondu. }

A name that is invalid, duplicated or reserved (after, done) gives no port; static validation names it. For a record whose fields come from a list — the HL7 and PN13 mapping nodes — use { listOf: { record: { name, forEach, field } } }.

A family of operations can use one discriminated oneOf config schema and conditional ports. Declare sdk: "^0.1.5" and add when to each relevant rule:

derivedPorts:
  inputs:
    - name: items
      type: { listOf: { fromConfig: type } }
    - name: other
      type: { listOf: { fromConfig: type } }
      when: { value: { fromConfig: operation.kind }, in: [concat] }

Here other exists only for concatenation. The editor, static checker, execution, simulation and workflow assistant consume the same rule. Keep operation marked typeHint: true: the port contract must be known before execution.

3. Add host-side assistance (optional)

When the configuration needs code to be checked or tested, point assist at a module exporting a NodeAssist:

import type { NodeAssist } from "@meridian/shared/plugin/assist";

export const assist: NodeAssist = {
  // Rules beyond the schema: run at save, at publication and in the editor.
  validateConfig: (config) => (config.questions as unknown[]).length > 50 ? ["50 questions au plus."] : [],
  // Values for the fields marked `x-suggest: <key>`, taken from the sample.
  suggest: (key, sample) => (key === "sourcePath" ? sourcePaths(sample).map((value) => ({ value })) : []),
  // The result shown by the editor's workshop.
  preview: (sample, config) => apply(sample, config),
};

With preview, the editor opens a workshop: sample on the left, configuration in the middle, result on the right. assist.example pre-fills it, and requirePreview: true applies a configuration only after a successful test. Nothing about the sample is stored.

4. Check it

  • node --import tsx libs/plugin-cli/src/cli.ts validate <workflow.yaml> --plugin <dir> checks a workflow against the schemas and the port rules (the plugin's own checks run in the host only).
  • In a unit test, validateSpec (@meridian/engine-core/engine/validate) and nodePorts (@meridian/shared/engine/dynamic-ports) give the errors and the ports the console will show.
  • meridian-plugin i18n extract --locale en adds the keys of the schema titles, descriptions and port descriptions to the English bundle.

Reuse the expression language

For a plugin calculation, import compiledExpression from @meridian/shared/engine/expression and expressionProblems from @meridian/shared/engine/expression-check. These are the same JEXL evaluator, transforms and checker used by guards, selectors and conversions. Pass the allowed roots to the checker (for example ["inputs"]), then provide only the explicit input scope to evalSync. Validate the result against the declared port type with validateValue; undefined is an absent output, not JSON data. Do not add a JavaScript evaluator or a separate expression grammar.

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