NeMo Guardrailsnemoguardrails 0.24.1 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
37 small wins to finish your pathNext lesson →

Custom input rail

A custom input rail is a flow of your own in rails.input.flows whose action returns a RailOutcome, the same decision object the library's rails return.

Last updated: 30 Sep, 2026 · NeMo Guardrails 0.24.1

The video sorts rails into three kinds: input rails, output rails and custom rails, which it also calls systematic rails, where you write your own Python such as regular expressions.

Input, output and custom rails · from The Complete AI Security Course In 8 Hours · 55:11 to 58:05

The clip's example is PII detection. A user might paste a mobile number or an SSN, and the guard should say the input contains personal information and not accept it. My SSN number is this. Is this relevant to my setup? is refused with a request to remove the personal data.

Syntax:

python
from nemoguardrails.actions.rail_outcome import RailOutcome

return RailOutcome.block(reason="found ssn")   # refuse
return RailOutcome.allow()                      # let it through
#   in Colang: if $verdict.is_blocked

The action

The same idea as the video's PII action, returning a decision instead of a list. reason is for a person reading a log.

python
import re

from nemoguardrails.actions import action
from nemoguardrails.actions.rail_outcome import RailOutcome

SSN = r"\b\d{3}-\d{2}-\d{4}\b"
EMAIL = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"


@action(is_system_action=True)
async def check_pii(context: dict):
    text = context.get("user_message", "")
    found = [name for name, pat in [("ssn", SSN), ("email", EMAIL)] if re.search(pat, text)]
    if found:
        return RailOutcome.block(reason="found " + ", ".join(found))
    return RailOutcome.allow()

The flow

It reads $verdict.is_blocked, the field self check input reads in the library flow.

text
define bot ask to remove pii
  "I noticed your message may contain sensitive information. Please remove any personal data before sending."

define flow check pii
  $verdict = execute check_pii
  if $verdict.is_blocked
    bot ask to remove pii
    stop
yaml
rails:
  input:
    flows:
      - check pii
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
config.yml
models:
  - type: main
    engine: openai
    model: openai/gpt-oss-120b
    api_key_env_var: GROQ_API_KEY
    parameters:
      base_url: https://api.groq.com/openai/v1
      temperature: 0

instructions:
  - type: general
    content: |
      You are an Enterprise IT Assistant specialising in Kubernetes,
      Intel hardware, and enterprise networking.
      Only answer questions about these topics.
      Answer in one or two short sentences.

rails:
  input:
    flows:
      - check pii

The video's SSN message against the rail

ExampleAPI key
from actions import check_pii
from nemoguardrails import LLMRails, RailsConfig

rails = LLMRails(RailsConfig.from_path("."))
rails.register_action(check_pii)


def chat(message):
    reply = rails.generate(messages=[{"role": "user", "content": message}])
    print("User:", message)
    print("Bot :", reply["content"])

chat("My SSN number is 123-45-6789. Is this relevant to my setup?")
chat("Is SR-IOV relevant to my setup?")

What the custom rail did

  • The SSN was refused before any model call.
  • The SR-IOV question was allowed and answered by the model.

Returning a list vs returning a RailOutcome

A list or boolRailOutcome
The flow testsif $pii_foundif $verdict.is_blocked
Carries a reasonNoYes
Same shape as the library's railsNoYes

Where you write one

  • Any check you want to read like the library's: PII, message length, a blocklist of customers.
  • A check whose reason you want in the logs.
Watch out. Order matters in rails.input.flows. Put the cheap regex rail before self check input, so a message with an SSN is refused without paying for a model call.
Try it yourself
  • Add a phone-number pattern and send Call me on 555-123-4567.
  • Return RailOutcome.block() for every message and send a normal question.

Every expert started right here.