Documentation / Conception / Guides pratiques / Branch with guards and switch

Branch with guards and switch

There is no if node in the DSL. To make a run take different paths depending on data, wire one of two gateway node types — flow.guard stops a branch outright when a condition doesn't hold, flow.switch picks exactly one of several named paths — and lean on absence propagation for the "else" you'd otherwise have to draw by hand: a node with a required input that never arrives is skipped, not errored. When exclusive branches lead to a common step, flow.merge joins them back. This recipe wires these into one branching workflow. For the model behind them, see Branching and absence.

Prerequisites

  • A workflow file to edit — see Your first workflow if you don't have one yet.
  • Comfort with port-to-port wiring; see Wire ports if from/to connections are new to you.

Stop a branch with flow.guard

Use a guard when the rest of a branch should run only if a condition on some data holds. This is the real gate from content/clinical-demo/workflows/bio-result.workflow.yaml:

  1. Add the node with its condition, a JEXL expression over criteria:

    nodes:
      guard_creatinine:
        type: flow.guard
        config:
          condition: criteria.code.code == "creatinine"
    
  2. Wire the tested data to .criteria:

    connections:
      - from: trigger.result
        to: guard_creatinine.criteria
    
  3. Wire .pass to whatever should run only when the condition is true:

    connections:
      - from: guard_creatinine.pass
        to: compute_dfg.result
    

    When the condition is false, the guard emits nothing at all — .pass never fires, so compute_dfg (and everything wired downstream of it) is skipped for that run. There is no second branch to author.

  4. If downstream needs a value other than criteria itself, wire it to the optional .value input — .pass then carries .value instead of .criteria. The same file does this for its second guard:

    nodes:
      guard_alerts:
        type: flow.guard
        config:
          condition: criteria.count > 0
    connections:
      - from: drug_safety.report
        to: guard_alerts.criteria    # tested
      - from: guard_alerts.pass
        to: notify_alert.report      # forwarded on pass
    

    Here guard_alerts.criteria is tested for count > 0, and because nothing is wired to .value, .pass forwards criteria itself (the report) to notify_alert.

flow.guard's type/valueType config fields (the types of criteria and value) are marked as inferred hints in the catalog (libs/shared/src/engine/catalog-metadata.ts) — the visual editor fills them in as soon as you wire .criteria/.value and shows them read-only. When hand-editing YAML, you don't need to set them at all; only condition is yours to write.

A guard needs a condition. Static validation refuses a guard whose condition is absent or blank (Nœud « … » : condition vide.), unless a connection feeds the condition config field. It also refuses a condition that does not compile or reads a variable other than criteria, value and context, with the column of the error:

Nœud « seuil » : condition, colonne 26 : « , » inattendu. Séparateur décimal : le point (6.0).

In the visual editor, the condition field shows these problems while you type.

Filter or guard

A trigger filter and a guard can test the same fact, at two different moments:

  • the trigger filter (trigger.selector) decides whether the run starts at all. It reads the event as received, bound to event. An event that no filter keeps starts no run: POST /api/events answers 200 with runs: [], and nothing appears in the executions list;
  • a guard stops a branch inside a run that has already started. It reads the outputs of the trigger or of other nodes.

On a lab result, criteria.code.code == "creatinine" on the trigger's result output (an Observation) tests what event.analyte == "creatinine" tests in a filter. Put the condition in the filter when the workflow has nothing to do otherwise; keep a guard when other branches of the run must go on. See Choose which events start a workflow and Events and triggers — what a filter reads.

Route to one of several cases with flow.switch

Use a switch when there are more than two outcomes, or you want them named instead of just pass/no-pass. This is the real gate from content/clinical-demo/workflows/analyse-posos.workflow.yaml:

  1. Add the node. Set config.type to the object type flowing through .value, and config.cases to an ordered list of { name, condition }, each condition a JEXL expression over value:

    nodes:
      check_alerts:
        type: flow.switch
        config:
          type: Integer
          cases:
            - name: critical
              condition: value > 0
    
  2. Wire the routed data to .value:

    connections:
      - from: interactions.count
        to: check_alerts.value
    
  3. Wire each case's output — one port per cases[].name — to its own downstream node:

    connections:
      - from: check_alerts.critical
        to: make_alert_obs.after
    

    A switch also always exposes a .default output, emitted when no case matches. Leaving it unwired is fine — as in this file, which has nothing to do when there's no critical count.

