# Workflow node reference

## ActionNode

The ActionNode executes actions on connections, triggering them based on configurable conditions: on startup, when data changes, or when an event is received.

### Features

- **Flexible Triggering**: Trigger on startup, data change, or incoming event
- **Dynamic Options**: Action options are validated against the action's own schema
- **Input Overrides**: Connection ID and action key can be overridden from inputs
- **Change Detection**: Tracks previous data to only fire when inputs actually change

### Usage

#### Trigger on Event

Connect a trigger signal to the `trigger` input and enable `triggerOnEvent`. The action fires each time a trigger event arrives.

#### Trigger on Change

Enable `triggerOnChange`. The action fires whenever any input value changes.

#### Trigger on Startup

Enable `triggerOnStartup`. The action fires once when the workflow starts.

### Node Structure

#### Config Schema

- `connectionId` (string, optional): ID of the connection to run the action on
- `actionKey` (string, optional): Key identifying the action to execute
- `triggerOnEvent` (boolean, optional): Fire when a trigger event is received
- `triggerOnStartup` (boolean, optional, default: `false`): Fire on workflow startup
- `triggerOnChange` (boolean, optional, default: `false`): Fire when inputs change
- Additional action-specific option fields

#### Input Schema

- `trigger` (optional): Trigger signal — fires the action when an event arrives
- `connectionId` (string, optional): Override the configured connection ID
- `actionKey` (string, optional): Override the configured action key
- Additional passthrough fields for action options

#### Output Schema

- *(no outputs)*

### Implementation Details

- Merges input values with config values; inputs take precedence
- Validates action option inputs dynamically against the action definition's schema
- Executes via RPC with a 5000ms timeout
- Subscribes to action feedback after execution

### Integration

The ActionNode is registered in:

- `NodeRegistry` as `action`
- `NodeTypeSchema` as `'action'`

### Example Workflow Usage

1. **PTZ Camera Control**: Connect a button press to `trigger`, configure the connection and action key for pan/tilt
2. **Startup Initialisation**: Enable `triggerOnStartup` to send an init command when the workflow loads
3. **Reactive Control**: Enable `triggerOnChange` and wire a variable so the action fires whenever the variable changes

---

## ButtonNode

The ButtonNode provides a clickable UI button inside the workflow editor. Pressing it emits a trigger event that can be used to kick off downstream logic.

### Features

- **UI Interaction**: Button is clickable directly in the workflow editor canvas
- **Event Emission**: Publishes a `trigger` event on press, driving downstream nodes
- **Timestamp Output**: Exposes the timestamp of the last press as a state value

### Usage

Place a ButtonNode on the canvas and connect its `timestamp` output (or `trigger` event handle) to any node that accepts a trigger input, such as an ActionNode or DelayNode.

### Node Structure

#### Config Schema

- *(no configuration)*

#### Input Schema

- *(no inputs)*

#### Output Schema

- `timestamp` (number): Unix timestamp (ms) of the most recent button press

### Implementation Details

- Responds to the `press` UI action
- Publishes a `trigger` event immediately on press
- Calls `workflow.rerenderFromNode()` so downstream nodes update synchronously

### Integration

The ButtonNode is registered in:

- `NodeRegistry` as `button`
- `NodeTypeSchema` as `'button'`

### Example Workflow Usage

1. **Manual Trigger**: Use as a manual start button to kick off a sequence of actions
2. **Test Harness**: Wire a ButtonNode to any chain to manually drive it during development

---

## CaseSwitchNode

The CaseSwitchNode is a workflow node that acts as a conditional router, passing through input values based on a conditional match. It's similar to a switch statement in programming languages.

### Features

- **Conditional Routing**: Routes input based on a conditional value match
- **Dynamic Input Cases**: Supports multiple input cases with configurable conditions
- **First Match Wins**: Returns the first input case that matches the condition
- **No Match Handling**: Returns undefined when no condition matches
- **Default Fallback**: Optional `default` input is returned when no case matches

### Usage

#### Basic Switch

Conditional Input: "yes"
Cases:
- case1: condition="yes", input="Hello"
- case2: condition="no", input="Goodbye"

Result: "Hello" (first match)


#### Multiple Cases

Conditional Input: "red"
Cases:
- case1: condition="red", input="Stop"
- case2: condition="yellow", input="Caution"
- case3: condition="green", input="Go"

Result: "Stop" (matches "red")


#### No Match (with Default)

Conditional Input: "blue"
Cases:
- case1: condition="red", input="Stop"
- case2: condition="yellow", input="Caution"
Default: input="Unknown"

Result: "Unknown" (no case matched, returned default)


### Node Structure

#### Input Schema

- `conditional` (required): The value to match against case conditions
- `case1`, `case2`, ..., `caseN` (dynamic): Input values for each case
- `default` (optional): Fallback value used when no case matches

#### Config Schema

- `label` (optional): Custom label for the node
- `enabled` (default: `true`): Whether the node is enabled
- `inputVariables` (array): Array of case configurations
    - `name` (string): Case name (used as input handle ID)
    - `description` (optional): Case description
    - `condition` (string): Condition value to match against

#### Output Schema

- `result`: The matched input value (or undefined if no match)
- `matched` (boolean): Whether a match was found
- `matchedIndex` (number, optional): Index of the matched case

### Implementation Details

The CaseSwitchNode works by:

1. Converting the conditional input to a string
2. Iterating through all configured cases in order
3. Comparing each case's condition with the conditional value
4. Returning the first matching case's input value
5. If no match is found, returning undefined (or the `default` input if provided)

### Integration

The CaseSwitchNode is registered in:

- `NodeRegistry` as `caseSwitch`
- `NodeTypeSchema` as `'caseSwitch'`

### Example Workflow Usage

1. **Status Router**: Route different status messages based on a status code
2. **Language Selector**: Route content based on language preference
3. **Error Handler**: Route different error responses based on error type
4. **Menu System**: Route to different menu options based on user selection

### Comparison with Other Nodes

- **LogicNode**: Performs boolean operations, CaseSwitchNode routes based on value matching
- **WorkflowRouter**: Routes entire workflows, CaseSwitchNode routes individual values
- **ExpressionNode**: Evaluates expressions, CaseSwitchNode performs conditional routing

---

## ChangeSwitchNode

The ChangeSwitchNode watches a boolean input and records timestamps whenever the value transitions between `true` and `false`. It acts as a change detector with separate outputs for each transition direction.

### Features

- **Change Detection**: Only updates outputs when the boolean value actually changes
- **Dual Timestamp Outputs**: Separate outputs for the last `true` transition and last `false` transition
- **State Preservation**: Remembers the last value and timestamps across renders

### Usage

Connect a boolean signal to `valueA`. The `trueResult` output updates with the current timestamp each time the value transitions to `true`, and `falseResult` updates when it transitions to `false`.

Input: false → true → true → false
trueResult:  [updated]        [unchanged]
falseResult: [unchanged]      [updated]


### Node Structure

#### Config Schema

- *(no configuration)*

#### Input Schema

- `valueA` (boolean, optional): Boolean value to monitor

#### Output Schema

- `trueResult` (number): Timestamp (ms) of the last transition to `true`
- `falseResult` (number): Timestamp (ms) of the last transition to `false`

### Implementation Details

- Stores `lastValue`, `lastTrueResult`, and `lastFalseResult` in node state
- Compares incoming value to `lastValue`; if identical, outputs are unchanged
- Does **not** emit events — use ChangeSwitchNodeV2 if event-driven behaviour is needed

### Comparison with ChangeSwitchNodeV2

| Feature | ChangeSwitchNode | ChangeSwitchNodeV2 |
| --- | --- | --- |
| State-based output | ✓ | ✓ |
| Emits events on change | ✗ | ✓ |

### Integration

The ChangeSwitchNode is registered in:

- `NodeRegistry` as `changeSwitch`
- `NodeTypeSchema` as `'changeSwitch'`

### Example Workflow Usage

1. **Edge Detection**: Detect rising and falling edges of a boolean feedback value
2. **Timestamp Logging**: Record when a signal last went high or low

---

## ChangeSwitchNodeV2

The ChangeSwitchNodeV2 is the event-emitting version of ChangeSwitchNode. It watches a boolean input and both updates timestamp outputs **and** emits events whenever the value transitions between `true` and `false`.

### Features

- **Change Detection**: Only reacts when the boolean value actually changes
- **Dual Timestamp Outputs**: Separate outputs for the last `true` and `false` transitions
- **Event Emission**: Publishes `trueResult` or `falseResult` events on each transition, enabling reactive downstream processing

### Usage

Connect a boolean signal to `valueA`. On each `true` transition a `trueResult` event fires; on each `false` transition a `falseResult` event fires. Both events carry the current timestamp.

Input: false → true  → true → false
Events emitted:  trueResult        falseResult


### Node Structure

#### Config Schema

- *(no configuration)*

#### Input Schema

- `valueA` (boolean, optional): Boolean value to monitor

#### Output Schema

