Gates
Condition Keys Reference
Condition Keys Reference
Twelve condition keys are available as gate conditions: nine leaf predicates that test the output directly, and three compound combinators (and, or, not) that compose other conditions into a recursive tree. Each leaf is a single-key object; the key names the predicate, the value is its argument. Choose the most specific predicate that works for your output format.
The nine leaf condition keys
| Key | Argument | What it checks |
|---|---|---|
equals | string | Exact string match against the trimmed output. |
integerEquals | integer | Output parsed as integer, compared equal. Tolerates trailing .0. |
integerRange | [a, b] | Output parsed as integer, must fall within inclusive [a, b]. |
numberEquals | number | Output parsed as float, compared equal (epsilon tolerance). |
numberRange | [a, b] | Output parsed as float, within inclusive range. |
contains | string | Substring match. More forgiving than equals — handles prose around the marker. |
in | [a, b, c, ...] | Output is one of the declared values. Handles LLM phrasing variance ("yes", "Y", "accept"). |
jsonMatches | JSON Schema | First JSON block extracted from output, validated against the schema. The structured gate. |
matches | regex (RE2) | Regex search against the output. Partial match — the pattern can appear anywhere in the string. |
# String equality
if: { equals: "READY" }
# Integer comparison (parsing-tolerant)
if: { integerEquals: 5 }
if: { integerRange: [4, 5] }
# Floating-point comparison
if: { numberEquals: 0.95 }
if: { numberRange: [0.7, 1.0] }
# Substring (handles prose around the marker)
if: { contains: "VERDICT: ACCEPT" }
# Set membership (handles phrasing variance)
if: { in: ["yes", "Y", "accept", "ACCEPT"] }
# Structured (extract JSON, validate schema)
if:
jsonMatches:
type: object
required: [verdict, score]
properties:
verdict: { type: string, enum: [accept] }
score: { type: number, minimum: 4 }
# Regex (partial match, case-insensitive flag)
if: { matches: "(?i)\\bPASS\\b" }Parsing behavior
The integer and number predicates trim whitespace, then attempt to parse the entire remaining string. They do not extract numbers from prose.
- An output of
"5"or"5.0"parses to the integer 5 (trailing.0is tolerated). - An output of
"Score: 5"or"The answer is 5."fails — the whole string is not a number.
Design system prompts to emit the number alone. If the LLM wraps the number in prose, use contains or jsonMatches instead.
Trailing .0 tolerance. LLMs sometimes emit 5.0 when asked for an integer. The integerEquals and integerRange predicates parse 5.0 as the integer 5. This is the only parsing tolerance beyond whitespace trimming.
Number epsilon. numberEquals uses an epsilon tolerance of 1e-9. This means 0.95 matches 0.9500000001 but not 0.9501. For practical purposes, numberEquals is exact for values produced by JSON serialization.
matches: Regex gates
matches applies an RE2 regular expression to the trimmed output. The match is a search (partial match), not a full-match — the pattern can appear anywhere in the string. This makes matches strictly more expressive than contains: any contains check is equivalent to a matches with the same literal string, but matches also handles character classes, anchors, quantifiers, alternation, and capture groups.
# Case-insensitive word match (equivalent to "in" with multiple casings)
if: { matches: "(?i)\\b(yes|y|accept)\\b" }
# Anchored: entire output must be a 4-digit year
if: { matches: "^\\d{4}$" }
# Extract a score embedded in prose: "Score: 42" or "score=42"
if: { matches: "score[:=]\\s*\\d+" }
# Reject outputs containing markdown code fences
if: { matches: "```", then: { jump: { stepId: rewrite } }, else: continue }Validation at parse time. The regex is compiled when the config is loaded. An invalid pattern fails immediately with a clear error:
Step "verify": if.matches has invalid regex: error parsing regexp: ...
This means typos in the pattern are caught before any pipeline run, not at runtime when a node happens to produce output.
RE2 syntax. The engine uses Go's regexp package, which implements RE2. This means:
- Supported:
.,*,+,?,{n,m},[abc],[^abc],^,$,\\b,\\d,\\w,\\s,|,(...),(?i)/(?s)/(?m)flags. - Not supported: backreferences (`\1`), lookaround (`(?<=...)`), possessive quantifiers. These are intentionally excluded for linear-time guarantees.
When to use matches vs contains. Use contains when you need a simple literal substring — it's clearer and faster. Use matches when the output has structure that a literal can't capture: variable casing, optional whitespace, multiple alternative spellings, or positional constraints (anchored to start/end).
| Need | Use |
|---|---|
| Literal substring | contains |
| Case-insensitive substring | matches: "(?i)keyword" |
| Match at start/end only | matches: "^prefix" or matches: "suffix$" |
| Multiple alternative patterns | `matches: "cat |
| Format validation (ID, date, code) | matches: "^\\d{4}-\\d{2}$" |
Choosing the right condition
Use the most specific predicate that works.
equalsis brittle — one whitespace difference fails. Use only when you control the output format exactly.containsis more forgiving — it handles prose around a marker. Good for "output contains VERDICT: ACCEPT" patterns.matchesis the generalization ofcontains— any patterncontainscan match,matchescan too, plus case-insensitivity, anchors, alternation, and character classes.integerRange/numberRangeare best for scored outputs (1-5 ratings, 0.0-1.0 confidence).jsonMatchesis best for structured outputs. It extracts JSON from the output and validates against a schema. The most powerful leaf condition.inis best when the LLM might phrase the same answer multiple ways ("yes","Y","accept").
| Output format | Recommended condition |
|---|---|
Bare token ("READY", "done") | equals or in |
Integer score ("4", "5.0") | integerEquals or integerRange |
Float score ("0.85") | numberEquals or numberRange |
Prose with marker ("VERDICT: ACCEPT after analysis") | contains |
| Case-insensitive or pattern-based marker | matches |
| Structured format (date, ID, code) | matches with anchors |
JSON object ({"score": 4, "verdict": "accept"}) | jsonMatches |
| Multiple possible phrasings | in or matches with alternation |
| Need to combine multiple checks | and / or / not (see below) |
If your system prompt produces structured JSON, always prefer jsonMatches over string parsing. It handles extraction (code fences, embedded JSON), validation (schema constraints), and is immune to prose formatting variance.
Compound conditions: and / or / not
Three structural combinators compose leaf conditions (or other compounds) into a recursive tree. This makes the gate expressive enough for any boolean logic without changing the output format or adding new leaf predicates.
| Key | Argument | Semantics |
|---|---|---|
and | [cond, cond, ...] | All sub-conditions must pass. Short-circuits on first failure. |
or | [cond, cond, ...] | Any sub-condition must pass. Short-circuits on first match. |
not | {cond | Negates the sub-condition. Passes when the inner condition fails. |
Each sub-condition is itself a full condition object — leaf or compound. Nesting is unlimited.
# Accept only if output contains "READY" AND has a digit somewhere
if:
and:
- { contains: "READY" }
- { matches: "\\d" }
# Accept if any common affirmative phrasing appears (case-insensitive)
if:
or:
- { matches: "(?i)\\byes\\b" }
- { matches: "(?i)\\baccept\\b" }
- { matches: "(?i)\\bapprove\\b" }
# Accept anything that does NOT contain "ERROR"
if:
not: { contains: "ERROR" }Nesting. Compound conditions compose freely. A realistic gate might combine a positive check with a negative exclusion:
# Accept high-confidence JSON that is NOT a duplicate
if:
and:
- jsonMatches:
type: object
required: [fact, confidence]
properties:
confidence: { type: number, minimum: 0.8 }
- not:
contains: "DUPLICATE"
then: continue
else: abortThis gate passes only when the output is valid JSON with confidence >= 0.8 and the text does not contain the literal "DUPLICATE" anywhere. Either condition alone is insufficient.
Short-circuit evaluation. and stops at the first failing sub-condition; or stops at the first passing one. The reason string in the gate result identifies which sub-condition caused the outcome. This means expensive conditions (like jsonMatches) should be placed last in an and chain — if an earlier contains fails, the JSON extraction never runs.
Empty arrays. An empty and: [] vacuously passes (all zero conditions hold). An empty or: [] vacuously fails (no condition can pass). This is standard boolean logic. However, the validator rejects both as configuration errors — if.and has empty array (must have at least 1 condition) — to prevent accidental no-op gates.
Validation. All sub-conditions are recursively validated at config load time. A regex inside a not is compile-checked; a jsonMatches inside an and is JSON-parsed; an inverted integerRange inside an or is caught. The error messages include the step ID for easy debugging.
Worked examples
Single-threshold accept/reject.
- id: verify
systemPrompt: 'Score 1-5. Output the integer alone.'
if:
integerRange: [4, 5]
then: continue
else: { jump: { stepId: refine } }Confidence-conditional early exit. Use jsonMatches to extract a confidence field and skip ahead when it's high.
- id: classify
systemPrompt: 'Output JSON {"confidence": "high"|"medium"|"low"}.'
if:
jsonMatches:
type: object
properties:
confidence: { type: string, enum: [high] }
then: { jump: { stepId: synthesize } }
else: continue # fall through to gather moreMulti-verifier majority vote. Run multiple verifiers; the gate naturally prunes dissenters.
- id: verify
nodes: 5
systemPrompt: 'Score 1-5. Output JSON {"score": N, "verdict": "accept"|"reject"}.'
if:
jsonMatches:
type: object
properties:
verdict: { type: string, enum: [accept] }
score: { type: number, minimum: 4 }
then: continue # only accepting verifiers survive
else: abortDownstream multi_ingest: { nodeRef: accumulate } reads all surviving (accepting) verifiers. Majority is implicit in the count of survivors.
Phrasing-tolerant acceptance. Use in to handle LLM phrasing variance.
- id: classify
systemPrompt: 'Output "yes" or "no".'
if:
in: ["yes", "Y", "YES", "accept", "ACCEPT", "true", "True"]
then: continue
else: abortThe gate accepts any reasonable affirmative phrasing without needing jsonMatches.
Regex format validation. Use matches when the output must conform to a specific pattern — a date, an ID, a code — and you don't want the overhead of jsonMatches.
- id: extract_date
systemPrompt: 'Extract the publication date. Output YYYY-MM-DD only.'
if:
matches: "^\\d{4}-\\d{2}-\\d{2}$"
then: continue
else: { jump: { stepId: retry } } # malformed date; try againThe anchored pattern (^ ... $) rejects outputs with extra prose. Without anchors, matches would accept "The date is 2024-01-15 and ..." — the pattern appears as a substring. Use anchors when you need the entire output to match.
Compound gate: accept-with-exclusion. Combine and + not to accept structured output that passes a quality bar but excludes known-bad patterns.
- id: generate_and_filter
nodes: 5
systemPrompt: 'Generate a concise answer. Output JSON {"answer": "...", "confidence": 0.0-1.0}.'
if:
and:
- jsonMatches:
type: object
required: [answer, confidence]
properties:
answer: { type: string, minLength: 10 }
confidence: { type: number, minimum: 0.7 }
- not:
matches: "(?i)(I cannot|I'm sorry|as an AI)"
then: continue # high-quality, non-refusal answers survive
else: abortThis is a common production pattern: the jsonMatches check ensures structural correctness and confidence; the not check filters out LLM refusals that happen to be wrapped in valid JSON. The and short-circuits — if the JSON fails validation, the refusal regex is never evaluated.
Compound gate: multi-format acceptance. Use or when the LLM might emit one of several valid output formats.
- id: verify
systemPrompt: 'Output a verdict: either a bare word (accept/reject) or JSON {"verdict": "accept"|"reject", "score": N}.'
if:
or:
- { equals: "accept" }
- jsonMatches:
type: object
properties:
verdict: { type: string, enum: [accept] }
then: continue
else: { jump: { stepId: refine } }The or accepts either a bare "accept" token or a JSON object with verdict: "accept". This handles LLMs that sometimes wrap their answer in JSON and sometimes emit a bare word — without forcing the prompt to constrain the format.