# Workflow Node Format

This page documents the on-disk JSON shape of a node inside a stored workflow, and
how that JSON is normalized into the internal `WorkflowNodeDTO` used by the
orchestrator and compiler.

There are **two representations of the same node**:

1. **Stored JSON** — the nested, camelCase structure the visual editor
   (React Flow / FlowDrop JS) produces. It is persisted **verbatim** on the
   workflow entity; nothing is transformed at save time.
2. **`WorkflowNodeDTO`** — a flattened, normalized read view built on demand
   when the engine needs to execute, compile, or validate a node.

The stored JSON is the single source of truth. The DTO is a one-way derived
view — the API never serializes a node back through the DTO.

!!! note "Internal API"
    `WorkflowNodeDTO` is marked `@internal`
    (`modules/flowdrop_workflow/src/DTO/WorkflowNodeDTO.php`). The stored JSON
    shape, however, is part of the contract with the editor and with workflow
    import/export bundles.

---

## Stored JSON shape

```jsonc
{
  "id": "calculator.1",          // see "id" below — "<metadata.id>.<n>"
  "type": "universalNode",       // constant, required by FlowDrop JS
  "position": { "x": 100, "y": 200 },
  "data": {
    "label": "My Calculator",    // user-facing label; defaults to node-type label
    "config": {                  // USER-supplied configuration values
      "operation": "multiply"
    },
    "metadata": {                // DERIVED snapshot of the node type + plugin
      "id": "calculator",                                   // node type entity ID
      "executor_plugin": "flowdrop_node_processor:calculator", // plugin ID
      "name": "Calculator",                                 // node type label
      "config": {                                           // resolved defaults
        "operation": "add",
        "precision": 2
      },
      "inputs":  [ { "id": "values", "name": "Values", "dataType": "array" } ],
      "outputs": [ { "id": "result", "name": "Result", "dataType": "number" } ]
    }
  }
}
```

### Field reference

| Field | Source of truth | Notes |
|---|---|---|
| `id` | editor | **Pattern:** `<data.metadata.id>.<number>`, e.g. `calculator.1`. The number usually increments per node of that type on the canvas, but this is a *convention only* — nothing validates the pattern or enforces uniqueness of the counter. Treat `id` as an opaque unique key. |
| `type` | editor | **Constant** `"universalNode"`. Required by FlowDrop JS — it is the React Flow component type, **not** the executor. Do not confuse with `data.metadata.executor_plugin`. |
| `position` | editor (drag / auto-layout) | Canvas coordinates `{ x, y }`. Variable. Desirably a multiple of `20` — produced indirectly by manual drag (snap) or by auto-layout. Purely visual; has no effect on execution. |
| `data.label` | user | Display label. **Defaults to the corresponding node type's label** (`data.metadata.name`). User may override per node. |
| `data.config` | user | Configuration **values** for this node instance. User-supplied. The available keys and value constraints are, in principle, validatable because they are backed by the combination of the node type's settings and the node plugin's schema. |
| `data.metadata.id` | node type | The node type **entity ID** (bare ID). |
| `data.metadata.executor_plugin` | node type | The **plugin ID** (namespaced, e.g. `flowdrop_node_processor:calculator`). This is the actual executor. |
| `data.metadata.name` | node type | The node type **label**. |
| `data.metadata.config` | derived | The node type's config settings applied to the node plugin — i.e. resolved **default** config values. |
| `data.metadata.inputs` | derived | Input port definitions: the node type's **port** settings applied to the node plugin. |
| `data.metadata.outputs` | derived | Output port definitions: the node type's **output** and **"exposed as tool"** settings applied to the node plugin. |

!!! info "`data.config` vs `data.metadata.config`"
    `data.metadata.config` holds the **defaults** snapshotted from the node type
    at edit time. `data.config` holds the **user's overrides** for this specific
    node instance. The effective configuration is the former merged with the
    latter (overrides win) — see the DTO mapping below.

---

## `WorkflowNodeDTO` mapping

`WorkflowNodeDTO::fromArray()`
(`modules/flowdrop_workflow/src/DTO/WorkflowNodeDTO.php:144`) flattens the nested
JSON into nine readonly properties:

| DTO property | Pulled from | Transformation |
|---|---|---|
| `id` | `id` | pass-through |
| `typeId` | `data.metadata.executor_plugin` → `data.metadata.id` → `type` | **fallback chain**; prefers the namespaced executor plugin ID |
| `label` | `data.label` → `data.metadata.name` → `id` | first non-empty |
| `config` | `array_merge(data.metadata.config, data.config)` | **computed**: defaults + user overrides |
| `metadata` | `data.metadata` | whole object, pass-through |
| `position` | `position` | coerced to `{ x: float, y: float }`; `null` if absent |
| `inputs` | `data.metadata.inputs` | pass-through |
| `outputs` | `data.metadata.outputs` | pass-through |
| `rawData` | entire node | retained for reference |

!!! warning "`typeId` resolution gotcha"
    `typeId` prefers `data.metadata.executor_plugin` — the **namespaced** plugin
    ID — and falls back to `data.metadata.id` (the bare node-type entity ID) and
    finally `type`. Anything resolving a processor from a node must use the
    namespaced plugin ID against the node-processor plugin manager; the bare
    `metadata.id` will not resolve. (This is the same field-confusion class of
    bug behind Note/trigger node filtering.)

### Convenience accessors

- `getConfigValue(key, default)` — single key from the merged `config`.
- `getMetadataValue(key, default)` — single key from `metadata`.
- `hasTriggerInput()` — true if any input port has `dataType === 'trigger'`.
- `isGateway()` — true if `metadata.type === 'gateway'`.
- `getCategory()` — `metadata.category`, default `'default'`.

### Serialization caveat

`WorkflowNodeDTO::toArray()` emits **snake_case** keys (`type_id`, …) and is a
debug/serialization aid only. It does **not** round-trip to the stored JSON
shape — storage uses the nested camelCase structure above. Persisting a node
always goes through the editor JSON, never `toArray()`.

---

## Source of truth and the trust boundary

The authoritative definition of a node at execution is the **node type settings
plus the node processor plugin's parameter schema** — never the stored JSON.
The only user-controlled, per-instance value is **`data.config`**.

Everything under `data.metadata` (`executor_plugin`, `name`, `config` defaults,
`inputs`, `outputs`) is a **cache** the editor produced. A stored workflow is
attacker-controllable — through the API, an imported bundle, or a tampered
config entity — so this cache must be treated as **untrusted** and must never be
an execution input.

The rules that follow from this:

- **Re-resolve, don't trust.** Given `data.metadata.id` (the node type entity
  ID), the executor plugin, config defaults, and port definitions are all
  re-derived from the node type entity + plugin at run time. The execution path
  already does this for parameters: `NodeRuntimeService::resolveParameters()`
  reads the schema from `$processor->getParameterSchema()` and defaults from
  `$nodeTypeEntity->getParameters()`, then `ParameterResolver::resolve()`
  validates values against the schema. Any path that instead reads behavior from
  `data.metadata` is a trust gap to close.
- **Validate `data.config` against the schema, per key,** plus the plugin's
  optional `validateParams()` (`FlowDropNodeProcessorInterface`). Unknown keys
  are rejected; known keys are checked against their type/enum/bounds. This
  should hold at **save** (so bad config never persists) as well as at
  **execution** (defense in depth, and to catch config that predates a schema
  change). `ToolParameterScope` (`flowdrop_runtime`) is the existing precedent:
  `projectSchema()` narrows the schema to allowed keys and `filterArgs()` drops
  everything else.

!!! danger "Do not trust `data.metadata` for behavior"
    `executor_plugin`, port definitions, and config defaults read from
    `data.metadata` can be forged. Resolve them from the node type entity keyed
    by `data.metadata.id`. The port definitions are especially sensitive in
    chat/tool contexts, where they shape what an LLM is told it can do and which
    inputs flow in.

## Why two shapes exist anyway

- The **editor** wants a self-contained node so it can render without a round
  trip — hence the `data.metadata` cache. The correct way to keep it fresh is to
  enrich it from the current node type on read, not to trust the stored copy.
- The **export/transport** layer (marketplace, import bundles) wants a
  self-describing node so a target site can validate and render it before the
  node type is installed — the legitimate home for a materialized snapshot.
- The **engine** wants a flat, resolved, *validated* view — and gets it by
  re-resolving from the source of truth, not from the cache.