The switch tests cases in order and routes to the first whose condition is true; put the more specific conditions first. Exactly one output fires per run (a case, or default); everything wired to the other output ports is skipped — the same absence propagation the guard relies on. Case names become port names, so keep them to identifiers (letters, digits, _, - — the editor sanitizes typed names to this set).

Static validation refuses a case:

  • without a name, or whose name has other characters than letters, digits, _ and -;
  • named default or done, which are already output ports of the node;
  • whose name repeats an earlier case;
  • without a condition, or whose condition does not compile or reads a variable other than value and context. The error names the case: Nœud « check_alerts » : cas « critical », condition, colonne 9 : ….

cases itself must be a list; absent, every value goes to default. The editor checks each case condition while you type, like the guard's.

Like the guard, flow.switch's config.type is an inferred hint, filled in by the editor once .value is wired; set it by hand only when authoring YAML directly.

Join exclusive branches with flow.merge

Use a merge when exclusive branches — the cases of a switch, or a guard and its opposite — each produce a value of the same type for one common step. Wiring every branch to the same input does not work: an input reads only its first connection. The merge reads one input per branch and emits the first one present.

  1. Add the node. Set config.type to the type of the joined values, and config.count (2 to 8, default 2) when there are more than two branches:

    nodes:
      note:
        type: flow.merge
        config:
          type: String
    
  2. Wire each branch to its own input, value1, value2…:

    connections:
      - from: note_high.value      # branch of the "high" case
        to: note.value1
      - from: note_low.value       # branch of the "low" case
        to: note.value2
    
  3. Wire .value to the common step, which then runs once, with the value of the branch taken:

    connections:
      - from: note.value
        to: send_note.body
    

The merge emits the first present input in the order value1, value2… When no branch produced a value (a switch that took default, a guard that stopped), it emits neither value nor done: send_note is skipped, like any node whose required input never arrives. When two inputs are present at once, value1 wins; the order is the order of the inputs, not the order in which the branches ran.

Branches that produce no value join the same way: wire each branch's done pin to its own input, set config.type to Boolean (done carries true), and wire the merge's done to the common step's .after. The step then runs once when any of the branches ran.

An input can also carry a fixed value (inputs:) as the fallback when no branch produces one. A fixed value is always present, so it goes on the last fed input: static validation refuses a fixed input followed by a fed one.

Static validation also refuses a merge with fewer than two fed inputs, a count outside 2 to 8, a missing type, and a merge outside a flow.map body fed by a node of that body (the messages are listed in Core primitives — flow.merge). In the visual editor, the first input you wire sets type when its source has a type (a done pin or a webhook body has none); the Value type field of the inspector sets or changes it. The Number of inputs field sets count; lowering it removes the connections and fixed values of the inputs that go away.

Let absence be the else you don't write

A required input a run never receives — because the node upstream chose not to emit it — leaves the node skipped, not errored. This is the same mechanism behind an unmatched guard or switch case, and it composes with any compute node for free. From external-plugins/demo/behaviors/clinical.ts:

export const detectConditionFromEgfr: BehaviorFor<"clinical.condition.detect-from-egfr"> = () => ({
  run: ({ inputs }) => {
    const proposal = suspectedConditionFromEgfr(inputs.egfr);
    return proposal ? { proposal } : {}; // no proposal → branch stopped
  },
});

detect_condition only emits proposal when eGFR is low enough to suspect a condition. Wire straight through, with nothing extra:

connections:
  - from: detect_condition.proposal
    to: confirm_condition.proposal
  - from: confirm_condition.condition
    to: record_condition.condition

When no proposal is emitted, confirm_condition and record_condition are both skipped for that run — there is no else branch to author.

Absence only takes over at runtime. A required input still needs a connection (or a fixed value) at authoring time: leaving guard_alerts.criteria or check_alerts.value unwired is a static validation error — Nœud « … » : entrée requise « … » non connectée. (libs/engine-core/src/engine/validate.ts) — not something absence propagation will paper over. Wire it, and let the run decide whether data actually shows up.

Verify both paths run

  • Validate the workflow to confirm every required input is wired and every connected type matches.
  • Simulate the workflow with inputs on each side of your guard's condition, and one per switch case, then check the node statuses: the branch you expect should read done, the others skipped.
97 documents8 sectionssource : /docs · généré au build