rdrailInputrecord and returnsGuardrailResult. OutputGuardrailreceives aGuardrailOutput` record and returns the same result type. This contract ensures every validation step is pure, stateless, and easily testable.
// Pre-execution validation: runs before the LLM is contacted
InputGuardrail sensitiveDataFilter = input -> {
String taskText = input.taskDescription().toLowerCase();
String context = input.contextOutputs().stream()
.map(ctx -> ctx.getRaw().toLowerCase())
.collect(Collectors.joining(" "));
if (taskText.contains("ssn") || context.contains("credit_card")) {
return GuardrailResult.failure("PII detected in task or upstream context");
}
return GuardrailResult.success();
};
Step 2: Implement Post-Execution Validation
Output guardrails execute after the LLM generates text and, if configured, after structured parsing completes. This ordering allows validation against both raw strings and typed Java objects.
// Post-execution validation: runs after parsing
OutputGuardrail structuralCompliance = output -> {
if (output.parsedOutput() instanceof ExecutiveSummary summary) {
if (summary.keyMetrics() == null || summary.keyMetrics().isEmpty()) {
return GuardrailResult.failure("Executive summary missing required key metrics");
}
if (summary.recommendations().size() < 2) {
return GuardrailResult.failure("At least two recommendations are required");
}
}
return GuardrailResult.success();
};
Step 3: Wire Guardrails into Task Configuration
Guardrails are attached per-task during builder initialization. The framework evaluates them in declaration order. The first failure short-circuits execution.
Task analysisTask = Task.builder()
.description("Generate quarterly performance report")
.expectedOutput("Structured JSON with metrics and recommendations")
.outputType(ExecutiveSummary.class)
.agent(analystAgent)
.inputGuardrails(List.of(sensitiveDataFilter, roleAccessCheck))
.outputGuardrails(List.of(structuralCompliance, lengthConstraint))
.build();
Step 4: Handle Violations with Structured Exceptions
When a guardrail fails, the framework throws GuardrailViolationException. This exception propagates through the workflow executor and wraps inside TaskExecutionException. Catching and unwrapping it enables precise routing to logging, metrics, or retry logic.
try {
ensemble.run();
} catch (TaskExecutionException ex) {
if (ex.getCause() instanceof GuardrailViolationException gve) {
String violationType = gve.getGuardrailType().name();
String taskContext = gve.getTaskDescription();
String violationMsg = gve.getViolationMessage();
metricsClient.increment("guardrail.violation." + violationType);
logger.warn("Blocked task [{}] | Type: {} | Reason: {}",
taskContext, violationType, violationMsg);
}
}
Architecture Rationale
The decision to use functional interfaces over annotation-driven or configuration-based validation serves three production requirements:
- Composability: Lambdas can be combined, wrapped, or conditionally applied without framework coupling. A single PII filter can span dozens of tasks.
- Thread Safety: Guardrails execute in parallel workflows. Stateless functions eliminate race conditions. If state is required, developers must explicitly manage synchronization.
- Predictable Latency: Synchronous execution keeps validation overhead under 5ms. Async operations, external API calls, or retry logic must be implemented inside the guardrail function itself, preserving framework simplicity while allowing complexity where needed.
Pitfall Guide
1. Async Blocking in Synchronous Functions
Explanation: Guardrails run synchronously. Calling blocking HTTP clients, database queries, or external classifiers inside a lambda will stall the workflow thread pool, causing cascading timeouts.
Fix: Keep guardrails pure. If external validation is required, offload it to a dedicated workflow step or use non-blocking reactive clients that return immediately. Reserve guardrails for in-memory, deterministic checks.
2. Ignoring Upstream Context
Explanation: Developers often validate only the current task description, missing violations introduced by prior agents. This creates blind spots in multi-step pipelines.
Fix: Always inspect input.contextOutputs(). Chain validation logic that verifies upstream results meet minimum quality thresholds before allowing downstream execution.
3. Semantic Overreach
Explanation: Attempting to enforce tone, creativity, or subjective quality through guardrails leads to brittle regex patterns and false positives. Guardrails are structural, not semantic.
Fix: Use guardrails for length limits, required fields, PII filters, and schema compliance. Delegate semantic evaluation to reflection phases, peer review agents, or dedicated quality scoring models.
4. Stateful Lambda Capture
Explanation: Capturing mutable collections or counters inside guardrail lambdas causes race conditions when tasks execute in parallel. The framework does not synchronize guardrail execution.
Fix: Keep lambdas stateless. If validation requires aggregation across tasks, use thread-safe structures (ConcurrentHashMap, AtomicInteger) or move state management to the workflow orchestrator.
Explanation: Catching generic Exception or Throwable discards the structured payload inside GuardrailViolationException. This breaks observability pipelines and makes debugging impossible.
Fix: Always catch TaskExecutionException, unwrap the cause, and extract guardrailType, taskDescription, and violationMessage. Route these fields directly to structured logging and metrics systems.
6. Short-Circuit Blindness
Explanation: Assuming all declared guardrails execute leads to missing validation coverage. The framework stops at the first failure by design.
Fix: Design guardrails with fail-fast in mind. If you require full failure aggregation, compose multiple checks into a single guardrail that collects all violations before returning a combined GuardrailResult.failure().
7. Bypassing Typed Output Validation
Explanation: Validating only output.rawResponse() ignores the deserialized object. This forces developers to parse JSON manually inside guardrails, defeating the purpose of structured output.
Fix: Leverage output.parsedOutput(). Cast to the expected record type and validate business rules against strongly-typed fields. This catches schema drift and missing required properties instantly.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Strict policy enforcement (PII, compliance) | Input guardrails only | Prevents LLM call entirely, saves tokens and compute | High savings |
| Structural output validation (JSON schema, required fields) | Output guardrails with typed parsing | Catches deserialization drift without manual regex | Neutral |
| Multi-agent pipeline with upstream dependencies | Context-aware input guardrails | Validates chain integrity before downstream execution | Medium savings |
| Subjective quality assessment (tone, creativity) | Reflection/review phase, not guardrails | Guardrails cannot reliably measure semantics | Low impact |
| High-throughput parallel workflows | Stateless, composable lambdas | Prevents race conditions and thread pool starvation | High savings |
Configuration Template
// Production-ready guardrail setup for AgentEnsemble
public class WorkflowGuardrails {
public static InputGuardrail createPiiAndContextGuardrail() {
return input -> {
String combined = Stream.of(
input.taskDescription(),
input.expectedOutput(),
input.contextOutputs().stream()
.map(ctx -> ctx.getRaw())
.collect(Collectors.joining(" "))
)
.collect(Collectors.joining(" ")).toLowerCase();
if (combined.matches(".*\\b(ssn|credit.card|passport)\\b.*")) {
return GuardrailResult.failure("Sensitive data detected in task or context");
}
return GuardrailResult.success();
};
}
public static OutputGuardrail createTypedOutputGuardrail() {
return output -> {
if (output.parsedOutput() instanceof ReportData report) {
if (report.sections().size() < 3) {
return GuardrailResult.failure("Report requires minimum 3 sections");
}
if (report.metadata().get("source") == null) {
return GuardrailResult.failure("Missing required source metadata");
}
}
return GuardrailResult.success();
};
}
public static Task configureValidatedTask(Agent agent) {
return Task.builder()
.description("Generate validated report")
.expectedOutput("ReportData JSON object")
.outputType(ReportData.class)
.agent(agent)
.inputGuardrails(List.of(createPiiAndContextGuardrail()))
.outputGuardrails(List.of(createTypedOutputGuardrail()))
.build();
}
}
Quick Start Guide
- Define your validation contract: Create
InputGuardrail and OutputGuardrail lambdas that return GuardrailResult.success() or GuardrailResult.failure("reason").
- Attach to task builder: Pass guardrail lists to
.inputGuardrails() and .outputGuardrails() during Task.builder() initialization.
- Handle violations: Wrap
ensemble.run() in a try-catch, unwrap TaskExecutionException, and extract GuardrailViolationException for logging and metrics.
- Test in isolation: Instantiate guardrails directly with mock input/output records. Verify success/failure paths without spinning up the full ensemble.
- Deploy with observability: Configure your metrics collector to increment counters on
guardrail.violation.INPUT and guardrail.violation.OUTPUT. Set alerts for violation rate spikes.