Redeo Docs
DocsLADR / Gates: The if/then/else Block

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:

  1. 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.
  2. A then action. What to do when the condition passes (or when any survivors remain). Default: continue.
  3. An else action. What to do when the condition fails (or when no survivors remain). Default: abort.
yaml
- 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 here

Both 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:

yaml
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.

yaml
- 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: append

In 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: and continueIf: mutual exclusion. Declaring both fails: Step "X": cannot declare both "continueIf" (deprecated) and "if" — use only "if".
  • integerRange well-formed. min <= max required. Inverted ranges fail: Step "X": if.integerRange has min (5) > max (3).
  • numberRange well-formed. Same rule.
  • in non-empty. if: { in: [] } fails: Step "X": if.in has empty array (must have at least 1 value).
  • jsonMatches valid JSON. Malformed JSON fails: Step "X": if.jsonMatches is not valid JSON: ....
  • jsonMatches non-empty. Empty schema fails.
  • matches valid regex. Invalid patterns fail at load time: Step "X": if.matches has invalid regex: ....
  • and / or non-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 / not are validated recursively — a bad regex nested three levels deep is still caught.
  • Jump targets. then: { jump: { stepId: X } } requires step X to exist. Cross-ladder ladderId values must parse as valid identifiers.
  • Write targets. then: { write: { to: { slate: X, folder: Y } } } requires slate X and folder Y to be declared.

Every error includes the step's id so the author can locate the problem in the config.