Gates
Gates: The if/then/else Block
Gates: The if/then/else Block
The if/then/else block is LADR's typed conditional. After a step's nodes complete, the gate evaluates a condition against each node's output, prunes the non-matching nodes, then fires one of four actions (continue, jump, write, or abort) based on whether any survivors remain. This is the primitive that makes ladders adaptive: same config, different behavior per call based on what the model actually produced.
Why gates exist
Without gates, every step's output is used unconditionally. A 5-node generate step produces 5 candidates, and all 5 flow into the next step. There's no way to prune the bad ones, no way to branch based on what was generated, no way to abort early on a clearly-wrong answer.
That covers simple cases (one-shot answer, refine, return) but misses the entire class of adaptive reasoning:
- Tree-of-Thoughts. Generate many candidates; keep only the ones that pass a verifier; drop the rest before they waste downstream cost.
- Reflexion. Generate, verify, if verification failed jump back and try again with feedback.
- Early exit. If the verifier is highly confident, skip the rest of the pipeline and synthesize immediately.
- Conditional persistence. If the output is worth saving, write it to a slate; otherwise skip.
Gates enable all of these. The gate is a typed predicate evaluated against a step's output, paired with actions for what to do on match and on miss. The runtime prunes non-matching nodes per-node first, then fires the step-level action based on whether any survivors remain.
The typed form (if: { integerRange: [4,5] }) is more powerful than the legacy string form (continueIf: "READY") because it handles numeric ranges, substrings, set membership, and JSON Schema validation. Existing ladders using continueIf continue to work unchanged; new ladders should prefer if:.
Anatomy of an if block
Every if: block has three parts:
- A condition. One of twelve condition keys: nine leaf predicates (
equals,integerEquals,integerRange,numberEquals,numberRange,contains,in,jsonMatches,matches) or three compound combinators (and,or,not) that nest recursively. Declared as a single-key object. - A
thenaction. What to do when the condition passes (or when any survivors remain). Default:continue. - An
elseaction. What to do when the condition fails (or when no survivors remain). Default:abort.
- id: classify
systemPrompt: "Output a single integer rating from 1 to 5."
if:
integerEquals: 5
then: continue # default; may be omitted
else: { jump: { stepId: refine } } # default is abort; override hereBoth then and else accept any of the four action forms (continue, abort, jump, write). Defaults: then: continue and else: abort. Omit then: to accept the default; omit else: to accept abort-on-failure.
The shorthand form is a bare string, equivalent to equals with default actions:
if: "READY"
# desugars to:
# if: { equals: "READY", then: continue, else: abort }This is exactly the legacy continueIf behavior. Existing ladders using continueIf work unchanged; the desugaring happens at parse time.
How the gate evaluates
The evaluation has two phases.
First, per-node pruning. For multi-node steps, the gate condition is checked against every node's output individually. Non-matching nodes are pruned; their outputs are no longer visible to downstream consumers. This is what preserves Tree-of-Thoughts pruning behavior: a 5-node generate step with a strict gate might produce 5 candidates but only 2 survivors visible downstream.
Second, step-level action. After pruning reduces the survivors:
- If any survivors remain, the
then:action fires. - If no survivors remain (every node failed the gate), the
else:action fires.
For single-node steps, the same logic applies but with one node: if it passes, then: fires; if it fails, else: fires.
- id: verify
nodes: 3 # three verifiers run in parallel
systemPrompt: 'Score 1-5. Output JSON {"score": N, "verdict": "accept|reject"}.'
if:
jsonMatches:
type: object
properties:
score: { type: number, minimum: 4 }
verdict: { type: string, enum: [accept] }
then: continue # survivor verifiers continue
else:
write: # rejected verifiers write a lesson on the way out
to: { slate: "Memory", folder: lessons, file: failures.md }
from: output
on: appendIn this example:
- Each of the 3 verifier nodes produces JSON.
- Per-node pruning keeps verifiers that emitted
{score: >=4, verdict: "accept"}; prunes the rest. - The pruned verifiers' outputs are written to the lessons slate (via the
else:write action).
The composed behavior: verifiers that pass survive; verifiers that fail contribute their analysis to the lessons slate. Pruning becomes productive signal — every pruned node teaches the ladder something for next time.
Validation rules
Gate-related rules checked at config load time:
if:andcontinueIf:mutual exclusion. Declaring both fails:Step "X": cannot declare both "continueIf" (deprecated) and "if" — use only "if".integerRangewell-formed.min <= maxrequired. Inverted ranges fail:Step "X": if.integerRange has min (5) > max (3).numberRangewell-formed. Same rule.innon-empty.if: { in: [] }fails:Step "X": if.in has empty array (must have at least 1 value).jsonMatchesvalid JSON. Malformed JSON fails:Step "X": if.jsonMatches is not valid JSON: ....jsonMatchesnon-empty. Empty schema fails.matchesvalid regex. Invalid patterns fail at load time:Step "X": if.matches has invalid regex: ....and/ornon-empty. Empty arrays fail:Step "X": if.and has empty array (must have at least 1 condition).- Recursive sub-condition validation. All sub-conditions inside
and/or/notare validated recursively — a bad regex nested three levels deep is still caught. - Jump targets.
then: { jump: { stepId: X } }requires stepXto exist. Cross-ladderladderIdvalues must parse as valid identifiers. - Write targets.
then: { write: { to: { slate: X, folder: Y } } }requires slateXand folderYto be declared.
Every error includes the step's id so the author can locate the problem in the config.