# Validators

A validator is the small piece of logic that turns a node's raw output into a
transition key for the `on:` map. AI nodes use validators to parse a JSON
envelope from the model; script nodes use them to interpret the value the
script returns. This page covers the three validator kinds shared by
`script` and `ai_judge`, the two additional kinds `consensus` adds, the JSON
shape each expects, and the matching `on:` keys you must declare.

You will use validators on `script`, `ai_judge`, and `consensus` nodes.
`ai_agent` nodes do not have validators because they cannot branch.

## How validators work

When a node finishes, the executor produces a raw value. The validator maps
that value to one of the keys in the node's `on:` map:

- `boolean` → `'true'` or `'false'`
- `enum` → one of the named routes
- `confidence` → `'true'` if score meets the threshold, else `'false'`

If the value cannot be parsed (for example, the model returned malformed
JSON), the AI executors retry up to `maxRetries` times before failing the
node. The retry prompt includes the previous response and the parser error
so the model can self-correct.

`maxRetries` is `0–5` for `ai_judge` and `ai_agent` (default `1`) and `0–3` for `consensus`
(default `1`).

## boolean

The simplest validator. Output is either `true` or `false`, and the `on:` map
must declare both keys.

### AI nodes

The prompt is automatically appended with this instruction:

```text
Respond with a JSON object and nothing else — no prose before or after,
no markdown fences:
{
  "reasoning": "<your step-by-step reasoning>",
  "result": <true or false>
}
```

The validator extracts the JSON, parses `result`, and maps `true → 'true'`,
`false → 'false'`.

```yaml
gate:
  type: ai_judge
  label: Should we deploy?
  model: claude-sonnet-4-5
  prompt: |
    Given the build output below, is it safe to deploy?

    {{ build.stdout }}
  validator:
    kind: boolean
  maxRetries: 2
  on:
    'true': deploy
    'false': abort
```

### Script nodes

The validator checks whether the value the script returns is truthy:

- A truthy return value → `'true'`
- A falsy return value (`false`, `0`, `''`, `null`, `undefined`) → `'false'`

Note that a script which _throws_, or a shell command that exits non-zero
when you did not use `.nothrow()`, is a hard node failure (status `failed`,
no transition), not a routed `'false'`. To branch on whether a command
succeeded, run it with `.nothrow()` and return the check yourself:

```yaml
test:
  type: script
  label: Run tests
  script: |
    import { $ } from 'bun';
    const res = await $`npm test`.nothrow();
    export default res.exitCode === 0;
  validator:
    kind: boolean
  on:
    'true': deploy
    'false': abort
```

Both keys are required. If you only declare `'true'`, the loader rejects the
workflow.

## enum

Routes to one of a named set of routes. Use this when the answer is a small
fixed set (e.g. `low | medium | high` risk classification). Enum validators
are supported on `ai_judge` and `script`. Consensus does **not** support
enum. See [Consensus](/docs/nodes/consensus) for why.

The validator declares the allowed values under `routes:`. Your `on:` map
must declare a key for every route name (and only those names).

### AI nodes

The prompt is appended with:

```text
Respond with a JSON object and nothing else — no prose before or after,
no markdown fences:
{
  "reasoning": "<your step-by-step reasoning>",
  "result": "<exactly one of: low, medium, high>"
}
The "result" value must be reproduced exactly (case-sensitive).
```

If the model returns a `result` that is not one of the declared routes, the
attempt is treated as a validator failure and the executor retries.

```yaml
classify:
  type: ai_judge
  label: Classify risk
  model: claude-sonnet-4-5
  prompt: |
    Classify the risk level of this change.

    Diff: {{ diff.stdout }}
  validator:
    kind: enum
    routes: [low, medium, high]
  maxRetries: 2
  on:
    low: auto-merge
    medium: human-review
    high: block
```

`routes` cannot be empty: that would make it impossible for the workflow to
advance past the node, and the integrity check rejects it.

### Script nodes

For enum validators, the script must **return** the chosen route name. The
executor coerces the returned value with `String(...)` and matches it against
the declared `on:` keys:

- If it exactly matches one of the keys → that route is taken.
- If it does not match any key → the node fails.

Return the routing string. Don't print it:

```yaml
classify:
  type: script
  label: Classify environment
  script: |
    export default process.env.ENV === 'production' ? 'high' : 'low';
  validator:
    kind: enum
    routes: [low, high]
  on:
    low: stage
    high: production
```

For richer branching logic prefer a `condition` node downstream of a
boolean `script` node.

## confidence

A graded boolean. The model returns a numeric score; the validator routes to
`'true'` if the score meets a threshold, otherwise `'false'`.

`threshold` is required and must be in the range `0–100`.

### AI nodes

The prompt is appended with:

```text
Respond with a JSON object and nothing else — no prose before or after,
no markdown fences:
{
  "reasoning": "<your step-by-step reasoning>",
  "score": <integer from 0 to 100>
}
```

The validator parses `score` and routes:

- `score >= threshold` → `'true'`
- `score < threshold` → `'false'`

The numeric score is also written to `variables.<id>.score` so downstream
nodes can reference it (see
[Output chaining](/docs/authoring/output-chaining)).

```yaml
quality-check:
  type: ai_judge
  label: How good is this answer?
  model: claude-sonnet-4-5
  prompt: |
    Score the quality of this answer from 0 to 100.

    Answer: {{ draft.response }}
  validator:
    kind: confidence
    threshold: 80
  on:
    'true': publish
    'false': revise
```

Both `'true'` and `'false'` keys are required, even when one branch loops
back to the same step.

### Script nodes

`script` nodes **do** support `confidence`: return a number (or an object with
a numeric `score` field) and it routes `'true'` at or above the threshold,
`'false'` below. See the [script node](/docs/nodes/script) for details.

## Consensus and validators

[Consensus](/docs/nodes/consensus) has its own validator schema with four
kinds: `boolean`, `confidence`, `answer`, and `most_consistent`. `enum` is
not supported, because there's no well-defined per-agent approval rule for
an arbitrary route name.

`boolean` and `confidence` are gate modes. Each agent approves or rejects
independently (for `confidence`, an agent approves if its score meets the
threshold), and the node routes to `approved` once `quorum` agents approve,
otherwise `rejected`:

```yaml
vote:
  type: consensus
  label: Multi-agent gate
  model: claude-sonnet-4-5
  prompt: Is the change safe to merge?
  agentCount: 3
  quorum: 2
  validator:
    kind: boolean
  on:
    approved: merge
    rejected: block
```

`answer` and `most_consistent` are consensus-only kinds with no `ai_judge`
or `script` equivalent. Each agent produces a free-form answer (`answer`
mode can restrict this to a fixed `choices` list), and the group votes by
plurality. These modes route to `decided` when the group converges on one
answer, or `undecided` otherwise, never `approved`/`rejected`:

```yaml
diagnose:
  type: consensus
  label: What's the likely root cause?
  model: claude-sonnet-4-5
  prompt: What is the most likely root cause of this incident?
  agentCount: 5
  quorum: 3
  validator:
    kind: answer
  on:
    decided: report
    undecided: escalate
```

Whichever kind you use, `on:` must declare both keys for that mode: never
`'true'`/`'false'`. `most_consistent` (self-consistency, or USC) skips the
per-agent vote entirely: every agent answers freely, and a separate
aggregation pass picks the single most-consistent response.

## Retries and self-correction

When an AI node returns malformed JSON or a value that does not match the
validator (e.g. an enum result not in `routes`), the executor:

1. Records the failure reason.
2. Builds a retry prompt that includes the original prompt, the bad
   response, and the parser error.
3. Sends the retry to the model.
4. Repeats up to `maxRetries` times.

If all retries fail, the node fails and the run halts at that node. You can
inspect the per-attempt trace in the web UI to see what the model said.

Set `maxRetries: 0` to disable retries entirely. The default of `1` is a
good starting point: most malformed-JSON cases recover on the second attempt.

## Where to next

- [AI judge node](/docs/nodes/ai-judge) — the most common home for a
  validator.
- [Consensus node](/docs/nodes/consensus) — quorum semantics and per-agent
  validator behaviour.
- [Script node](/docs/nodes/script) — return-value mapping in
  more detail.
- [Output chaining](/docs/authoring/output-chaining) — what the validator
  writes back to `variables`.