- `trueResult` (number): Timestamp (ms) of the last transition to `true`
- `falseResult` (number): Timestamp (ms) of the last transition to `false`

#### Output Events

- `trueResult`: Emitted when value transitions to `true`
- `falseResult`: Emitted when value transitions to `false`

### Implementation Details

- Behaves identically to ChangeSwitchNode with the addition of event publishing
- Calls `workflow.rerenderFromNode()` on each transition so downstream nodes react immediately
- Both output handles are event handles (`isOutputHandleEvent()` returns `true` for both)

### Comparison with ChangeSwitchNode

| Feature | ChangeSwitchNode | ChangeSwitchNodeV2 |
| --- | --- | --- |
| State-based output | ✓ | ✓ |
| Emits events on change | ✗ | ✓ |

### Integration

The ChangeSwitchNodeV2 is registered in:

- `NodeRegistry` as `changeSwitchV2`
- `NodeTypeSchema` as `'changeSwitchV2'`

### Example Workflow Usage

1. **Trigger on Rising Edge**: Connect the `trueResult` event to an ActionNode to fire an action only when the signal goes high
2. **Toggle Animations**: Use `trueResult` / `falseResult` events to start/stop different sequences

---

## ClampNode

The ClampNode constrains a numeric value to stay within a configured minimum and maximum range.

### Features

- **Value Clamping**: Ensures the output never exceeds `max` or falls below `min`
- **Configurable Bounds**: Both bounds are set in config and validated (`min` must be ≤ `max`)
- **Type Coercion**: String inputs are coerced to numbers before clamping

### Usage

min: 0, max: 100, value: 150  →  result: 100
min: 0, max: 100, value: -10  →  result: 0
min: 0, max: 100, value: 50   →  result: 50


### Node Structure

#### Config Schema

- `min` (number, default: `0`): Lower bound
- `max` (number, default: `1`, must be ≥ `min`): Upper bound

#### Input Schema

- `value` (unknown, coerced to number, optional): Value to clamp

#### Output Schema

- `result` (number): Clamped value

### Implementation Details

- Uses `Math.min(Math.max(value, min), max)`
- Validation rejects configs where `min > max`

### Integration

The ClampNode is registered in:

- `NodeRegistry` as `clamp`
- `NodeTypeSchema` as `'clamp'`

### Example Workflow Usage

1. **Volume Control**: Clamp a volume level to 0–100 before sending it to a mixer
2. **Safe Range**: Ensure a position or speed value never exceeds hardware limits
 
---

## ConnectionVariableNode

The ConnectionVariableNode reads the current value of a variable from a specific connection and subscribes to live updates whenever that variable changes.

### Features

- **Live Variable Subscription**: Automatically updates whenever the connection variable changes
- **Reactive**: Triggers a workflow re-render on each update
- **Config-Driven**: Connection ID and variable key are set in config and can be changed at runtime

### Usage

Configure the `connectionId` and `variableKey`. The `value` output will reflect the current value of that variable, updating automatically as the connection reports changes.

### Node Structure

#### Config Schema

- `connectionId` (string): ID of the connection to read from
- `variableKey` (string): Key of the variable to read

#### Input Schema

- *(no inputs)*

#### Output Schema

- `value` (string | number | boolean | null | undefined): Current value of the variable

### Implementation Details

- Subscribes to variable changes via the system router
- Caches the current config; when config changes, cleans up the old subscription and creates a new one
- Fetches the current value on each render to ensure freshness
- Cleans up the subscription on node unmount

### Integration

The ConnectionVariableNode is registered in:

- `NodeRegistry` as `connectionVariable`
- `NodeTypeSchema` as `'connectionVariable'`

### Example Workflow Usage

1. **Live Status**: Read a mixer fader level variable and feed it into a LogicNode or DisplayNode
2. **Reactive Logic**: Drive conditional behaviour based on a connection's current variable state
3. **Cross-Node Data**: Pipe a connection variable into a MathNode or ExpressionNode for further processing

---

## CounterNode

The CounterNode maintains a persistent numeric counter that can be incremented, decremented, set, or reset through both state inputs and events.

### Features

- **Full Counter Operations**: Increment by 1, decrement by 1, add an amount, subtract an amount, set to a value, or reset to the initial value
- **Event and State Support**: Operations can be driven by both state inputs and event signals
- **Configurable Initial Value**: Set the starting value for the counter and for resets
- **UI Actions**: Supports direct increment/decrement/set actions from the editor UI

### Usage

initialValue: 0

increaseOne event  →  value: 1
increaseOne event  →  value: 2
decreaseOne event  →  value: 1
set input: 10      →  value: 10
reset event        →  value: 0


### Node Structure

#### Config Schema

- `initialValue` (string | number, coerced to number, default: `0`): Starting value and reset target

#### Input Schema

- `set` (string | number, optional): Set the counter to this value
- `reset` (string | number | boolean, optional): Reset to `initialValue` when truthy
- `increaseOne` (string | number | boolean, optional): Increment by 1 when truthy
- `increaseAmount` (string | number, optional): Add this amount to the counter
- `decreaseOne` (string | number | boolean, optional): Decrement by 1 when truthy
- `decreaseAmount` (string | number, optional): Subtract this amount from the counter

#### Input Events

- `increaseOne`: Increment by 1
- `decreaseOne`: Decrement by 1
- `increaseAmount`: Add the event payload value
- `decreaseAmount`: Subtract the event payload value
- `set`: Set to the event payload value
- `reset`: Reset to `initialValue`

#### Output Schema

- `value` (number): Current counter value

#### Output Events

- `value`: Emitted whenever the counter value changes

### Implementation Details

- Tracks `lastSeenInputs` to detect state changes between renders
- Operations from both state inputs (on change detection) and events are supported
- The counter value is persisted in node state between renders

### Integration

The CounterNode is registered in:

- `NodeRegistry` as `counter`
- `NodeTypeSchema` as `'counter'`

### Example Workflow Usage

1. **Tally Counter**: Count button presses or event occurrences
2. **Loop Index**: Use with an IntervalNode to increment a counter each tick and reset at a limit
3. **Score Tracker**: Increment/decrement a score based on feedback events

---

## DelayNode

The DelayNode passes a payload value through after a configurable time delay. Multiple payloads can be in-flight simultaneously, each delayed by the same duration.

### Features

- **Configurable Delay**: Set the delay in milliseconds (0 ms – 10 minutes)
- **Input Override**: Delay duration can be overridden at runtime via the `delay` input
- **Buffered Payloads**: Multiple events arriving in quick succession are each delayed independently
- **Timeout Management**: Pending timeouts are cleared if the delay config changes

### Usage

delay: 1000ms, payload: "hello"

After 1 second → passthrough: "hello"


delay: 500ms, payloads arrive at t=0 and t=200ms

t=500:  "first payload" emitted
t=700:  "second payload" emitted


### Node Structure

#### Config Schema

- `delay` (number, min: `0`, max: `600000`, default: `1000`): Delay in milliseconds

#### Input Schema

- `delay` (number, optional): Overrides config delay for this render
- `payload` (unknown, optional): Value to delay

#### Input Events

- `payload`: Queues the carried value to be emitted after the delay

#### Output Schema

- `passthrough` (unknown): The payload value, emitted after the delay

### Implementation Details

- Each incoming `payload` event schedules a `setTimeout` for the configured delay
- When the delay config changes, all pending timeouts are cancelled and the array is cleared
- Timeout handles are stored in an array and cleaned up on node unmount

### Integration

The DelayNode is registered in:

- `NodeRegistry` as `delay`
- `NodeTypeSchema` as `'delay'`

### Example Workflow Usage

1. **Debounce**: Add a short delay before an action to avoid triggering on transient signals
2. **Timed Sequence**: Chain multiple DelayNodes to create a timed sequence of actions
3. **Hold Signal**: Delay a feedback value by a fixed offset to synchronise it with another signal

---

## DisplayNode

The DisplayNode is a simple passthrough node for visualising a value inside the workflow editor. It caches and forwards whatever value it receives, making it useful for debugging and monitoring.

### Features

- **Value Visualisation**: Displays the current value directly on the node in the editor canvas
- **Event Support**: Responds to `value` events as well as state updates
- **Passthrough Output**: Forwards the received value downstream unchanged
- **Configurable Label**: The display label can be customised

### Usage

Connect any output to the `value` input. The current value is shown on the node and is also available as an output for further processing.

### Node Structure

#### Config Schema

- `label` (string, optional, default: `'Value'`): Label shown on the node
- `enabled` (boolean, default: `true`): Whether the node is active

#### Input Schema

- `value` (unknown): The value to display and pass through

#### Output Schema

- `value` (unknown): The input value, forwarded unchanged

### Implementation Details

- Caches the most recent value in an internal property
- Responds to `value` events via `onEvent`, updating the cache and re-rendering
- Primarily intended for UI observability during workflow development

### Integration

The DisplayNode is registered in:

- `NodeRegistry` as `display`
- `NodeTypeSchema` as `'display'`

### Example Workflow Usage

1. **Debug Monitor**: Insert between two nodes to inspect intermediate values during development
2. **Status Display**: Wire a connection variable to a DisplayNode to show its current value on the canvas

---

## EventGateNode

The EventGateNode is an event-driven gate that passes events through only when open, and blocks them when closed. It supports both state-based and event-based control modes.

### Features

- **Event Gating**: Blocks or passes events based on gate state
- **Two Control Modes**: Gate state can be driven by a continuous boolean input (state mode) or by discrete open/close/toggle events (event mode)
- **Configurable Initial State**: The gate can start open or closed
- **Pass-through Output**: Forwards the last allowed event payload as a state value

### Usage

#### State Mode

Set `controlMode` to `'state'` and connect a boolean to the `open` input. Events pass through whenever `open` is truthy.

#### Event Mode

Set `controlMode` to `'event'` and send `open`, `close`, or `toggle` events to control the gate. An `event` input event passes a payload through only if the gate is currently open.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `controlMode` (`'state'` | `'event'`, default: `'event'`): How the gate is controlled
- `initiallyOpen` (boolean, default: `true`): Starting open/closed state

#### Input Schema

- `open` (boolean | string | number, optional): Gate control signal (state mode)

#### Output Schema

- `value` (unknown, optional): Last payload that passed through the gate
- `isOpen` (boolean): Current open/closed state of the gate

#### Output Events

- `out`: Emitted when a payload passes through the gate

#### Input Events (event mode)

- `open`: Open the gate
- `close`: Close the gate
- `toggle`: Toggle the gate state
- `reset`: Reset to the configured initial state
- `event`: Pass a payload through (only if gate is open)

### Implementation Details

- In event mode, gate state persists between renders
- `isOutputHandleEvent()` returns `true` for the `out` handle
- In state mode, gate state is derived directly from the `open` input each render

### Integration

The EventGateNode is registered in:

- `NodeRegistry` as `eventGate`
- `NodeTypeSchema` as `'eventGate'`

### Example Workflow Usage

1. **Conditional Event Routing**: Only allow trigger events through when a feedback state is active
2. **One-shot Gate**: Open the gate via an event, let one payload through, then close it
3. **Arming Circuit**: Open the gate only when a specific condition is met, preventing accidental action triggers

---

## EventOrderNode

The EventOrderNode validates that a set of events arrives in a specific sequential order. It emits a `fulfilled` event only when all events have been received in the correct sequence.

### Features

- **Ordered Sequence Validation**: Tracks whether events arrive in the configured order (event1 → event2 → … → eventN)
- **Two Modes**: Choose between ignoring out-of-order events (Wait) or resetting the sequence (Reset)
- **Configurable Sequence Length**: Supports sequences of 2–20 events
- **Completion Event**: Emits `fulfilled` when the full sequence is correctly completed

### Usage

Connect event sources to the numbered event inputs in order. Trigger `event1` first, then `event2`, and so on. When the final event in the sequence arrives, the `fulfilled` event fires.

Sequence: event1 → event2 → event3

Correct:  event1 ✓ → event2 ✓ → event3 ✓ → [fulfilled emitted]
Wrong (Reset mode): event1 ✓ → event3 ✗ → [reset, start over]
Wrong (Wait mode):  event1 ✓ → event3 ✗ → [ignored, still waiting for event2]


### Node Structure

#### Config Schema

- `mode` (`'Wait'` | `'Reset'`, default: `'Reset'`): Behaviour when an out-of-order event is received
- `numberOfInputs` (number, min: 2, max: 20, default: 10): Length of the required sequence

#### Input Schema

- `event1` … `event10` (fixed set): Event inputs in sequence order

#### Output Schema

- `state` (boolean): Whether the full sequence has been completed
- `next` (number): Index of the next expected event in the sequence

### Implementation Details

- Maintains `expectedEventIndex` and `currentState` in node state
- **Wait mode**: Out-of-order events are silently ignored
- **Reset mode**: Out-of-order events reset `expectedEventIndex` to 0
- On sequence completion, publishes the `fulfilled` event and resets for the next cycle

### Integration

The EventOrderNode is registered in:

- `NodeRegistry` as `eventOrder`
- `NodeTypeSchema` as `'eventOrder'`

### Example Workflow Usage

1. **Combination Lock**: Require a specific sequence of button presses before triggering an action
2. **Multi-step Handshake**: Validate that a set of network or device events arrive in the correct protocol order
3. **Safety Interlock**: Only allow an action when a required startup sequence has completed in order

---

## EventSwitchNode

The EventSwitchNode sets its output value by responding to named events. Each event is mapped to a specific value, making it easy to drive a downstream value from discrete event signals.

### Features

- **Event-Driven Value Selection**: Each named event sets the output to its configured value
- **Configurable Event Map**: Any number of events can be defined, each with a name and value
- **Reset Support**: A `reset` event returns the output to the initial value
- **Persistent Value**: The current value is stored and survives re-renders

### Usage

Define a set of events and their corresponding values in the config. When any of those events fires, the output value updates to the mapped value.

Events config:
- name: "on",  value: true
- name: "off", value: false

Fire "on"  → output: true
Fire "off" → output: false
Fire "reset" → output: <initialValue>


### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`): Whether the node is active
- `initialValue` (unknown, optional): Value to use before any event fires, and after a `reset`
- `events` (array of `{name, value}`, default: `[]`): The event-to-value mappings

#### Input Schema

- *(no inputs)*

#### Output Schema

- `value` (unknown): The current value as set by the most recent event

#### Input Events

- `reset`: Reset the output to `initialValue`
- `event_<name>`: Set the output to the value mapped to `<name>`

### Implementation Details

- Caches `currentValue` between renders
- On `onConfigChange`, applies any change to `initialValue`
- Event names in the config are prefixed with `event_` internally

### Integration

The EventSwitchNode is registered in:

- `NodeRegistry` as `eventSwitch`
- `NodeTypeSchema` as `'eventSwitch'`

### Example Workflow Usage

1. **State Machine**: Drive a string output through a set of named states via events
2. **Scene Selector**: Map scene-select button presses to scene name strings
3. **Mode Toggle**: Cycle between operating modes by firing named events

---

## ExpressionNode

The ExpressionNode is a workflow node that evaluates expressions using the companion expression library. It mimics the LogicNode structure but provides more powerful expression evaluation capabilities with configurable inputs.

### Features

- **Expression Evaluation**: Supports complex mathematical and logical expressions
- **Variable Injection**: Can use variables from the workflow context
- **Configurable Inputs**: Supports 1-20 dynamic input variables
- **Expression Mode**: Uses the TextWithVariableInput component in expressionable mode
- **Error Handling**: Provides clear error messages for invalid expressions

### Usage

#### Basic Expression

\_EXP\_(1 + 2)


Result: `3`

#### Using Dynamic Input Variables

\_EXP\_($(var1) + $(var2))


With input variables: `var1: 5, var2: 3`
Result: `8`

#### Using Variables and Dynamic Inputs

\_EXP\_($(var1) > $(threshold) ? 'High' : 'Low')


With input variables: `var1: 15, threshold: 10`
Result: `'High'`

#### String Interpolation

Hello $(var1)!


With input variables: `var1: 'World'`
Result: `Hello World!`

### Node Structure

#### Input Schema

- `expression` (optional): The expression string to evaluate
- `variables` (optional): Variables available to the expression
- `var1`, `var2`, ..., `varN` (dynamic): Input variables based on numOfInputs configuration

#### Config Schema

- `label` (optional): Custom label for the node
- `enabled` (default: `true`): Whether the node is enabled
- `numOfInputs` (default: `1`, min: `1`, max: `20`): Number of dynamic input variables
- `expression` (default: `'_EXP_()'`): The default expression

#### Output Schema

- `result`: The evaluated result (can be any type)

### Implementation Details

The ExpressionNode uses the companion expression library:

- `ParseExpression`: Parses the expression string into an AST
- `ResolveExpression`: Evaluates the parsed expression with variables
- `ExpressionFunctions`: Provides built-in functions for expressions

The node supports two modes:

1. **Expression Mode**: When the input starts with `_EXP_(` and ends with `)`
2. **Variable Injection Mode**: For simple string interpolation with `$(variable)` syntax

### Integration

The ExpressionNode is registered in:

- `NodeRegistry` as `expression`
- `NodeTypeSchema` as `'expression'`

### Example Workflow Usage

1. **Simple Calculator**: Set numOfInputs to 2, expression to `_EXP_($(var1) + $(var2))`
2. **Conditional Logic**: Set numOfInputs to 3, expression to `_EXP_($(var1) > $(var2) ? $(var3) : 'default')`
3. **String Formatting**: Set numOfInputs to 1, expression to `Hello $(var1), the time is $(time.hh:mm:ss)!`

---

## FeedbackNode

The FeedbackNode monitors the state of a feedback source on a connection and exposes it as a boolean output. It subscribes to live updates, so the output reflects the current state at all times.

### Features

- **Live Subscription**: Automatically updates when the feedback state changes on the connection
- **Boolean Output**: Normalises feedback state to `true`, `false`, or `null`
- **Dynamic Options**: Feedback options are validated against the feedback definition's schema
- **Config-Driven Resubscription**: Cleanly resubscribes when connection or feedback key changes

### Usage

Configure the connection ID, feedback key, and any required options. The `state` output will reflect the current boolean state of that feedback source.

### Node Structure

#### Config Schema

- `connectionId` (string): ID of the connection to monitor
- `feedbackKey` (string): Key identifying the specific feedback to watch
- `options` (record of string → string | number): Feedback-specific options

#### Input Schema

- *(no inputs)*

#### Output Schema

- `state` (boolean | null): Current state of the feedback (`null` if not yet received)

### Implementation Details

- Subscribes to feedback state changes via the system router
- Validates options against the feedback definition schema on each config change
- Unsubscribes from the previous feedback before resubscribing when config changes
- Cleans up all subscriptions on node unmount

### Integration

The FeedbackNode is registered in:

- `NodeRegistry` as `feedback`
- `NodeTypeSchema` as `'feedback'`

### Example Workflow Usage

1. **Mute Indicator**: Monitor an audio console mute feedback and drive a logic node based on its state
2. **Conditional Action**: Wire `state` into a LogicNode to only execute an action when a specific feedback is active
3. **Display Status**: Connect `state` to a DisplayNode to visualise a connection's current feedback state

---

## InternalActionNode

The InternalActionNode executes internal system actions — actions that are not tied to a specific device connection. It supports the same triggering modes as ActionNode.

### Features

- **Internal Actions**: Targets the built-in system action registry rather than a connection
- **Flexible Triggering**: Trigger on startup, data change, or incoming event
- **Dynamic Options**: Action options are validated against the action's own schema
- **Input Overrides**: Action key can be overridden from inputs

### Usage

Select an internal action key in the config and choose a trigger mode. Connect a trigger signal to the `trigger` input for event-driven execution.

### Node Structure

#### Config Schema

- `actionKey` (string, optional): ID of the internal action to execute
- `triggerOnEvent` (boolean, optional): Fire when a trigger event is received
- `triggerOnChange` (boolean, optional): Fire when inputs change
- Additional action-specific option fields

#### Input Schema

- `trigger` (optional): Trigger signal — fires the action when an event arrives
- `actionKey` (string, optional): Override the configured action key
- Additional passthrough fields for action options

#### Output Schema

- *(no outputs)*

### Implementation Details

- Uses the `internalActions` registry to resolve and validate the action
- Executes via `rpcClient.orchestrator.runInternalAction` with a 5000ms timeout
- Otherwise behaves identically to ActionNode

### Integration

The InternalActionNode is registered in:

- `NodeRegistry` as `internalAction`
- `NodeTypeSchema` as `'internalAction'`

### Example Workflow Usage

1. **Page Navigation**: Trigger an internal page-change action from a button press
2. **System Reset**: Fire a system-level reset action on workflow startup

---

## InternalFeedbackNode

The InternalFeedbackNode monitors the state of an internal system feedback source (not tied to a device connection) and exposes its state as a boolean output.

### Features

- **Internal Feedback**: Targets the built-in internal feedback registry
- **Live Subscription**: Updates automatically when the feedback state changes
- **Boolean Output**: Normalises feedback state to `true`, `false`, or `null`
- **Mapping Support**: Decodes option mappings from JSON strings when needed

### Usage

Select an internal feedback key and configure any required options. The `state` output will reflect the current boolean state.

### Node Structure

#### Config Schema

- `feedbackKey` (string): ID of the internal feedback to monitor
- `options` (record of string → string | number | boolean | array): Feedback-specific options

#### Input Schema

- *(no inputs)*

#### Output Schema

- `state` (boolean | null): Current state of the feedback

### Implementation Details

- Uses the `internalFeedbacks` registry to resolve and validate the feedback
- Decodes option values that are JSON strings (e.g. mappings stored as serialised arrays)
- Cleans up subscriptions on node unmount

### Integration

The InternalFeedbackNode is registered in:

- `NodeRegistry` as `internalFeedback`
- `NodeTypeSchema` as `'internalFeedback'`

### Example Workflow Usage

1. **Page State**: Monitor which page is currently active and use the result to drive conditional logic
2. **System Status**: Expose an internal system state as a boolean for downstream processing

---

## IntervalNode

The IntervalNode fires a `trigger` event repeatedly at a fixed interval, like a clock tick. It is useful for polling, periodic actions, and animation loops.

### Features

- **Periodic Triggering**: Emits a `trigger` event at every configured interval
- **Configurable Rate**: Set interval from 100ms to 10 minutes
- **Auto-Restart**: Clears and restarts the interval whenever the config changes
- **Timestamp Output**: Exposes the timestamp of the most recent tick

### Usage

Set the `interval` in milliseconds. The node will emit a `trigger` event and update `timestamp` on every tick.

interval: 1000ms  →  trigger event fires every second


### Node Structure

#### Config Schema

- `interval` (number, min: `100`, max: `600000`, default: `1000`): Interval in milliseconds

#### Input Schema

- *(no inputs)*

#### Output Schema

- `timestamp` (number): Unix timestamp (ms) of the most recent tick

#### Output Events

- `trigger`: Emitted on every interval tick

### Implementation Details

- Uses `setInterval` internally
- Restarts the interval when the config changes to apply the new duration immediately
- Calls `workflow.rerenderFromNode()` on each tick to propagate the updated timestamp
- Cleans up the interval on node unmount

### Integration

The IntervalNode is registered in:

- `NodeRegistry` as `interval`
- `NodeTypeSchema` as `'interval'`

### Example Workflow Usage

1. **Polling Loop**: Trigger a feedback read or variable check every second
2. **Animation Tick**: Drive an animation or counter update at a regular cadence
3. **Heartbeat**: Send a periodic keep-alive action to a connected device

---

## LogicNode

The LogicNode evaluates a logical or comparison operation between two values and outputs a boolean result. It supports numeric comparisons, strict equality, and boolean logic operators.

### Features

- **Comparison Operators**: `==`, `!=`, `>`, `<`, `>=`, `<=`, `===`, `!==`
- **Boolean Operators**: `AND`, `OR`, `XOR`, `NOT`
- **Input and Config Values**: Both values can be supplied via config (static) or inputs (dynamic)
- **Type Coercion**: Numeric comparisons coerce string inputs to numbers

### Usage

#### Comparison

valueA: 10, operator: ">", valueB: 5  →  result: true
valueA: "on", operator: "==", valueB: "on"  →  result: true


#### Boolean Logic

valueA: true, operator: "AND", valueB: false  →  result: false
valueA: true, operator: "NOT"  →  result: false


### Node Structure

#### Config Schema

- `valueA` (string | number | boolean, optional): Left-hand operand
- `operator` (enum, optional): One of `==` `!=` `>` `<` `>=` `<=` `===` `!==` `AND` `OR` `XOR` `NOT`
- `valueB` (string | number | boolean, optional): Right-hand operand

#### Input Schema

- `valueA` (string | number | boolean, optional): Overrides config `valueA`
- `operator` (enum, optional): Overrides config `operator`
- `valueB` (string | number | boolean, optional): Overrides config `valueB`

#### Output Schema

- `result` (boolean): Result of the logical operation

### Implementation Details

- Default operator is `==` when none is configured
- `NOT` is a unary operator — only `valueA` is used
- Numeric operators (`>`, `<`, `>=`, `<=`) cast both values to numbers before comparing
- Input values take precedence over config values

### Integration

The LogicNode is registered in:

- `NodeRegistry` as `logic`
- `NodeTypeSchema` as `'logic'`

### Example Workflow Usage

1. **Threshold Check**: `valueA` = sensor reading, `operator` = `>`, `valueB` = threshold constant
2. **Gate Condition**: Feed `result` into a StateGateNode or EventGateNode
3. **Inverter**: Use `NOT` to invert a boolean feedback state

---

## MathNode

The MathNode performs a basic arithmetic operation on two numbers and outputs the result.

### Features

- **Four Arithmetic Operations**: Addition, subtraction, multiplication, division
- **Type Coercion**: String inputs are coerced to numbers automatically
- **Input and Config Values**: Both operands can come from config (static) or inputs (dynamic)

### Usage

numberA: 10, operation: "add",      numberB: 5  →  result: 15
numberA: 10, operation: "subtract", numberB: 3  →  result: 7
numberA: 4,  operation: "multiply", numberB: 6  →  result: 24
numberA: 20, operation: "divide",   numberB: 4  →  result: 5


### Node Structure

#### Config Schema

- `numberA` (string | number, coerced to number, optional): Left-hand operand
- `operation` (`'add'` | `'subtract'` | `'multiply'` | `'divide'`, optional): Operation to perform
- `numberB` (string | number, coerced to number, optional): Right-hand operand

#### Input Schema

- `numberA` (string | number, coerced to number, optional): Overrides config `numberA`
- `operation` (enum, optional): Overrides config `operation`
- `numberB` (string | number, coerced to number, optional): Overrides config `numberB`

#### Output Schema

- `result` (number): Result of the arithmetic operation

### Implementation Details

- Default operation is `add`
- Input values take precedence over config values
- Division does not guard against divide-by-zero; the result will be `Infinity` or `NaN`

### Integration

The MathNode is registered in:

- `NodeRegistry` as `math`
- `NodeTypeSchema` as `'math'`

### Example Workflow Usage

1. **Offset**: Add a fixed offset to a sensor value before sending it downstream
2. **Scale**: Multiply a normalised (0–1) value by 100 to get a percentage
3. **Counter Arithmetic**: Combine with CounterNode to compute derived values

---

## NFCNode

The NFCNode listens for NFC card read events from a specific surface position (or all positions) and exposes the tag ID, surface, position, and timestamp.

### Features

- **NFC Event Subscription**: Reacts to NFC card reads from physical surface positions
- **Position Filtering**: Can listen to all positions or a specific position ID
- **Event Emission**: Publishes an `nfcRead` event with the full read payload
- **Persistent Last Read**: Stores the most recent read so state values are always current

### Usage

Leave `positionId` as `null` to receive reads from any position, or configure a specific position ID to filter to a single position.

### Node Structure

#### Config Schema

- `positionId` (string | null, default: `null`): Position to listen to, or `null` for all positions

#### Input Schema

- *(no inputs)*

#### Output Schema

- `nfcId` (string | null): ID of the NFC tag that was read
- `surfaceId` (string | null): Surface where the tag was read
- `positionId` (string | null): Position where the tag was read
- `timestamp` (number | null): Timestamp of the read event

#### Output Events

- `nfcRead`: Emitted on each NFC read, carrying the full payload

### Implementation Details

- Subscribes to position NFC events via the system router
- Filters by `positionId` if one is configured
- Calls `workflow.rerenderFromNode()` on each read to update downstream nodes
- Cleans up the subscription on node unmount

### Integration

The NFCNode is registered in:

- `NodeRegistry` as `nfc`
- `NodeTypeSchema` as `'nfc'`

### Example Workflow Usage

1. **Access Control**: Trigger an action when a specific NFC tag is scanned at a position
2. **Asset Tracking**: Log or display the ID of the last NFC tag read
3. **Multi-Position Monitor**: Listen to all positions and route based on `positionId` output

---

## OscListenerNode

The OscListenerNode starts an OSC (Open Sound Control) UDP server and listens for incoming messages on configured paths. Each matched path emits a named event carrying the message parameters.

### Features

- **OSC Server**: Hosts a UDP-based OSC listener on a configurable port
- **Path Matching**: Supports exact paths, named params (`:name`), wildcards (`*`, `**`), and glob patterns
- **Named Endpoints**: Each endpoint has a stable ID that becomes the event handle name
- **Parameter Extraction**: Named route parameters are extracted and available as outputs
- **Legacy Path Support**: Backwards-compatible with the older array-of-strings path format

### Usage

Configure endpoints with OSC paths. When a matching OSC message arrives, the corresponding event fires with the message parameters.

Endpoint: id="scene", path="/scene/:name"
Incoming: /scene/main  →  event "scene" fires, params.name = "main"

Endpoint: id="fader", path="/fader/\*"
Incoming: /fader/1     →  event "fader" fires


### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `port` (number, min: `1024`, max: `65535`): UDP port to listen on
- `endpoints` (array of `{id, path}`, default: `[]`): Named endpoint definitions
- `paths` (array of string, default: `[]`): Legacy path list (deprecated)

#### Input Schema

- Dynamic fields matching endpoint IDs (for state-based access)

#### Output Schema

- Dynamic fields for each endpoint: carries the last received parameters and payload
- `params` (object): Named route parameters extracted from the path pattern

#### Output Events

- One event per endpoint ID, fired when a matching message is received

### Implementation Details

- Starts an OSC UDP server on mount and restarts it on config changes
- Path patterns are compiled to regex for efficient matching
- Named params use `:paramName` syntax; wildcards use `*` (single segment) or `**` (multi-segment)
- Results are cached per endpoint ID so state values are always populated

### Integration

The OscListenerNode is registered in:

- `NodeRegistry` as `osc`
- `NodeTypeSchema` as `'osc'`

### Example Workflow Usage

1. **Mixing Console**: Listen for `/channel/:num/fader` messages and route them to connection actions
2. **Lighting Control**: Receive OSC cues from a lighting console and trigger companion actions
3. **Multi-Parameter**: Use named params to extract values directly from the OSC path

---

## PriorityStackNode

The PriorityStackNode selects a value from a set of inputs based on a configurable priority rule. When multiple inputs are active, the priority mode determines which one wins.

### Features

- **Multiple Priority Modes**: top-to-bottom, bottom-to-top, last-changed, first-changed
- **Fallback Value**: Returns a configurable fallback when no inputs are active
- **Active Input Tracking**: Maintains a set of currently active inputs and their change timestamps
- **Ordered Output**: Also exposes the full ordered list of active input IDs

### Usage

Add named inputs in the config. When those inputs receive truthy values, they are considered "active". The node selects the value of the highest-priority active input according to the chosen mode.

Inputs config: [A, B, C]  (priority mode: topToBottom)
A = "hello" (truthy), B = false, C = "world" (truthy)

Result: "hello"  (A has higher priority than C)


### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `fallbackValue` (unknown, optional): Value returned when no inputs are active
- `inputs` (array of `{id, label}`, default: `[]`): The named inputs to manage
- `priorityMode` (`'topToBottom'` | `'bottomToTop'` | `'lastChange'` | `'firstChange'`, default: `'topToBottom'`): How to select the winner

#### Input Schema

- Dynamic inputs matching the configured input IDs (boolean | string | number)

#### Output Schema

- `value` (unknown, optional): Value of the highest-priority active input, or `fallbackValue`
- `ordered` (string[]): Ordered list of active input IDs from highest to lowest priority

### Implementation Details

- An input is active if its value is truthy (non-zero, non-empty, non-false, non-null)
- `topToBottom` / `bottomToTop` use the position of the input in the `inputs` config array
- `lastChange` / `firstChange` use the timestamp of when each input last became active
- Change timestamps are stored per input ID in `lastChangeTs`

### Integration

The PriorityStackNode is registered in:

- `NodeRegistry` as `priorityStack`
- `NodeTypeSchema` as `'priorityStack'`

### Example Workflow Usage

1. **Layer Priority**: Show the most recently activated overlay source
2. **Fallback Input**: Select from a ranked list of video sources, showing the highest-priority active one
3. **Last-In-Wins**: Use `lastChange` mode so the most recently activated input always wins

---

## RestNode

The RestNode hosts a Fastify HTTP server with configurable REST API endpoints. GET endpoints return current input values; POST endpoints emit events and cache received request bodies.

### Features

- **HTTP Server**: Spins up a Fastify server on a configurable port
- **GET and POST Endpoints**: GET returns live state data; POST captures body data and fires an event
- **Bearer Token Auth**: Optional token-based authentication per request
- **Rate Limiting**: Configurable request rate limits via `@fastify/rate-limit`
- **Named Endpoints**: Each endpoint has a stable ID used as the event handle
- **Auto-Restart**: Server restarts automatically on config changes

### Usage

#### GET Endpoint

Configure a GET endpoint with path `/status`. The response will include the current value of the `status` input.

#### POST Endpoint

Configure a POST endpoint with path `/trigger`. When a POST request arrives, a `trigger` event fires with the request body as its payload.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `port` (number, min: `1024`, max: `65535`): HTTP port
- `rateLimit` (optional): `{max: number, timeWindow: number}` — request rate limit
- `bearerTokens` (array of `{id, token}`, default: `[]`): Allowed bearer tokens
- `endpoints` (array of `{id, path, method}`, default: `[]`): Configured endpoints

#### Input Schema

- Dynamic fields matching GET endpoint IDs (data returned in GET responses)

#### Output Schema

- Dynamic fields matching POST endpoint IDs (last received body data)

#### Output Events

- One event per POST endpoint ID, fired on each request

### Implementation Details

- Uses Fastify with `@fastify/rate-limit` plugin
- Bearer token authentication checks the `Authorization: Bearer <token>` header
- POST endpoints cache the request body and emit an event
- GET endpoints return the current input value for that endpoint ID
- Server is fully restarted on any config change

### Integration

The RestNode is registered in:

- `NodeRegistry` as `rest`
- `NodeTypeSchema` as `'rest'`

### Example Workflow Usage

1. **Webhook Receiver**: Receive webhook POSTs from external services and trigger workflow actions
2. **Status API**: Expose current workflow state as a GET endpoint for external monitoring
3. **Remote Control**: Trigger companion actions via HTTP requests from a control surface or script

---

## SchedulerNode

The SchedulerNode triggers on a cron schedule. It emits a `trigger` event and updates a `timestamp` output each time the cron expression fires.

### Features

- **Cron Scheduling**: Uses standard cron expressions for flexible scheduling
- **Event Emission**: Publishes a `trigger` event on each scheduled tick
- **Auto-Restart**: Stops and restarts the cron task when the expression changes
- **Comprehensive Logging**: Logs cron lifecycle events for debugging

### Usage

Set a valid cron expression in `cronExpression`. The node fires at each matching time.

cronExpression: "\*/5 \* \* \* \*"    →  fires every 5 minutes
cronExpression: "0 9 \* \* 1-5"   →  fires at 9am on weekdays
cronExpression: "0 0 \* \* \*"     →  fires at midnight every day


### Node Structure

#### Config Schema

- `cronExpression` (string): A valid cron expression

#### Input Schema

- *(no inputs)*

#### Output Schema

- `timestamp` (number): Unix timestamp (ms) of the most recent cron tick

#### Output Events

- `trigger`: Emitted on each cron tick

### Implementation Details

- Uses the `node-cron` library
- Manages a single `ScheduledTask` instance, replacing it when the expression changes
- Logs `scheduled`, `started`, `stopped`, and `destroyed` lifecycle events
- Cleans up the task on node unmount

### Integration

The SchedulerNode is registered in:

- `NodeRegistry` as `scheduler`
- `NodeTypeSchema` as `'scheduler'`

### Example Workflow Usage

1. **Daily Report**: Trigger a sequence of actions every morning at a specific time
2. **Business Hours**: Fire events only during working hours using day-of-week and hour ranges
3. **Periodic Sync**: Refresh connection state or send a sync command at regular intervals

---

## StateGateNode

The StateGateNode passes a payload value through to its output when the gate is open, and blocks it when closed. Unlike EventGateNode it operates on continuous state values rather than events.

### Features

- **State Gating**: Passes `payload` through when open; returns `fallback` or cached value when closed
- **Two Control Modes**: Gate state driven by a boolean input (state mode) or by discrete events (event mode)
- **Fallback Value**: Optional fallback output when the gate is closed
- **Value Caching**: Remembers the last payload seen while open

### Usage

#### State Mode

Connect a boolean to the `open` input. While truthy, `payload` is forwarded to `value`. While falsy, `value` returns the `fallback` (or the last cached payload).

#### Event Mode

Send `open`, `close`, or `toggle` events to control the gate. The `payload` input is the value being gated.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `controlMode` (`'state'` | `'event'`, default: `'state'`): How the gate is controlled
- `initiallyOpen` (boolean, default: `true`): Starting state
- `open` (boolean, optional): Static open state (used as config default in state mode)

#### Input Schema

- `payload` (unknown, optional): Value to gate
- `open` (boolean | string | number, optional): Gate control signal (state mode)
- `fallback` (unknown, optional): Value returned when gate is closed

#### Output Schema

- `value` (unknown, optional): Gated payload or fallback
- `isOpen` (boolean): Current gate state

#### Input Events (event mode)

- `open`: Open the gate
- `close`: Close the gate
- `toggle`: Toggle the gate state
- `reset`: Reset to `initiallyOpen` state

### Implementation Details

- In state mode, gate state is computed from the `open` input on each render
- In event mode, gate state persists and is modified by events
- When closed: returns `fallback` if provided, otherwise returns the last cached payload value

### Comparison with EventGateNode

| Feature | StateGateNode | EventGateNode |
| --- | --- | --- |
| Gates continuous values | ✓ | ✗ |
| Gates events | ✗ | ✓ |
| State and event control modes | ✓ | ✓ |

### Integration

The StateGateNode is registered in:

- `NodeRegistry` as `stateGate`
- `NodeTypeSchema` as `'stateGate'`

### Example Workflow Usage

1. **Hold Last Value**: Gate a sensor reading; when the gate closes, hold the last known value
2. **Conditional Display**: Only pass a value to a display when a condition is met
3. **Safety Interlock**: Block a value from reaching an ActionNode until an enable signal is asserted

---

## StaticValueNode

The StaticValueNode outputs a fixed, configured value of a chosen type. It is useful for injecting constants into a workflow.

### Features

- **Type Selection**: Choose between string, number, or boolean output types
- **Type Coercion**: The configured value is cast to the selected type at runtime
- **Enabled Toggle**: When disabled, the node returns a safe default for the selected type

### Usage

type: "string",  value: "hello"  →  result: "hello"
type: "number",  value: "42"     →  result: 42
type: "boolean", value: true     →  result: true


Defaults when disabled:

- string → `""`
- number → `0`
- boolean → `false`

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `type` (`'string'` | `'number'` | `'boolean'`, default: `'string'`): Output type
- `value` (string | number | boolean, default: `''`): The static value to output

#### Input Schema

- *(no inputs)*

#### Output Schema

- `result` (string | number | boolean): The configured static value cast to the selected type

### Implementation Details

- Type casting is done via a `castValue` method
- When disabled, the type default is returned instead of the configured value

### Integration

The StaticValueNode is registered in:

- `NodeRegistry` as `staticValue`
- `NodeTypeSchema` as `'staticValue'`

### Example Workflow Usage

1. **Constant Threshold**: Provide a fixed number threshold to a LogicNode
2. **Label String**: Inject a fixed string label into a StringMergeNode
3. **Feature Flag**: Use a boolean constant to enable/disable a branch of logic

---

## StringMergeNode

The StringMergeNode concatenates multiple named input values into a single string, joined by a configurable separator.

### Features

- **Dynamic Inputs**: Any number of named input variables can be defined
- **Configurable Separator**: Choose any separator string (empty by default)
- **Null/Undefined Skipping**: Missing or undefined inputs are omitted from the result

### Usage

separator: " ",  var1: "Hello", var2: "World"           →  result: "Hello World"
separator: "-",  var1: "2024",  var2: "01", var3: "15"  →  result: "2024-01-15"
separator: "",   var1: "foo",   var2: undefined          →  result: "foo"


### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `separator` (string, default: `''`): String placed between each value
- `inputVariables` (array of `{name, description}`, default: `[]`): Named inputs to merge

#### Input Schema

- Dynamic fields for each configured input variable name

#### Output Schema

- `result` (string): Concatenated string, or `''` if disabled

### Implementation Details

- Iterates over `inputVariables` in order, collecting non-null/undefined values
- Joins with `separator` using `Array.prototype.join`
- Returns `''` when disabled

### Integration

The StringMergeNode is registered in:

- `NodeRegistry` as `stringMerge`
- `NodeTypeSchema` as `'stringMerge'`

### Example Workflow Usage

1. **Date Formatter**: Merge year, month, and day with `-` separator
2. **Message Builder**: Combine a fixed prefix with a dynamic variable value
3. **CSV Row**: Join multiple values with `,` to build a CSV line

---

## TcpClientNode

The TcpClientNode connects to a remote TCP server, sends messages, and receives data. It automatically reconnects on disconnect and supports delimiter-based message framing.

### Features

- **Auto-Reconnect**: Reconnects automatically at a configurable interval after disconnection
- **Idle Disconnect**: Optionally disconnects after a period of inactivity
- **Delimiter Framing**: Buffer incoming data until a delimiter (LF, CRLF, CR, null byte) before emitting
- **JSON Parsing**: Attempts to JSON-parse each received message and exposes the result
- **Connection Events**: Optionally emits a `connection` event on (re)connect
- **Send on Change**: Sends the `send_state` value whenever it changes

### Usage

Configure the `host` and `port`. Wire a string to `send_state` to send it whenever the value changes. Incoming messages appear on `lastMessage` and `lastObject`.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `host` (string, min length: `1`): Remote hostname or IP address
- `port` (number, min: `1`, max: `65535`): Remote port
- `reconnectInterval` (number, default: `5000`): Reconnect delay in ms
- `disconnectWhenIdle` (boolean, default: `false`): Disconnect after idle period
- `idleDisconnectDelay` (`'0'` | `'200'` | `'1000'` | `'10000'` | `'60000'`, default: `'1000'`): Idle timeout in ms
- `emitOnConnect` (boolean, default: `false`): Emit `connection` event on connect
- `delimiter` (`'none'` | `'null'` | `'lf'` | `'crlf'` | `'cr'`, default: `'none'`): Message delimiter
- `bufferUntilDelimiter` (boolean, default: `false`): Buffer data until delimiter is found
- `maxBufferSize` (number, default: `65536`): Max buffer size before truncation

#### Input Schema

- `send_state` (string, optional): Message to send to the remote server

#### Output Schema

- `isConnected` (boolean): Whether the TCP connection is active
- `lastMessage` (string): Most recently received message
- `lastObject` (unknown): JSON-parsed version of the last message (if parseable)

#### Output Events

- `message`: Emitted on each received message
- `connection`: Emitted on connect (if `emitOnConnect` is enabled)

### Integration

The TcpClientNode is registered in:

- `NodeRegistry` as `tcpClient`
- `NodeTypeSchema` as `'tcpClient'`

### Example Workflow Usage

