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:
$status = execute lookup_ticket(ticket_id=$ticket_id) # one action's result into the next
if $status == "unknown" # the check
bot unknown ticket
stopThe lookup actions
find_ticket_id pulls an id like INC-42 out of the message; lookup_ticket takes it by name.
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
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- written in define user and define bot
- written in define user and define bot
View the code here
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)
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:
- 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.
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?")User: What is the status of ticket INC-42? Bot : Ticket INC-42 is resolved. User: Has INC-99 been fixed? Bot : I cannot find that ticket number.
What the check did
- INC-42 was found and reported with the values from the actions:
$ticket_idand$statuswere filled into thedefine bottext. - 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
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"))['config', 'input', 'output', 'retrieval', 'dialog', 'actions', 'tool_output', 'tool_input'] False
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 execute | rails.execution | |
|---|---|---|
| Runs in 0.24.1 | Yes | No, dropped silently |
| Where | rails.co | config.yml |
Where you check results
- Lookups in ticketing, inventory or user systems.
- Any value that ends up inside a
define botsentence.
== on strings. If the action returns None for an unknown ticket, compare with None or the check never fires.Related
- Previous: embeddings_only and the threshold
- Next: Rails exceptions
- Reference: Action parameters
- Ask about INC-77.
- Make
lookup_ticketreturn an empty string for unknown tickets and fix the flow.
You understood something today that you didn't yesterday.