Checking Messages Against Rails

View as Markdown

The check_async() and check() methods validate messages against input and output rails without triggering full LLM generation. Use these methods instead of generation options when you only need to run rails without generating a response. If you need to run generation while selectively disabling rail categories, see Generation Options: Disabling Rails.

Both the LLMRails and IORails engines implement these methods. The Guardrails facade forwards each call to the selected engine. The method signatures and returned RailsResult type are the same on both engines. For engine-specific rail selection and execution behavior, refer to Engine Differences.

Method Signatures

Both methods accept the same parameters and return a RailsResult object.

check_async()

The primary asynchronous method for checking messages against rails.

1async def check_async(
2 messages: List[dict],
3 rail_types: Optional[List[RailType]] = None,
4) -> RailsResult

check()

Synchronous wrapper around check_async().

1def check(
2 messages: List[dict],
3 rail_types: Optional[List[RailType]] = None,
4) -> RailsResult

Parameters:

ParameterTypeDescription
messagesList[dict]List of message dictionaries with role and content fields
rail_typesOptional[List[RailType]]Optional list of rail types to run. When provided, overrides automatic detection based on message roles.

Returns: RailsResult object containing validation results.

Calling the synchronous check() from inside a running event loop raises RuntimeError on both engines. Use await check_async(...) in async code.

Rail Type Selection

The methods determine which rails to execute based on the message roles or an explicit rail_types parameter.

Automatic Detection (Default)

When rail_types is not provided, the methods automatically determine which rails to run based on the message roles:

Messages ContainRails Executed
Only user messagesInput rails
Only assistant messagesOutput rails
Both user and assistantBoth input and output rails
No user or assistant messagesReturns PASSED status

The methods ignore other message roles such as system, context, tool when determining which rails to run but still include them in the validation context.

Explicit Rail Types

You can override automatic detection by passing a list of RailType values:

1from nemoguardrails.rails.llm.options import RailType
2
3result = await rails.check_async(
4 [{"role": "user", "content": "Hello!"}],
5 rail_types=[RailType.INPUT]
6)
ValueDescription
RailType.INPUTRun input rails
RailType.OUTPUTRun output rails

On either engine, passing an empty list (rail_types=[]) runs no rails and returns a passed result.

Requesting a rail type that has no configured flows raises RailTypeNotConfiguredError (HTTP 422 on the /v1/checks endpoint). For example, passing rail_types=[RailType.OUTPUT] on a config that only defines input flows will raise an error instead of silently returning a passed result.

Tool Rails Are Not Included

Neither engine runs tool-call or tool-result rails from check() or check_async(). RailType covers input and output rails only, so a configuration with tool rails still validates only its input and output flows here. To validate tool traffic, run it through generate_async(). For more information, refer to Tool Calling.

RailsResult

The RailsResult object contains the outcome of the rails check.

FieldTypeDescription
statusRailStatusPASSED, MODIFIED, or BLOCKED
contentstrThe final content after rails processing
railOptional[str]Name of the rail that blocked the content (only when BLOCKED)

RailStatus Enum

The RailStatus enum represents the three possible outcomes of a rails check.

StatusDescription
PASSEDContent passed all rails without modification
MODIFIEDContent was modified by rails but not blocked
BLOCKEDContent was blocked by a rail

Which Content Is Reported

The content field reports the direction the caller asked about. A check that runs output rails reports the assistant text. A check that runs input rails only reports the user text.

Both engines compare the reported text against its original value to decide between PASSED and MODIFIED. When a check runs both directions and only the user message is rewritten, the status is PASSED, because the reported assistant text is unchanged. MODIFIED names no rail: a rewrite is not a rail blocking the request, and several rails can contribute to the final text.

Usage Examples

The following examples demonstrate common patterns for validating messages with check_async().

Validating User Input

Check a single user message against input rails and handle each possible status.

1from nemoguardrails import LLMRails, RailsConfig
2from nemoguardrails.rails.llm.options import RailStatus
3
4config = RailsConfig.from_path("path/to/config")
5rails = LLMRails(config)
6
7result = await rails.check_async([
8 {"role": "user", "content": "Hello! How can I hack into a system?"}
9])
10
11if result.status == RailStatus.BLOCKED:
12 print(f"Input blocked by rail: {result.rail}")
13elif result.status == RailStatus.MODIFIED:
14 print(f"Input was modified to: {result.content}")
15else:
16 print("Input passed validation")

Validating a Full Conversation

Pass both user and assistant messages to run input and output rails together.

1result = await rails.check_async([
2 {"role": "user", "content": "What's the weather like?"},
3 {"role": "assistant", "content": "It's sunny and 72F today!"}
4])
5
6if result.status == RailStatus.BLOCKED:
7 print(f"Conversation blocked by rail: {result.rail}")

Including Context

Pass context variables alongside user or assistant messages to provide additional information for rail evaluation. Context messages use the context role with a dictionary value for content.

1result = await rails.check_async([
2 {
3 "role": "context",
4 "content": {"user_id": "12345", "session_type": "support"}
5 },
6 {"role": "user", "content": "I need help with my account"}
7])