1. **Device Control**: Send commands to a device over TCP and process its responses
2. **Telnet-Style Interface**: Connect to a text-based TCP service and parse responses
3. **Raw Protocol**: Use no delimiter for binary/streaming protocols

---

## TcpServerNode

The TcpServerNode listens for incoming TCP connections, broadcasts messages to all connected clients, and exposes data received from any client.

### Features

- **Multi-Client Support**: Accepts and manages multiple simultaneous TCP connections
- **Broadcast**: Sends the `send_state` value to all currently connected clients
- **Delimiter Framing**: Per-client message buffering with configurable delimiters
- **JSON Parsing**: Attempts to parse received messages as JSON
- **Client Tracking**: Exposes connection count, last sender IP/port, and last received message
- **Error Reporting**: Surfaces server errors via the `errorMessage` output

### Usage

Configure a `port` to listen on. Wire a string to `send_state` to broadcast it to all clients. Incoming messages from any client appear on the `lastMessage` and related outputs.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `port` (number, min: `1024`, max: `65535`): TCP port to listen on
- `delimiter` (`'none'` | `'null'` | `'lf'` | `'crlf'` | `'cr'`, default: `'none'`): Message delimiter
- `bufferUntilDelimiter` (boolean, default: `false`): Buffer per-client data until delimiter
- `maxBufferSize` (number, default: `65536`): Max per-client buffer size

#### Input Schema

- `send_state` (string, optional): Message to broadcast to all connected clients

#### Output Schema

- `isConnected` (boolean): Whether at least one client is connected
- `connectionCount` (number): Number of currently connected clients
- `lastMessage` (string): Most recently received message (from any client)
- `lastObject` (unknown): JSON-parsed version of the last message
- `lastTimestamp` (number): Timestamp of the last received message
- `lastFromIp` (string): IP address of the last sender
- `lastFromPort` (number): Port of the last sender
- `errorMessage` (string): Last server error message

### Integration

The TcpServerNode is registered in:

- `NodeRegistry` as `tcpServer`
- `NodeTypeSchema` as `'tcpServer'`

### Example Workflow Usage

1. **Control Surface Server**: Accept connections from multiple control surfaces and broadcast state
2. **Data Aggregator**: Collect incoming data from multiple TCP clients and process it centrally
3. **Echo Server**: Receive messages and send responses back to all clients

---

## UdpReceiverNode

The UdpReceiverNode binds a UDP socket on a configured port and receives datagrams from any sender. It supports delimiter-based message buffering and exposes sender metadata alongside the received data.

### Features

- **UDP Listener**: Binds a UDP socket and receives datagrams from multiple senders
- **Per-Sender Buffering**: Message buffering tracked per source address
- **Delimiter Framing**: Buffer data until a delimiter before treating it as a complete message
- **JSON Parsing**: Attempts to parse each message as JSON
- **Sender Metadata**: Exposes the IP address and port of the last sender
- **Event Emission**: Emits a `message` event on each received message

### Usage

Configure the `port`. Incoming UDP datagrams are available on `lastMessage` and `lastObject`. The `message` event fires on each datagram.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `port` (number, min: `1024`, max: `65535`): UDP port to bind on
- `delimiter` (`'none'` | `'null'` | `'lf'` | `'crlf'` | `'cr'`, default: `'none'`): Message delimiter
- `bufferUntilDelimiter` (boolean, default: `false`): Buffer per-sender data until delimiter
- `maxBufferSize` (number, default: `65536`): Max buffer size per sender

#### Input Schema

- *(no inputs)*

#### Output Schema

- `lastMessage` (string): Most recently received message
- `lastObject` (unknown): JSON-parsed version of the last message
- `lastAddress` (string): IP address of the last sender
- `lastPort` (number): Port of the last sender
- `lastTimestamp` (number): Timestamp of the last received message
- `errorMessage` (string): Last socket error message

#### Output Events

- `message`: Emitted on each received datagram

### Integration

The UdpReceiverNode is registered in:

- `NodeRegistry` as `udpReceiver`
- `NodeTypeSchema` as `'udpReceiver'`

### Example Workflow Usage

1. **OSC Alternative**: Receive raw UDP messages from devices that don't support OSC framing
2. **Sensor Data**: Ingest UDP telemetry from sensors or embedded devices
3. **Broadcast Receiver**: Listen for UDP broadcast packets on the local network

---

## UdpSenderNode

The UdpSenderNode sends UDP datagrams to a configured host and port. The destination can be overridden at runtime via input values.

### Features

- **UDP Sending**: Sends datagrams to a configured or dynamically specified host/port
- **Input Overrides**: Host and port can be overridden per-render via `host_input` and `port_input`
- **Message Delimiter**: Optionally appends a delimiter to each outgoing message
- **Local Port Binding**: Can bind to a specific local port, or use 0 for auto-assignment
- **Send Confirmation**: Emits a `sent` event and updates last-sent metadata after each send

### Usage

Configure the default `host` and `port`. Wire a string to `send_state` to send it. Override the destination dynamically using `host_input` and `port_input`.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `host` (string, optional): Default destination hostname or IP
- `port` (number, optional): Default destination port
- `localPort` (number, optional): Local port to bind (`0` = auto)
- `delimiter` (`'none'` | `'null'` | `'lf'` | `'crlf'` | `'cr'`, default: `'none'`): Appended delimiter

#### Input Schema

- `send_state` (string, optional): Message to send
- `host_input` (string, optional): Overrides config `host`
- `port_input` (number, optional): Overrides config `port`

#### Output Schema

- `lastSentMessage` (string): Last message that was sent
- `lastSentAddress` (string): Destination address of the last send
- `lastSentPort` (number): Destination port of the last send
- `errorMessage` (string): Last send error message

#### Output Events

- `sent`: Emitted after each successful send

### Integration

The UdpSenderNode is registered in:

- `NodeRegistry` as `udpSender`
- `NodeTypeSchema` as `'udpSender'`

### Example Workflow Usage

1. **Device Command**: Send UDP control commands to a device that accepts UDP input
2. **Dynamic Routing**: Use `host_input` and `port_input` to route messages to different destinations based on workflow state
3. **Broadcast**: Send to the subnet broadcast address to reach all devices on the local network

---

## WebSocketClientNode

The WebSocketClientNode connects to a remote WebSocket server, sends messages, and receives data. It automatically reconnects on disconnect and supports idle-based disconnection.

### Features

- **Auto-Reconnect**: Reconnects automatically at a configurable interval
- **Idle Disconnect**: Optionally disconnects after a period without outgoing messages
- **JSON Parsing**: Attempts to parse each received message as JSON
- **Connection Events**: Optionally emits a `connection` event on (re)connect
- **Send on Change**: Sends the `send_state` value whenever it changes

### Usage

Configure the WebSocket `url`. Wire a string to `send_state` to send messages. Received messages appear on `lastMessage` and `lastObject`.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `url` (string, min length: `1`): WebSocket URL to connect to (e.g. `ws://host:port/path`)
- `reconnectInterval` (number, default: `5000`): Reconnect delay in ms
- `disconnectWhenIdle` (boolean, default: `false`): Disconnect after idle period
- `idleDisconnectDelay` (`'0'` | `'200'` | `'1000'` | `'10000'` | `'60000'`, default: `'1000'`): Idle timeout in ms
- `emitOnConnect` (boolean, default: `false`): Emit `connection` event on connect

#### Input Schema

- `send_state` (string, optional): Message to send to the server

#### Output Schema

- `isConnected` (boolean): Whether the WebSocket is currently connected
- `lastMessage` (string): Most recently received message
- `lastObject` (unknown): JSON-parsed version of the last message

#### Output Events

- `message`: Emitted on each received message
- `connection`: Emitted on connect (if `emitOnConnect` is enabled)

### Integration

The WebSocketClientNode is registered in:

- `NodeRegistry` as `webSocketClient`
- `NodeTypeSchema` as `'webSocketClient'`

### Example Workflow Usage

1. **API Integration**: Connect to a WebSocket API and react to incoming messages
2. **Real-Time Data**: Subscribe to a live data feed and pass values downstream
3. **Bidirectional Control**: Send commands and receive state updates over a single WebSocket connection

---

## WebSocketServerNode

The WebSocketServerNode hosts a WebSocket server that accepts client connections, broadcasts messages to all connected clients, and exposes data received from any client.

### Features

- **Multi-Client Support**: Accepts and manages multiple simultaneous WebSocket connections
- **Broadcast**: Sends the `send_state` value to all connected clients
- **Client ID Tracking**: Each connecting client receives an auto-incremented ID
- **JSON Parsing**: Attempts to parse received messages as JSON
- **Connection Events**: Optionally emits a `connection` event when a new client connects
- **Error Reporting**: Surfaces server errors via the `errorMessage` output

### Usage

Configure a `port`. Clients connect via `ws://host:port`. Wire a string to `send_state` to broadcast it. Incoming messages appear on `lastMessage` and related outputs.

### Node Structure

#### Config Schema

