# Variables & Data Mapping

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `crates/cheng-engine/src/executor/workflow_executor/node_execution.rs`, `crates/cheng-engine/src/executor/context.rs`, `crates/cheng-nodes/src/nodes/builtin/utils/smart_variable_node.rs`, `chengflow-ui/src/features/editor/components/VariableSelectorDrawer.tsx`

Data moves between nodes in three ways: **edges** (the primary mechanism), **`${...}`
substitution** inside a node's own config, and **ambient injection** by the engine. They have a
strict precedence order, and knowing it explains almost every "why did my node get that value"
question.

## Edges

An edge names four things: source node, source output port, target node, target input port.

```text
Node A ──[ text ]────────►[ user_message ] Node B
        source_output       target_input
```

At execution time the engine walks every incoming edge of a node, reads the source node's
output for that port, and writes it into the target node's input under the target port's name.
Ports are matched by **name**, so renaming an output port in a node's schema breaks any edge
that referenced the old name.

### Dynamic array ports

A port named `prefix_N` resolves against an array output when no port of that exact name exists.
The engine tries `prefix_outputs[N]` first, then `prefixs[N]`:

```text
branch_0  →  branch_outputs[0]   (tried first)
          →  branches[0]         (fallback)
```

This is what lets a router node declare one array output and still expose numbered ports on the
canvas.

### Null means "don't take this path"

A node whose incoming edges **all** produce null is auto-skipped rather than run with empty
inputs. That is the mechanism behind conditional routing: a condition node emits a value on the
branch it selects and null on the others, and the unselected downstream subtrees skip
themselves. Skips cascade — skipping a node can unblock the skip check for nodes further down.

## Input precedence

When a node runs, its inputs are assembled in this order. **Later steps do not overwrite earlier
ones unless explicitly noted.**

| # | Source | Overwrites? |
|---|---|---|
| 1 | Incoming edges | — (first writer) |
| 2 | Flat execution inputs (start nodes only, no incoming edges) | Fills gaps only |
| 3 | Loop virtual inputs (`item`, `index`, previous-iteration value) | Fills gaps only |
| 4 | The node's own config, after `${...}` substitution | Fills gaps only |
| 5 | Execution-time inputs keyed by node id | **Overwrites** |
| 6 | `AgentContext` into `context` | Only if `context` is unset |
| 7 | Ambient fields (`workspace_id`, `conversation_id`, `document_id`) | Fills gaps only |

Two consequences people trip over:

- **An edge always beats the field you typed into the property panel.** Config is step 4; edges
  are step 1. A connected input is not editable in the panel for exactly this reason.
- **Per-node execution inputs beat everything.** That is what makes single-node testing work:
  the test runner supplies inputs keyed by node id and they override the wired graph.

Step 6 is a real bug guard: if an edge already supplied `context` — a RAG retriever's results,
say — the engine will not clobber it with the agent context.

## `${...}` substitution

Inside a node's config, `${name}` is replaced with the execution input called `name`:

```json
{ "url": "https://api.example.com/v1/${resource}",
  "header": "Bearer ${api_token}" }
```

The rules are deliberately narrow:

- Only strings are substituted, recursively through objects and arrays.
- Only **simple** values interpolate — string, number, boolean. An object or array value leaves
  the placeholder untouched.
- An unknown name leaves `${name}` in place rather than substituting an empty string. A missing
  value therefore shows up as a visible placeholder in the request you sent, not as a silent
  blank.

`__env__`-prefixed keys carry skill-resolved secrets. They are available to `${__env__KEY}`
substitution but are **never** exposed as direct node inputs, because node inputs get persisted
alongside outputs and a secret would end up in the execution record.

## Ambient fields

`workspace_id`, `conversation_id`, and `document_id` are injected by the engine from the agent
context or execution metadata. On the canvas these inputs are hidden and normally left blank.

For **tool calls** the injection is a force-override, not a gap-fill: whatever the model put in
the field is replaced. A model cannot widen its own scope by inventing a `workspace_id`. See
[Knowledge Bases & Workspace Context](/docs/data-knowledge-base).

## Compatibility shims worth knowing

- `ai/llm` and `agent/llm` fall back to `raw_input` when `user_message` is empty. Gateway and
  chat entry points pass the user's text as `raw_input`, so a model node works when wired
  directly to a chat input without a mapping node.
- An execution input keyed by node id that is *not* an object is written to `text` for
  `io/input_text`, and to `content` for everything else.

## The Smart Variable node

`utils/smart_var` exists for the case template substitution handles badly: you have a blob of
JSON, a curl command, or a YAML snippet, and you want its fields to become wired inputs.

Paste the text into `template`, and the node extracts variable names and **generates one input
port per variable**. Connect the ports you care about; the node emits the fully substituted
text.

**Extraction modes**:

| Mode | Behaviour |
|---|---|
| `auto` (default) | Detects key names — parses the text as JSON and walks it recursively; falls back to a regex scan when it is not valid JSON |
| `manual` | Only `{{var}}` placeholders |

Duplicate names get an index suffix (`token`, `token_2`, …) so two fields with the same key
become two distinct ports rather than silently colliding in a map. Meaningless words are
filtered out of the candidate list.

**Outputs**:

- `full_text` — the template with every variable substituted; this is what downstream nodes
  usually consume.
- `selected_vars` — only the variables you ticked in `output_vars`, as a key-value map.
- `all_variables` — every name found, which is what populates the picker.
- `preview_content` — a formatted preview for the canvas.
- `statistics` — total / connected / defaulted / missing variable counts, plus template and
  output sizes.

That `statistics` block is the debugging tool: `missing_variables` greater than zero tells you a
port is unconnected before you ever look at the output text.

### The variable picker

`output_vars` is edited through a drawer, not a text field: search across names and
descriptions, tick individually, or select/deselect everything currently matching the filter.
It shows selected-versus-total counts, so on a 60-field payload you can see at a glance how much
you have picked.

## Choosing a mechanism

| Situation | Use |
|---|---|
| One node's output feeds another's input | An edge |
| A fixed config string needs one runtime value | `${...}` in the config |
| A text blob has many fields to wire | `utils/smart_var` |
| A node needs the current workspace or conversation | Nothing — it is ambient |

## Next steps

- [Node Configuration & Schema-Driven Forms](/docs/editor-node-configuration) — where config values come from.
- [Subflows & Batch Subflows](/docs/editor-subflows) — mapping across a nested execution.
- [Execution Model](/docs/exec-model) — when inputs are assembled relative to scheduling.