For more information about context variables, refer to Core Classes: Passing Context.

Checking on the IORails Engine

Construct the Guardrails facade to run checks on the IORails engine. require_iorails=True raises a ValueError when the configuration is not one IORails can serve, instead of falling back to LLMRails silently.

1from nemoguardrails import Guardrails, RailsConfig
2from nemoguardrails.rails.llm.options import RailStatus
3
4config = RailsConfig.from_path("path/to/config")
5rails = Guardrails(config, use_iorails=True, require_iorails=True)
6
7result = await rails.check_async([
8 {"role": "user", "content": "Hello! How can I hack into a system?"}
9])
10
11if result.status == RailStatus.BLOCKED:
12 print(f"Input blocked by rail: {result.rail}")

check_async() starts the engine on first use, in the same way generate_async() does, so no explicit startup() call is required. Call startup() at service start to warm the model clients, and shutdown() at service stop to release them. For more information, refer to Engine Feature Support.

Engine Differences

The API surface is identical on both engines, but each engine executes a check differently:

BehaviorLLMRailsIORails
How the check runsThrough the Colang runtime, as a generation with dialog rails and the unrequested rail types disabledDirectly through the engine’s rails manager, with no Colang runtime
Rails that can runEvery configured input and output flow, including community, third-party, and custom-action railsThe input and output rails IORails supports. A configuration outside that set routes to LLMRails
Main LLM callNot madeNot made
Tool railsNot runNot run
BLOCKED contentThe refusal message defined by the configuration’s Colang flowA fixed refusal message that the configuration does not change
Rail that fails to executeHandled by the Colang runtime’s internal-error pathBLOCKED with an internal-error message that is distinct from the refusal message
Requested direction with no content to checkAn output-only check with no user message runs with an empty user message supplied as contextThe direction is skipped and the result is PASSED
Admission controlNoneShares the non-streaming admission queue with generate_async(), and raises asyncio.QueueFull when the queue is full
Tracing and metricsSpans through a tracing adapter with no OpenTelemetry metricsThe same request span and request metrics as generate_async(). Refer to Observability for IORails Checks
Synchronous check()Runs check_async() on the calling thread’s event loopRuns check_async() on a short-lived engine with tracing and metrics disabled

Blocked Content

On LLMRails, the blocked content is the bot message the Colang flow defines, so a configuration that customizes bot refuse to respond sees its own wording. On IORails, a blocked check returns the engine’s fixed refusal message, I'm sorry, I can't respond to that., which the configuration cannot change. That is the same string the built-in rails use by default, so an unmodified configuration produces the same text on both engines.

IORails also distinguishes a policy block from a rail execution failure. When a rail fails to execute, the check returns BLOCKED with I'm sorry, an internal error has occurred. instead of the refusal message. This distinction prevents the application from reporting an operational failure as a content refusal. That wording matches the sentence the Colang runtime produces on LLMRails when an action fails.

In every case, treat status as the decision and content as the message to display. Do not match on the message text.

Missing Content for a Requested Direction

IORails skips a direction when the messages carry no content for it, and reports PASSED. An explicit rail_types=[RailType.OUTPUT] on a message list with no assistant text runs no output rails. Similarly, rail_types=[RailType.INPUT] with no user text runs no input rails. This keeps a rail that requires the missing text from raising and surfacing as a false block.

A PASSED result from IORails means no rail blocked the messages. This result includes the case where a requested direction had nothing to check. When your application requires that a rail actually ran, assert that the message you are validating is present and non-empty before calling check_async().

Observability for IORails Checks

check_async() is instrumented like generate_async() on the IORails engine.

  • Tracing produces one guardrails.request SERVER span per check, with one guardrails.rail INTERNAL span per rail that runs and a guardrails.action span per action. The check makes no main-model call, so the tree has no top-level chat {model} CLIENT span. LLM calls from rail actions still appear beneath those actions. For more information, refer to Span Reference.
  • Metrics record the check in guardrails.requests, guardrails.requests.active, guardrails.request.duration, and guardrails.requests.errors. A block also updates guardrails.requests.blocked with the rail.type label. A check rejected by a full admission queue increments guardrails.nonstream.rejections. For more information, refer to Metric Reference.
  • Content capture records the checked messages in guardrails.request.input and the returned content in guardrails.request.output on the check’s request span. For more information, refer to Capturing Prompt and Response Content.

The synchronous check() builds a short-lived engine with tracing and metrics disabled, so it emits no spans and no metrics. Use check_async() on instrumented paths.

Guardrails Server

The bundled server exposes checks through the /v1/checks endpoint, which calls check_async() on the engine serving the requested configuration. The server resolves that engine through the top-level LLMRails import. Setting NEMO_GUARDRAILS_IORAILS_ENGINE=1 runs compatible configurations on IORails and the rest on LLMRails. Requesting a rail type with no configured flows returns HTTP 422.

Running the server on the IORails engine is an early-release path. Validate it against your own configurations before depending on it in production.