Skip to content

Flow rules

Any field can carry rules in on. They run in code right after the stage containing the field finishes, so they are exact and cannot be talked out of by the input.

"policy_number": {
  "type": "string",
  "on": [
    {"when": {"len_lt": 16}, "do": "reject"},
    {"when": {"confidence_lt": 0.9}, "do": "review"}
  ]
}

A rule has:

  • when: conditions on the field's value. All operators in one object must match (AND). Omit when to always apply. A plain list is shorthand for in: "when": ["harmful"].
  • do: the action. A field name means "jump to that field".
  • max: how many times a return or jump may fire (default 3). After that the rule is skipped, so loops always end.
  • at: check a path inside the value instead of the whole value, for fields that hold objects (for example an API result): {"at": "policy_status", "when": {"ne": "active"}, "do": "reject"}.

Actions

do Effect
reject Stop. Nothing else runs. Fields of the same stage and fields that did not run get their on_reject value. The response has rejected: {"field", "value"}.
review Flag for a human. The request continues; the field is listed in flagged (with the reason) and in needs_review.
drop Keep the value in result, but do not show it to later fields.
return Start over. Every field runs again and sees the previous round's values and the reason.
"<field>" or {"do": "jump", "to": "<field>"} Jump. Forward: skip the fields in between (null). Backward: rerun the target and the fields after it, seeing their previous values.
switch Pick one branch: {"do": "switch", "cases": {"<value>": "<field>", ...}, "default": "<field>"}. Only the chosen field runs; the other targets are skipped (null).
end Finish successfully. Fields that did not run are null; ended is set in the response.

Rules of one field are checked in order. The first matching flow action (reject, return, jump, switch, end) is applied and checking stops. review and drop are markers: they apply and checking continues, so you can combine them, for example "flag it and hide it":

"on": [{"when": {"confidence_lt": 0.9}, "do": "review"}, {"do": "drop"}]

Only one flow action is applied per stage: the first one in schema order.

Conditions

Operator Applies to Matches when
eq, ne any equal / different (text: trimmed, case-insensitive)
in, not_in any in / not in the list
gt, gte, lt, lte numbers numeric comparison
len_eq, len_lt, len_lte, len_gt, len_gte text, lists trimmed text length, or number of items
contains text, lists text contains the substring / list has the item
match, not_match text a regular expression matches / does not match the whole text
eq_expr, ne_expr, lt_expr, lte_expr, gt_expr, gte_expr numbers comparison with an expression over other fields, e.g. {"ne_expr": "subtotal - discount + vat", "tol": 1}
confidence_lt, confidence_lte, confidence_gt, confidence_gte model fields the field's confidence compared with a number (0–1)

Notes:

  • Expressions use field names, numbers, + - * /, parentheses, abs, min, max, round. tol is the difference still treated as equal. Referenced fields must run in the same or an earlier stage; they do not need to be dependencies.
  • A condition on a value of the wrong type does not match: lt on null, len_lt on a number, true against 1.
  • Checks run in code, so they are exact. Do not ask the model to count characters or compare totals; write a rule.

Recipes

Reject bad input before anything else runs

"safety": {"enum": ["safe", "harmful"], "on": [{"when": ["harmful"], "do": "reject"}]},
"answer": {"type": "string", "free_form": true, "depends_on": ["safety"],
           "on_reject": "Sorry, this request cannot be processed."}

Check totals, retry once, then ask a human

"total": {"type": "number", "on": [
  {"when": {"ne_expr": "subtotal - discount + vat", "tol": 1}, "do": "return", "max": 1},
  {"when": {"ne_expr": "subtotal - discount + vat", "tol": 1}, "do": "review"}]}

If the document itself is wrong (a misprinted total), the retry reads the same value again and the field is flagged, which is what you want.

Route to one team

"team": {"enum": ["infra", "dev", "security", "other"],
         "on": [{"do": "switch",
                 "cases": {"infra": "msg_infra", "dev": "msg_dev", "security": "msg_security"},
                 "default": "msg_other"}]}

return reruns everything; jumps rerun less

return fills every field again. When only one field needs another look, jump back to it instead ("do": "total"): only that field and the ones after it run again.

Do not retry doubt

For low confidence, go straight to review. A rerun sees its own previous answer and usually repeats it with higher confidence, which hides the doubt.