How to Implement GuardRail Validation for Agent Input and Output in Embabel

Embabel provides a plug‑in validation mechanism called Guard Rails that lets you enforce safety, quality, and policy rules on both user input reaching an LLM and assistant output returned by the model.

The embabel/embabel-agent repository includes a comprehensive validation system within the embabel‑agent‑api module. This system intercepts messages at the API boundary, allowing you to implement custom validation logic that runs automatically before requests are sent to language models and after responses are received.

Core GuardRail Interfaces

The validation framework centers on three interfaces located in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/.

GuardRail serves as the base contract. It defines the validate(String, Blackboard): ValidationResult method that all implementations must provide. This interface lives in [GuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/GuardRail.kt).

UserInputGuardRail extends the base interface specifically for incoming user messages. The source file [UserInputGuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/UserInputGuardRail.kt) marks the entry point for input validation.

AssistantMessageGuardRail handles outgoing assistant messages and adds an overload that receives a ThinkingResponse. This allows validation of both the final text and intermediate reasoning steps. See [AssistantMessageGuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/AssistantMessageGuardRail.kt).

Global Registration via GlobalGuardRailsRegistry

All guard‑rails are discovered and instantiated by the GlobalGuardRailsRegistry class, a Spring @Component located in [GlobalGuardRailsRegistry.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/guardrails/GlobalGuardRailsRegistry.kt).

The registry reads two comma‑separated configuration properties:

  • embabel.agent.guardrails.user-input
  • embabel.agent.guardrails.assistant-message

During its @PostConstruct init() method, the registry calls instantiateGuardRails<T>() for each configured class list. This method uses reflection via ClassUtils.forName to load classes and validates that they implement the required interface. If a class cannot be instantiated and embabel.agent.guardrails.fail-on-error=true is set, the registry throws a GuardRailInstantiationException. Otherwise, errors are logged and the offending guard‑rail is skipped.

The registry exposes the created singletons through static helpers: getUserInputGuardRails() and getAssistantMessageGuardRails().

Where GuardRails Are Enforced

User‑input validation occurs in the GuardedOperationsProxyFactory (located in the embabel-agent-common module). This factory wraps LLM calls and iterates over the global user‑input guard‑rails before sending the request to the model. If any guard returns a failure ValidationResult, the pipeline immediately throws a GuardRailViolationException, aborting the request.

Assistant‑output validation runs after the model produces a response. Production code in embabel-agent-openai and other model modules follows the pattern demonstrated in [StreamingChatClientOperationsGuardRailTest.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperationsGuardRailTest.kt). After receiving an AssistantMessage or ThinkingResponse, each registered AssistantMessageGuardRail is invoked sequentially.

Creating Custom GuardRail Implementations

Follow these steps to add custom validation logic:

  1. Implement the appropriate interface — Choose UserInputGuardRail for input validation or AssistantMessageGuardRail for output validation.
  2. Override the validate method — Return ValidationResult.success() on pass or ValidationResult.failure("reason") on violation.
  3. Register the class — Add the fully‑qualified class name to the appropriate property in application.properties.
  4. Configure error handling — Optionally set embabel.agent.guardrails.fail-on-error=true to force startup failure if instantiation fails.

Example: Profanity Filter for User Input

package com.mycompany.guardrails

import com.embabel.agent.api.validation.guardrails.UserInputGuardRail
import com.embabel.common.core.validation.ValidationResult
import com.embabel.common.util.loggerFor
import com.embabel.common.core.Blackboard

class ProfanityFilterGuardRail : UserInputGuardRail {
    private val logger = loggerFor<ProfanityFilterGuardRail>()
    private val banned = setOf("badword1", "badword2")

    override val name: String = "ProfanityFilter"

    override fun validate(input: String, blackboard: Blackboard): ValidationResult {
        val containsBanned = banned.any { input.contains(it, ignoreCase = true) }
        return if (containsBanned) {
            logger.warn("Profanity detected in user input")
            ValidationResult.failure("Profanity is not allowed")
        } else {
            ValidationResult.success()
        }
    }
}

Register in application.properties:

embabel.agent.guardrails.user-input=com.mycompany.guardrails.ProfanityFilterGuardRail

Example: Output Length Guard for Assistant Messages

package com.mycompany.guardrails

import com.embabel.agent.api.validation.guardrails.AssistantMessageGuardRail
import com.embabel.common.core.validation.ValidationResult
import com.embabel.common.util.loggerFor
import com.embabel.common.core.Blackboard

class MaxLengthGuardRail : AssistantMessageGuardRail {
    private val logger = loggerFor<MaxLengthGuardRail>()
    private val MAX_LENGTH = 500

    override val name: String = "MaxLengthGuard"

    override fun validate(input: String, blackboard: Blackboard): ValidationResult =
        if (input.length > MAX_LENGTH) {
            logger.warn("Assistant message exceeds max length")
            ValidationResult.failure("Response too long")
        } else {
            ValidationResult.success()
        }
}

Register in application.properties:

embabel.agent.guardrails.assistant-message=com.mycompany.guardrails.MaxLengthGuardRail

Handling GuardRail Violations

When a guard‑rail signals failure, the core pipeline throws a GuardRailViolationException defined in [GuardRailViolationException.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/GuardRailViolationException.kt). You should catch this exception in your higher‑level service layer to return user‑friendly error messages or trigger alternative handling workflows.

Summary

  • Guard Rails in Embabel provide a plug‑in mechanism for validating LLM inputs and outputs via the embabel-agent-api module.
  • Three core interfaces define the contracts: GuardRail, UserInputGuardRail, and AssistantMessageGuardRail.
  • GlobalGuardRailsRegistry automatically discovers and instantiates guard‑rails from the embabel.agent.guardrails.user-input and embabel.agent.guardrails.assistant-message properties.
  • Enforcement points include GuardedOperationsProxyFactory for input validation and model-specific handlers for output validation.
  • Implementations must return ValidationResult objects and can trigger GuardRailViolationException on failure.

Frequently Asked Questions

What is the difference between UserInputGuardRail and AssistantMessageGuardRail?

UserInputGuardRail validates raw text strings sent by users before they reach the LLM, while AssistantMessageGuardRail validates text generated by the model before it returns to the user. The assistant variant includes an additional overload validate(ThinkingResponse<*>, Blackboard) that allows inspection of intermediate reasoning steps, not just the final text output.

How do I register multiple GuardRails for the same stage?

Add multiple fully‑qualified class names as a comma‑separated list in the appropriate property. For example: embabel.agent.guardrails.user-input=com.example.FirstGuardRail,com.example.SecondGuardRail. The GlobalGuardRailsRegistry will instantiate and apply each guard‑rail in the order specified.

What happens if a GuardRail class cannot be instantiated at startup?

If embabel.agent.guardrails.fail-on-error is set to true, the application will fail to start with a GuardRailInstantiationException. If left as false (the default), the registry logs the error and continues without that specific guard‑rail, allowing the application to start with reduced validation coverage.

Can I access the conversation history or metadata inside a GuardRail?

Yes. Every validate method receives a Blackboard parameter that provides access to contextual data, conversation history, and other metadata stored in the current execution context. You can use this blackboard to implement stateful validation rules that depend on previous messages or session-specific information.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →