RAIL defaults on_fail to NOOP while the Python API defaults to EXCEPTION
What happens
The RAIL/XML path and the Python API disagree on the default on_fail, and the XML path's default is the permissive one.
guardrails/validator_base.py:139-140:
if on_fail is None:
on_fail = OnFailAction.EXCEPTIONguardrails/schema/rail_schema.py:50-52:
on_fail = on_fail_handlers.get(
validator.rail_alias.replace("/", "_"), OnFailAction.NOOP
)So a validator constructed in Python raises when it fails, and the same validator declared in RAIL does nothing when it fails, unless an on-fail-* attribute is present and its name matches.
The part that makes it silent
The handler dict is keyed on the XML attribute name (rail_schema.py:44, key[len("on-fail-"):]), and the lookup key is validator.rail_alias.replace("/", "_"). Those two do not always agree, and a miss is not an error, it is NOOP.
XML attribute the user writes validator rail_alias resolved
on-fail-two-words two-words exception
on-fail-toxic-language guardrails/toxic_language noop
on-fail-guardrails_toxic_language guardrails/toxic_language exception
on-fail-valid-length valid_length noopRows 2 and 4 are the problem. A namespaced alias needs on-fail-guardrails_toxic_language, mixing a hyphenated prefix with an underscored body, and an underscored alias like valid_length needs on-fail-valid_length rather than the hyphenated spelling the rest of the attribute uses. Write the natural-looking one and the validator still runs, still detects the violation, and does nothing about it. Nothing warns.
Why the default matters more than the mismatch
Either default is defensible on its own. Having two is what causes trouble: the same guard, expressed two ways, enforces in one and observes in the other, and the observing one is reached by the configuration format aimed at people who are not writing Python.
NOOP also fails in the permissive direction. A user who misspells an attribute gets a validator that reports nothing rather than one that errors at parse time, so the failure is invisible in exactly the case where the user already made a mistake.
Suggested shape
Two options, either fine:
Align the defaults, making the RAIL path default to EXCEPTION as the Python API does. This is a behaviour change and would want a note in the changelog.
Or keep NOOP as the default but fail loudly on an unmatched on-fail-* attribute. If a RAIL file names a handler that binds to no validator, that is a configuration error and worth raising at parse time rather than resolving to silence.
The second is smaller and catches the case I would worry about, which is the user who thought they had configured enforcement.
Verification
Read against main on 2026-09-14. The transcript above is the resolution logic from rail_schema.py:44 and :50-52 run directly; I did not execute the full RAIL parser, so treat the alias examples as illustrating the key-matching rule rather than as tested end to end. The two defaults themselves are quoted from the files as they stand.
Source: guardrails-ai/guardrails