#1656·guardrails

RAIL defaults on_fail to NOOP while the Python API defaults to EXCEPTION

Author: arian-goganiCreated Sep 14, 2026Updated Sep 14, 2026

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:

python
if on_fail is None:
    on_fail = OnFailAction.EXCEPTION

guardrails/schema/rail_schema.py:50-52:

python
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                   noop

Rows 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