Specification
Recursion
A step with a recursion block can spawn a child execution of the same stilt from its own output, enabling recursive refinement.
Recursion
A step with a recursion block can spawn a child execution of the same stilt from its own output, enabling recursive refinement.
How Recursion Works
A step with a recursion block can spawn a child execution of the same stilt. The step runs normally first — fields resolve, the prompt is assembled, the LLM call is made, and the output is stored. Then the recursion check fires. If the current depth hasn't reached maxDepth, a child instance is created. The child runs the same stilt config from step 0, with the step's output replacing input.context. The parent stilt pauses at this step and waits for the child to finish.
The child runs through every step from top to bottom. If the child also reaches the recursion step, it produces its own output, and the same check fires — spawn another child if depth allows. This nesting continues until a level hits maxDepth.
At maxDepth, the recursion step's output is used directly — no child is spawned. That output flows into any remaining steps after it, and the exit step produces the final output for that deepest instance.
That deepest instance's exit output then bubbles back up. The level above receives it, and it replaces the recursion step's stored output. Any steps after the recursion step in that level now see the child's refined output instead of the raw step output. That level's exit output bubbles up again.
This continues until the original parent receives the final refined output. The parent's recursion step output is replaced, any downstream steps run with the refined output, and the parent's exit step produces the final answer returned to the caller.
maxDepth 2 — Step by Step
Consider a stilt with three steps where refine has maxDepth: 2:
steps:
- id: analyze
name: Analyze
type: normal
fields:
- name: Context
type: text
from: input.context
systemPrompt: "Identify key themes and weaknesses."
- id: refine
name: Refine
type: normal
recursion:
maxDepth: 2
fields:
- name: Context
type: text
from: input.context
systemPrompt: "Improve depth and coherence."
- id: polish
name: Polish
type: normal
fields:
- name: Refined
type: ingest
from:
stepId: refine
loopRef: current
systemPrompt: "Final polish for clarity and tone."
exit: polishLevel 0 (original call, depth=0): The user's question arrives as input.context. The analyze step reads it and identifies themes. The refine step reads the same input, produces a draft — labeled R0. Recursion check: depth 0 < maxDepth 2, so a child is spawned with input.context = R0. The parent pauses here.
Level 1 (first child, depth=1): The child starts from step 0 with input.context = R0. The analyze step reads R0 and identifies what to improve. The refine step reads R0, produces R1. Recursion check: depth 1 < maxDepth 2, so another child is spawned with input.context = R1. This child pauses here.
Level 2 (grandchild, depth=2): The grandchild starts from step 0 with input.context = R1. The analyze step reads R1. The refine step reads R1, produces R2. Recursion check: depth 2 = maxDepth 2 — no more recursion. R2 is the raw output. Now the polish step runs — it ingests R2 via the Refined field and produces P2. Since exit: polish, the grandchild returns P2 as its final output.
Back to Level 1: The grandchild returned P2. This replaces the refine step's stored output (was R1, now P2). The polish step runs — it ingests P2 and produces P1. The child returns P1.
Back to Level 0: The child returned P1. This replaces the refine step's stored output (was R0, now P1). The polish step runs — it ingests P1 and produces P0. Since exit: polish, P0 is the stilt's final answer.
The output went through three rounds of analyze+refine (one per depth level) and three rounds of polish (one per depth level). Each level builds on the previous level's result.
If the recursion step is also the exit step — meaning there are no steps after it — the behavior is simpler. The deepest level's exit output bubbles straight up through each level without any further processing at intermediate levels, because there are no steps to run after the recursion step.
Depth Control
The recursion block has one property: maxDepth. It can be a hardcoded integer or a knob reference:
recursion:
maxDepth: 3
recursion:
maxDepth: "{{knobs.iterations}}"maxDepth controls how many levels of recursion can happen. At depth 0 (the original run), recursion is allowed if depth < maxDepth. Each spawned child increments depth by 1. When depth equals maxDepth, the step's raw output is used — no child spawns.
Using a knob reference lets the caller control depth at call time. A recursion knob type is the conventional way to expose this (covered in section 3.1).
Only one step in the entire stilt can have a recursion block. If two steps have recursion, validation fails before the stilt runs.
What the Child Sees
Every child instance runs the same stilt config — the same steps, fields, system prompts, and exit step. Three things change:
input.context is replaced. Instead of the caller's original message, the child's input.context becomes the parent step's output. Every text field with from: input.context in the child reads this refined output. This was covered in section 5.2 — during recursion, input.context becomes whatever the parent recursion step produced.
maxLoops is always 1. Regardless of the parent's loop count, the child runs a single pass through the steps. No looping in the child. This means loopRef: accumulate in the child collects nothing — there are no previous loops. Fields using loopRef: 0 work since loop 0 is the only loop that exists. The multi_ingest field on the child's recursion step with loopRef: accumulate will be empty on the child's only pass.
Knobs are inherited. The child gets the same knob values as the parent. If the parent had coverage: 8, the child also has coverage: 8. Node counts driven by knobs match the parent's. The recursion knob itself is also inherited, but since the child checks depth >= maxDepth, the knob value still controls when recursion stops.
Loops and Recursion Together
Loops and recursion are independent dimensions of control flow. The loop counter doesn't advance during child execution.
When a looping stilt has a recursion step, the parent hits the recursion step on loop 0, spawns a child, waits for it, gets the refined result, stores it, then moves to loop 1. On loop 1, the recursion step runs again, spawns another child, waits, gets the result. Each loop iteration gets its own independent child. The child always runs with maxLoops: 1 — it does one clean pass.
The recursion step's refined output (after the child returns) is what gets stored for that loop iteration. A multi_ingest field with loopRef: accumulate on the recursion step collects the refined outputs — one per loop. Loop 2 sees the refined output from loop 0 and loop 1.
Example: a stilt with 3 loops and maxDepth 2. The recursion step runs 3 times (once per loop). Each time, it spawns a child that itself spawns a grandchild. The grandchild's polished output bubbles up to the child, then to the parent. The parent stores that polished output for that loop iteration. Loop 2's multi_ingest field sees: refined output from loop 0, refined output from loop 1 — both having gone through two levels of recursion.
Example
A recursive refinement step inside a looping stilt:
name: Recursive Draft Refinement
allowedTargets:
strategy: universal
exit: final
knobs:
iterations:
name: Iterations
type: recursion
input: numerical
default: 2
min: 1
max: 5
rounds:
name: Rounds
type: loops
input: numerical
default: 3
min: 1
max: 5
steps:
- id: draft
name: Draft
type: normal
fields:
- name: Context
type: text
from: input.context
systemPrompt: "Write an initial draft."
- id: final
name: Final Draft
type: normal
recursion:
maxDepth: "{{knobs.iterations}}"
fields:
- name: Context
type: text
from: input.context
- name: Earlier
type: multi_ingest
from:
- stepId: final
loopRef: accumulate
systemPrompt: "Review and improve. Build on earlier drafts."Walkthrough with the default knobs (3 rounds, maxDepth 2): the stilt runs 3 loops. On each loop, draft produces an initial draft from the context, then final reads the context plus all previous loops' final outputs (accumulated). final produces its output, then spawns a child (depth 1). The child's input.context is that output — it runs the same two steps, final spawns a grandchild (depth 2). The grandchild runs but doesn't recurse further (depth = maxDepth). The grandchild's exit output bubbles up through both levels, replacing the parent final step's output. That refined output is stored for this loop iteration. On the next loop, the Earlier field accumulates one more entry.
Common Validation Failures
Putting recursion on a group step — groups can't recurse, only normal and sequential steps can. Having more than one step with a recursion block — only one step per stilt can recurse. Referencing a knob in maxDepth that doesn't exist in the knobs section. Setting maxDepth to 0 — the step produces its output but no child ever spawns, making the recursion block pointless.