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). Omitwhento always apply. A plain list is shorthand forin:"when": ["harmful"].do: the action. A field name means "jump to that field".max: how many times areturnor 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":
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.tolis 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:
ltonnull,len_lton a number,trueagainst1. - 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.