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 →

Execution rails

An execution rail checks what an action returned before the bot uses it; in Colang 1.0 it is an if in the flow, right after the execute.

Last updated: 30 Sep, 2026 · NeMo Guardrails 0.24.1

Actions so far checked messages. An action that looks something up can hand back a value no user should see. The IT assistant here looks up incident tickets.

Syntax:

text
  $status = execute lookup_ticket(ticket_id=$ticket_id)   # one action's result into the next
  if $status == "unknown"                                  # the check
    bot unknown ticket
    stop

The lookup actions

find_ticket_id pulls an id like INC-42 out of the message; lookup_ticket takes it by name.

python
import re

from nemoguardrails.actions import action

TICKETS = {"INC-42": "resolved", "INC-77": "in progress"}


@action(is_system_action=True)
async def find_ticket_id(context: dict):
    found = re.search(r"INC-\d+", context.get("user_message", ""))
    return found.group(0) if found else ""


@action(is_system_action=True)
async def lookup_ticket(ticket_id: str):
    return TICKETS.get(ticket_id, "unknown")

The flow with its check

text
define user ask ticket status
  "what is the status of ticket INC-42"
  "has INC-77 been fixed"

define bot report ticket
  "Ticket $ticket_id is $status."

define bot unknown ticket
  "I cannot find that ticket number."

define flow ticket status
  user ask ticket status
  $ticket_id = execute find_ticket_id
  $status = execute lookup_ticket(ticket_id=$ticket_id)
  if $status == "unknown"
    bot unknown ticket
    stop
  bot report ticket
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
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 known and an unknown ticket

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 find_ticket_id, lookup_ticket
from nemoguardrails import LLMRails, RailsConfig

rails = LLMRails(RailsConfig.from_path("."))
rails.register_action(find_ticket_id)
rails.register_action(lookup_ticket)


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

chat("What is the status of ticket INC-42?")
chat("Has INC-99 been fixed?")

What the check did

  • INC-42 was found and reported with the values from the actions: $ticket_id and $status were filled into the define bot text.
  • INC-99 is not in the table. Without the check, the reply would have been Ticket INC-99 is unknown., a sentence built from a value never meant for a user.

The rails.execution key

Example
from nemoguardrails import RailsConfig
from nemoguardrails.rails.llm.config import Rails

print(list(Rails.model_fields))
config = RailsConfig.from_content(yaml_content="""
rails:
  execution:
    flows:
      - check tool output
""")
print(hasattr(config.rails, "execution"))

NeMo's configuration reference shows rails: execution: flows:. In 0.24.1 the rails section has no such field, and the key is dropped without a warning. A rail configured that way never runs. tool_input and tool_output are real: they check tool calls a model makes itself.

A check in the flow vs rails.execution

if after executerails.execution
Runs in 0.24.1YesNo, dropped silently
Whererails.coconfig.yml

Where you check results

  • Lookups in ticketing, inventory or user systems.
  • Any value that ends up inside a define bot sentence.
Watch out. Colang compares with == on strings. If the action returns None for an unknown ticket, compare with None or the check never fires.
Try it yourself
  • Ask about INC-77.
  • Make lookup_ticket return an empty string for unknown tickets and fix the flow.

You understood something today that you didn't yesterday.