> This page is for version v0.24.0.
> For other versions, use one of these documentation indexes:
> - Latest (default): https://docs.nvidia.com/nemo/guardrails/latest/llms.txt
> - v0.24.1: https://docs.nvidia.com/nemo/guardrails/v0.24.1/llms.txt
> - v0.24.0: https://docs.nvidia.com/nemo/guardrails/v0.24.0/llms.txt
> - v0.23.0: https://docs.nvidia.com/nemo/guardrails/v0.23.0/llms.txt
> - v0.22.0: https://docs.nvidia.com/nemo/guardrails/v0.22.0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/guardrails/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/guardrails/_mcp/server.

# Checking Messages Against Rails

> Validate messages against input and output rails using check_async and check methods on the LLMRails and IORails engines.

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](/run-guardrailed-inference/using-python-apis/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](/run-guardrailed-inference/using-python-apis/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](#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.

```python
async def check_async(
    messages: List[dict],
    rail_types: Optional[List[RailType]] = None,
) -> RailsResult
```

### check()

Synchronous wrapper around `check_async()`.

```python
def check(
    messages: List[dict],
    rail_types: Optional[List[RailType]] = None,
) -> RailsResult
```

**Parameters:**

| Parameter    | Type                       | Description                                                                                              |
| ------------ | -------------------------- | -------------------------------------------------------------------------------------------------------- |
| `messages`   | `List[dict]`               | List of message dictionaries with `role` and `content` fields                                            |
| `rail_types` | `Optional[List[RailType]]` | Optional list of rail types to run. When provided, overrides automatic detection based on message roles. |

**Returns:** `RailsResult` object containing validation results.

> **Note**
>
> 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 Contain                  | Rails Executed              |
| --------------------------------- | --------------------------- |
| Only `user` messages              | Input rails                 |
| Only `assistant` messages         | Output rails                |
| Both `user` and `assistant`       | Both input and output rails |
| No `user` or `assistant` messages | Returns PASSED status       |

> **Note**
>
> 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:

```python
from nemoguardrails.rails.llm.options import RailType

result = await rails.check_async(
    [{"role": "user", "content": "Hello!"}],
    rail_types=[RailType.INPUT]
)
```

| Value             | Description      |
| ----------------- | ---------------- |
| `RailType.INPUT`  | Run input rails  |
| `RailType.OUTPUT` | Run output rails |

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

> **Note**
>
> 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](/configure-guardrails/guardrail-catalog/tool-calling).

## RailsResult

The `RailsResult` object contains the outcome of the rails check.

| Field     | Type            | Description                                                     |
| --------- | --------------- | --------------------------------------------------------------- |
| `status`  | `RailStatus`    | `PASSED`, `MODIFIED`, or `BLOCKED`                              |
| `content` | `str`           | The final content after rails processing                        |
| `rail`    | `Optional[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.

| Status     | Description                                   |
| ---------- | --------------------------------------------- |
| `PASSED`   | Content passed all rails without modification |
| `MODIFIED` | Content was modified by rails but not blocked |
| `BLOCKED`  | Content 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.

```python
from nemoguardrails import LLMRails, RailsConfig
from nemoguardrails.rails.llm.options import RailStatus

config = RailsConfig.from_path("path/to/config")
rails = LLMRails(config)

result = await rails.check_async([
    {"role": "user", "content": "Hello! How can I hack into a system?"}
])

if result.status == RailStatus.BLOCKED:
    print(f"Input blocked by rail: {result.rail}")
elif result.status == RailStatus.MODIFIED:
    print(f"Input was modified to: {result.content}")
else:
    print("Input passed validation")
```

### Validating a Full Conversation

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

```python
result = await rails.check_async([
    {"role": "user", "content": "What's the weather like?"},
    {"role": "assistant", "content": "It's sunny and 72F today!"}
])

if result.status == RailStatus.BLOCKED:
    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`.

```python
result = await rails.check_async([
    {
        "role": "context",
        "content": {"user_id": "12345", "session_type": "support"}
    },
    {"role": "user", "content": "I need help with my account"}
])
```

For more information about context variables, refer to [Core Classes: Passing Context](/run-guardrailed-inference/using-python-apis/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.

```python
from nemoguardrails import Guardrails, RailsConfig
from nemoguardrails.rails.llm.options import RailStatus

config = RailsConfig.from_path("path/to/config")
rails = Guardrails(config, use_iorails=True, require_iorails=True)

result = await rails.check_async([
    {"role": "user", "content": "Hello! How can I hack into a system?"}
])

if result.status == RailStatus.BLOCKED:
    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](/reference/engine-feature-support#iorails).

## Engine Differences

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

| Behavior                                     | LLMRails                                                                                              | IORails                                                                                                                                         |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| How the check runs                           | Through the Colang runtime, as a generation with dialog rails and the unrequested rail types disabled | Directly through the engine's rails manager, with no Colang runtime                                                                             |
| Rails that can run                           | Every configured input and output flow, including community, third-party, and custom-action rails     | The input and output rails IORails supports. A configuration outside that set routes to `LLMRails`                                              |
| Main LLM call                                | Not made                                                                                              | Not made                                                                                                                                        |
| Tool rails                                   | Not run                                                                                               | Not run                                                                                                                                         |
| `BLOCKED` content                            | The refusal message defined by the configuration's Colang flow                                        | A fixed refusal message that the configuration does not change                                                                                  |
| Rail that fails to execute                   | Handled by the Colang runtime's internal-error path                                                   | `BLOCKED` with an internal-error message that is distinct from the refusal message                                                              |
| Requested direction with no content to check | An output-only check with no user message runs with an empty user message supplied as context         | The direction is skipped and the result is `PASSED`                                                                                             |
| Admission control                            | None                                                                                                  | Shares the non-streaming admission queue with `generate_async()`, and raises `asyncio.QueueFull` when the queue is full                         |
| Tracing and metrics                          | Spans through a tracing adapter with no OpenTelemetry metrics                                         | The same request span and request metrics as `generate_async()`. Refer to [Observability for IORails Checks](#observability-for-iorails-checks) |
| Synchronous `check()`                        | Runs `check_async()` on the calling thread's event loop                                               | Runs `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.

> **Warning**
>
> 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](/observability/tracing/span-reference#iorails).
* 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](/observability/metrics/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](/observability/tracing/content-capture).

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.

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

---

## Related Resources

* [Core Classes](/run-guardrailed-inference/using-python-apis/core-classes) - `LLMRails` and `RailsConfig` class reference
* [Generation Options](/run-guardrailed-inference/using-python-apis/generation-options) - Fine-grained control over generation with the `options` parameter
* [Engine Feature Support](/reference/engine-feature-support) - What the `LLMRails` and `IORails` engines each support