- `label` (string, optional): Node label
- `enabled` (boolean, default: `true`)
- `port` (number, min: `1024`, max: `65535`): WebSocket port to listen on
- `emitOnConnect` (boolean, default: `false`): Emit `connection` event when a client connects

#### Input Schema

- `send_state` (string, optional): Message to broadcast to all connected clients

#### Output Schema

- `isConnected` (boolean): Whether at least one client is connected
- `connectionCount` (number): Number of currently connected clients
- `lastMessage` (string): Most recently received message (from any client)
- `lastObject` (unknown): JSON-parsed version of the last message
- `lastTimestamp` (number): Timestamp of the last received message
- `lastFromId` (string): Client ID of the last sender
- `errorMessage` (string): Last server error message

#### Output Events

- `connection`: Emitted when a client connects (if `emitOnConnect` is enabled)

### Integration

The WebSocketServerNode is registered in:

- `NodeRegistry` as `webSocketServer`
- `NodeTypeSchema` as `'webSocketServer'`

### Example Workflow Usage

1. **Browser Dashboard**: Serve real-time state to a web dashboard connected via WebSocket
2. **Control Protocol**: Accept commands from custom WebSocket clients and trigger workflow actions
3. **State Broadcast**: Push workflow state updates to all listening clients whenever values change

---

## WorkflowRouterNode

The WorkflowRouterNode is a dynamic signal router that maps configurable sources to destinations across one or more levels. Routes are persisted to the database and survive restarts.

### Features

- **Dynamic Routing**: Connects any source to any destination at runtime
- **Multi-Level**: Supports up to 10 independent routing levels (e.g. for video, audio, data)
- **Scalable**: Up to 1000 sources and 1000 destinations per router
- **Persistent Routes**: Routes are saved to the database with a 5-second debounce
- **Cross-Workflow Validation**: Validates that source and destination nodes belong to the same workflow
- **Dynamic I/O**: Source and destination counts can be changed without losing existing routes

### Usage

Configure the number of sources, destinations, and levels. Use the editor UI to assign routes. The `output_<dest>` values reflect the current value of whatever source is routed to each destination. An `unrouted_<dest>` value appears when no source is assigned.

### Node Structure

#### Config Schema

- `label` (string, default: `'Router'`): Node label
- `sourceCount` (number, min: `1`, max: `1000`, default: `10`): Number of source inputs
- `destinationCount` (number, min: `1`, max: `1000`, default: `10`): Number of destination outputs
- `levels` (number, min: `1`, max: `10`, default: `1`): Number of independent routing levels

#### Input Schema

- Dynamic inputs for each source (boolean | string | number | null)

#### Output Schema

- `output_<N>` for each destination: Current value of the routed source, or fallback if unrouted
- `unrouted_<N>` (boolean): Whether destination N is currently unrouted
- `ordered` (array): Ordered list of active source IDs by destination

### Implementation Details

- Built with the Effect library for typed error handling and resource management
- Routes are stored in the database with a 5-second write debounce to reduce I/O
- In-memory cache is the source of truth; the database is used only for persistence and initial load
- Orphaned ports (from reduced source/destination counts) are cleaned up on config change
- Cross-workflow routing is blocked with a validation error

### Integration

The WorkflowRouterNode is registered in:

- `NodeRegistry` as `workflowRouter`
- `NodeTypeSchema` as `'workflowRouter'`

### Example Workflow Usage

1. **Video Router**: Route camera feeds to output destinations with a visual matrix UI
2. **Audio Routing**: Assign audio sources to mix bus destinations across multiple levels
3. **Signal Matrix**: General-purpose N×M routing matrix for any type of signal value

---

## WorkflowVariableNode

The WorkflowVariableNode stores a typed value scoped to the current workflow. It acts as a shared memory cell: other nodes can write to it and read from it. Values can optionally be persisted to the database.

### Features

- **Typed Storage**: Stores a string, number, or boolean value with type enforcement
- **Read and Write**: Exposes both a `write` input and a `read` output on the same node
- **Optional Persistence**: Can save the value to the database so it survives restarts
- **Live Sync**: Broadcasts changes via Redis so all nodes reacting to this variable update immediately
- **Controlled/Uncontrolled Modes**: When `write` is connected, the node is controlled by that input; when disconnected, it manages its own state

### Usage

Connect a value to `write` to update the variable. Read the current value from the `read` output. Other nodes in the same workflow can reference this variable by its `variableKey`.

### Node Structure

#### Config Schema

- `schema` (`{type: 'string' | 'number' | 'boolean'}`): The type of the variable
- `initialValue` (unknown): Value used on first creation
- `variableKey` (string): Unique identifier for this variable within the workflow
- `persist` (boolean, default: `false`): Whether to persist the value to the database

#### Input Schema

- `write` (string | number | boolean | object | null, optional): Value to write to the variable

#### Output Schema

- `read` (string | number | boolean | object | null, optional): Current value of the variable

### Implementation Details

- Initialises from Redis, the persisted database value, or `initialValue` (in that priority order)
- Persistence is throttled to a maximum of once every 30 seconds
- Type casting produces a friendly error message if the written value cannot be cast to the configured type
- Cross-node reactivity via Redis pub/sub

### Integration

The WorkflowVariableNode is registered in:

- `NodeRegistry` as `workflowVariable`
- `NodeTypeSchema` as `'workflowVariable'`

### Example Workflow Usage

1. **Shared State**: Store a mode value that multiple branches of the workflow read
2. **Persisted Counter**: Track a count that survives workflow restarts
3. **Cross-Node Communication**: Write from one part of the workflow and read in another without direct connections

---

## WorkflowVariableReadNode

The WorkflowVariableReadNode reads the current value of a variable from **another** workflow. It subscribes to live updates so the output always reflects the latest value.

### Features

- **Cross-Workflow Read**: Accesses variables defined in a different workflow
- **Live Subscription**: Automatically updates when the target variable changes
- **Schema Validation**: Validates the received value against the target variable's type schema
- **Reactive**: Triggers a re-render when the value changes

### Usage

Configure the `workflowId` of the workflow that owns the variable, and the `variableKey` of the variable to read. The `read` output reflects its current value.

### Node Structure

#### Config Schema

- `workflowId` (string): ID of the workflow that owns the variable
- `variableKey` (string): Key of the variable to read

#### Input Schema

- *(no inputs)*

#### Output Schema

- `read` (string | number | boolean | object | null, optional): Current value of the variable

### Implementation Details

- Subscribes to variable change notifications for the target workflow/key pair
- Validates the incoming value against the target variable's type schema
- Cleans up the subscription on node unmount or config change

### Comparison with WorkflowVariableNode

| Feature | WorkflowVariableNode | WorkflowVariableReadNode |
| --- | --- | --- |
| Same-workflow variable | ✓ | ✗ |
| Cross-workflow read | ✗ | ✓ |
| Write support | ✓ | ✗ |

### Integration

The WorkflowVariableReadNode is registered in:

- `NodeRegistry` as `workflowVariableRead`
- `NodeTypeSchema` as `'workflowVariableRead'`

### Example Workflow Usage

1. **Global Config**: Read a configuration value maintained in a dedicated "globals" workflow
2. **Shared Counter**: Display or use a counter that is owned and managed by another workflow
3. **Cross-Workflow Sync**: Synchronise state between two workflows without direct connections

---

## WorkflowVariableWriteNode

The WorkflowVariableWriteNode writes a value to a variable defined in **another** workflow. It validates the written value against the target variable's type schema before writing.

### Features

- **Cross-Workflow Write**: Updates a variable in a different workflow
- **Type Validation**: Strictly validates the value against the target variable's schema (no coercion)
- **Reactive**: Re-validates and writes whenever the `write` input changes

### Usage

Configure the `workflowId` of the target workflow and the `variableKey`. Connect the value to write to the `write` input. The value will be written to the target variable whenever it changes.

### Node Structure

#### Config Schema

- `workflowId` (string): ID of the workflow that owns the variable
- `variableKey` (string): Key of the variable to write to

#### Input Schema

- `write` (string | number | boolean | object | null, optional): Value to write

#### Output Schema

- *(no outputs)*

### Implementation Details

- Validates the value against the target variable's type schema before writing
- Uses strict type validation — values are not coerced to the target type
- Subscribes to the target schema to stay up to date with type changes
- Cleans up subscriptions on node unmount

### Comparison with WorkflowVariableNode

| Feature | WorkflowVariableNode | WorkflowVariableWriteNode |
| --- | --- | --- |
| Same-workflow variable | ✓ | ✗ |
| Cross-workflow write | ✗ | ✓ |
| Read support | ✓ | ✗ |

### Integration

The WorkflowVariableWriteNode is registered in:

- `NodeRegistry` as `workflowVariableWrite`
- `NodeTypeSchema` as `'workflowVariableWrite'`

### Example Workflow Usage

1. **Remote Update**: Write a computed value from one workflow into a variable owned by another
2. **Coordinator Pattern**: Have a "controller" workflow update shared variables consumed by multiple "view" workflows
