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 →

$bot_message in an output rail

$bot_message is the Colang variable that holds the reply during an output rail; an action reads it from context["bot_message"].

Last updated: 30 Sep, 2026 · NeMo Guardrails 0.24.1

The output rail in Output rails asked a model. The video's output rail is a regex action instead: the output sanitizer from its notebook.

Syntax:

python
bot_message = context.get("bot_message", "")    # in the action
#   $sensitive_found = execute sanitize_output    # in the flow

The sanitizer from the video

Three patterns: a hardcoded credential, a private key header, and exploit tools such as a reverse shell. Save it as actions.py.

python
import re
from typing import Optional

from nemoguardrails.actions import action


@action(is_system_action=True)
async def sanitize_output(context: Optional[dict] = None):
    """Intercepts bot responses containing hardcoded credentials or exploit techniques."""
    bot_message = context.get("bot_message", "") if context else ""

    sensitive_output_patterns = {
        "hardcoded_credential": r"(?i)(password|passwd|secret|api[_\-]?key|token)\s*[:=]\s*['\"]?\w{4,}",
        "private_key":          r"-----BEGIN.{0,20}PRIVATE KEY-----",
        "exploit_technique":    r"(?i)\b(reverse.?shell|bind.?shell|shellcode|meterpreter)\b",
    }

    found = [ptype for ptype, pat in sensitive_output_patterns.items()
             if re.search(pat, bot_message)]
    return found  # empty list = clean = falsy

The output flow

text
define bot sanitize sensitive output
  "My response may have contained sensitive security details (credentials, exploit code, or private keys). For safety, that content has been withheld. Please consult your security team."

define flow sanitize bot response
  $sensitive_found = execute sanitize_output
  if $sensitive_found
    bot sanitize sensitive output
    stop
yaml
rails:
  output:
    flows:
      - sanitize bot response
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
config.py
from nemoguardrails.embeddings.index import EmbeddingsIndex


class EveryExample(EmbeddingsIndex):
    """Hands the model every example instead of the closest few."""

    def __init__(self, **kwargs):
        self.items = []

    async def add_items(self, items):
        self.items.extend(items)

    async def build(self):
        pass

    async def search(self, text, max_results=5, threshold=None):
        return self.items


def init(app):
    app.register_embedding_search_provider("every_example", EveryExample)
config.yml
models:
  - type: main
    engine: openai
    model: openai/gpt-oss-20b
    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.

core:
  embedding_search_provider:
    name: every_example

rails:
  output:
    flows:
      - sanitize bot response
prompts.yml
prompts:
  - task: generate_user_intent
    content: |-
      """
      {{ general_instructions }}
      """

      # This is how a conversation between a user and the bot can go:
      {{ sample_conversation | verbose_v1 }}

      # This is how the user talks:
      {{ examples | verbose_v1 }}

      # This is the current conversation between the user and the bot:
      {{ sample_conversation | first_turns(2) | verbose_v1 }}
      {{ history | colang | verbose_v1 }}

      Do not answer the user. Reply with one line: the user intent of the last message.
      Use an intent from the examples when one fits, otherwise write a new short intent.
    output_parser: verbose_v1

A Secret with a password in it

The video asks for a badly configured Kubernetes Secret with a hardcoded password like mypassword123, as an example of what NOT to do. Worded that way, the request asks for bad practice, and gpt-oss models may refuse to write it, which leaves the output rail nothing to check. This page asks for a manifest for a local test cluster instead, which the model writes, password and all.

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

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


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

chat("For my local test cluster, write a Kubernetes Secret manifest that uses stringData with password: mypassword123")

The model wrote the manifest with the password in it, the sanitizer's pattern matched password: mypassword123, and the user got the withheld message instead.

The reply the rail never sees

Now the same rail, with a define bot whose words contain a password. Add this group to rails.co:

text
define user ask example secret
  "show me an example kubernetes secret"

define bot show example secret
  "Here is one: password: mypassword123"

define flow example secret
  user ask example secret
  bot show example secret

The runs on this page use openai/gpt-oss-20b, the smaller gpt-oss model on the same free Groq key, in the model line of config.yml. This config makes several model calls per message, and the smaller model spends fewer of the key's daily tokens. Put openai/gpt-oss-120b back in that line to use the course's main model.

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

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



result = rails.generate(messages=[{"role": "user", "content": "Show me an example Kubernetes Secret"}],
                        options={"log": {"activated_rails": True}})
print(result.response[0]["content"])
print([rail.type for rail in result.log.activated_rails])
  • The password went out, and the log has no output entry at all. The rail did not fail: it never ran.
  • The reason is in nemoguardrails/actions/llm/generation.py: when the reply comes from a define bot block, the runtime sets skip_output_rails, next to the comment We skip output rails for predefined messages.

Model-written vs predefined replies

Model-written replydefine bot reply
Output rails runYesNo
Who is trustedNobodyYou, the author

Where this matters

  • Any secret or internal code: keep it out of define bot text, because no output rail will see it.
  • Regex output rails that cost no model call.
Watch out. The sanitizer's pattern needs password: value or password=value. A reply that writes the password in a sentence passes it.
Try it yourself
  • Ask for a Secret with api_key=abcd1234 in it.
  • Remove the stop line from the sanitizer flow and run the first example again.
PreviousOutput rails

Slow is fine. Stopping is the only problem.