Foundations
Node Addressing: loopRef, nodeRef, pruned state
Node Addressing: loopRef, nodeRef, pruned state
Every output in a ladder is addressable by a triple: (stepId, loopRef, nodeRef). Understanding this addressing scheme is essential for wiring fields correctly. This article covers the three loopRef keywords, the three nodeRef keywords, and how pruning affects node numbers.
The addressing triple
Every LLM call output in a ladder is stored as a node belonging to a step, in a specific loop iteration. To read an output, you address it with three coordinates:
| Coordinate | Values | Meaning |
|---|---|---|
stepId | any step ID | Which step produced the output |
loopRef | current, previous, accumulate, or a number | Which loop iteration |
nodeRef | current, previous, accumulate, or omitted | Which node within the step |
# Read node 1 of step "draft" from the current loop
- name: Draft
type: ingest
from: { stepId: draft, loopRef: current }
# Read all nodes of step "generate" from the previous loop
- name: PreviousAttempts
type: multi_ingest
from: [{ stepId: generate, loopRef: previous, nodeRef: accumulate }]
# Read all nodes across all prior loops
- name: AllHistory
type: multi_ingest
from: [{ stepId: generate, loopRef: accumulate, nodeRef: accumulate }]loopRef values
loopRef selects which loop iteration(s) to read from. Four values:
current — the loop iteration currently executing. If the pipeline is on loop 3, current reads from loop 3. This is the most common value for same-loop data dependencies.
previous — the loop iteration before the current one. If the pipeline is on loop 3, previous reads from loop 2. Used for cross-loop refinement: "take the previous loop's output and improve it."
accumulate — all prior loop iterations (0 through current-1). Expands to multiple reads, one per loop. Only valid in multi_ingest fields. Used for aggregation: "read all previous attempts."
Numeric (0, 1, 2, ...) — a specific loop iteration by index. Rarely used; useful for debugging or fixed-depth patterns.
# Same-loop: read draft from the current loop
from: { stepId: draft, loopRef: current }
# Cross-loop: read draft from the previous loop
from: { stepId: draft, loopRef: previous }
# Accumulate: read all prior loops' drafts
from: [{ stepId: draft, loopRef: accumulate, nodeRef: accumulate }]Negative indices and indices beyond the current loop resolve to empty. The runtime does not error on out-of-range loop references — it simply returns no data.
nodeRef values
nodeRef selects which node(s) within a step to read. Three keyword values:
current — the node matching the current execution context. If the current step is executing node 3, and the field references { stepId: draft, nodeRef: current }, it reads node 3 of the draft step. This enables parallel alignment: "verifier node 3 evaluates draft node 3."
previous — the node before the current one. If current is node 3, reads node 2. If current is node 1, falls back to node 1 (there's no node 0). Used in sequential steps for chain-of-refinement: "improve on what the previous node produced."
accumulate — all nodes of the referenced step. Expands to multiple reads. Only valid in multi_ingest fields. Used for aggregation: "read all candidates that survived the gate."
Omitted — when nodeRef is not specified, the step's combined output is read (all nodes joined with double newlines). This is different from accumulate — omitted reads the step-level output, while accumulate reads each node individually.
# Parallel alignment: verifier N reads draft N
- name: Candidate
type: ingest
from: { stepId: draft, loopRef: current, nodeRef: current }
# Sequential chain: node N reads node N-1
- name: Previous
type: ingest
from: { stepId: refine, loopRef: current, nodeRef: previous }
skipFirstNode: true
# Aggregate: read all surviving nodes individually
- name: Survivors
type: multi_ingest
from: [{ stepId: verify, loopRef: current, nodeRef: accumulate }]How pruning affects node numbers
When a gate prunes nodes, the surviving nodes are remapped to a contiguous range. This is the pruned: true flag on NodesFrom.
Example: A step has 5 nodes. The gate prunes nodes 1, 3, 4. Survivors: nodes 2, 5.
After pruning, downstream consumers see the survivors as a contiguous sequence:
| Physical node | Survivor index | Visible as |
|---|---|---|
| 2 | 1 | node 1 |
| 5 | 2 | node 2 |
A downstream field with nodeRef: current reading node 1 gets the survivor node 1 (physical node 2), not the physical node 1 (which was pruned).
# Read the survivors count (not the original node count)
nodes: { from: { stepId: generate, loopRef: current, pruned: true } }The pruned: true flag on a NodesFrom tells the runtime to count only surviving nodes, not all nodes. This is how a synthesize step knows how many candidates survived the gate.
Without pruning context: If a field references a step that had nodes pruned, but doesn't use pruned: true, the runtime applies the remap automatically for current and previous nodeRefs. The survivor mapping is tracked in the runtime's PrunedNodeMap.
Key insight: Authors rarely need to think about pruning explicitly. The runtime handles the remapping. But when counting nodes for nodes: declarations, use pruned: true to get the survivor count rather than the original count.
Common addressing patterns
Parallel evaluation (ToT). N generators produce candidates; N evaluators score them in parallel. Each evaluator reads its corresponding generator node.
- id: generate
nodes: 5
# ...
- id: evaluate
nodes: 5
fields:
- name: Candidate
type: ingest
from: { stepId: generate, loopRef: current, nodeRef: current }Evaluator node 3 reads generator node 3. One-to-one alignment.
Aggregation. A synthesis step reads all surviving candidates.
- id: synthesize
fields:
- name: Candidates
type: multi_ingest
from: [{ stepId: evaluate, loopRef: current, nodeRef: accumulate }]All surviving evaluate nodes are joined into a numbered list.
Cross-loop refinement. Each loop improves on the previous loop's output.
- id: improve
fields:
- name: Previous
type: ingest
from: { stepId: improve, loopRef: previous }
fallback: ""On loop 1, previous reads from loop 0 (which doesn't exist → empty → fallback). On loop 2, reads loop 1's output. On loop 3, reads loop 2's output. Each loop builds on the last.
Full history accumulation. Read all outputs from all prior loops.
- name: History
type: multi_ingest
from: [{ stepId: improve, loopRef: accumulate, nodeRef: accumulate }]On loop 3, this reads all nodes from loops 0, 1, and 2. The resulting list contains every attempt so far.