How to validate tool results with outputSchema in OpenClaw and recover from type mismatches within the agent loop
When operating production agents, you'll eventually encounter this situation: a tool reports success, but the returned JSON structure subtly mismatches what the next tool expects, sending the entire loop in a strange direction. At first you suspect a prompt issue and dig in, only to find the real cause is that tool contracts were never explicitly declared and results are flowing between steps unchecked.
OpenClaw's outputSchema is a field that declares, in JSON Schema, what structure a tool will return. It's not merely a documentation aid — it's an operational component that performs actual validation at runtime and injects recovery signals into the agent loop on failure. As more teams track parse success rate, repair recovery rate, and schema validation rate as reliability metrics, how you design this mechanism determines a significant portion of agent stability.
This article walks through how to declare outputSchema, how the loop performs self-correction when validation fails, and where practitioners tend to diverge in judgment.
What outputSchema Does in the Agent Loop
What Is a Tool Contract
In the OpenClaw Plugin SDK, a tool plugin can declare an outputSchema field to specify the structure of its execution result as a JSON Schema. The JSON Schema specification has two incompatible variants — draft-07 and 2020-12 — with differing keywords such as $defs (2020-12) vs definitions (draft-07), prefixItems, and unevaluatedProperties. It's safest to check which dialect the OpenClaw runtime version supports in the Tool plugins documentation before writing your schema. Mixing dialects causes unknown keywords to be silently ignored by the runtime, quietly disabling validation.
This schema isn't consumed only at compile time like TypeScript type hints — validation actually runs at runtime. The sequence of operations is as follows:
- Before the tool call, OpenClaw first validates the
outputSchemaitself. If the schema is invalid, tool execution never starts (fail-fast). - After tool execution completes and hook processing finishes, the returned
detailsvalue is validated against the schema. - If validation fails, the call in
completedstate is treated as failed, and the error is returned to the agent loop.
How the Self-Correction Loop Actually Works
When validation fails, OpenClaw re-injects into the model context both the reason the previous call failed and the original outputSchema. Because the model sees the schema again, it can reference the required structure to regenerate the call, and through several iterations of this process the structural mismatch is resolved.
One thing to be careful about here is not to take numbers like "X% automatic recovery rate" at face value when cited without context. Recovery rates vary significantly by model, tool type, error distribution, and retry budget, so you must measure them separately in your own environment. The more important practical question is "what types of cases don't recover?" — and you need to be able to log those before you can design the routing logic discussed in the next section.
How to Declare and Use It in Practice
Basic outputSchema Declaration
The following is a conceptual example. It's written with reference to the plugin definition format of the OpenClaw Plugin SDK; for specific API signatures, check the Building plugins documentation for your runtime version.
// Conceptual example - based on OpenClaw Plugin SDK
const searchPlugin = {
name: "web_search",
description: "Search the web for information",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
maxResults: { type: "number" }
},
required: ["query"],
additionalProperties: false
},
outputSchema: {
type: "object",
properties: {
results: {
type: "array",
items: {
type: "object",
properties: {
title: { type: "string" },
url: { type: "string", format: "uri" },
snippet: { type: "string" }
},
required: ["title", "url", "snippet"],
additionalProperties: false
}
},
totalCount: { type: "number" },
searchedAt: { type: "string", format: "date-time" }
},
required: ["results", "totalCount"],
additionalProperties: false
},
execute: async (input) => {
// actual search logic
}
};Making additionalProperties: false a habit prevents the scenario where a tool returns an unexpected field that quietly flows into the next tool and explodes somewhere far downstream. It means keeping the contract clearly closed.
Including Structured Error Variants in the Schema
This is something practitioners often miss. Cases where a tool doesn't throw an exception but "fails gracefully" — zero search results, file not found, permission denied — need to be included in the schema as well.
// Conceptual example
outputSchema: {
oneOf: [
{
type: "object",
properties: {
status: { type: "string", const: "success" },
results: { type: "array", items: { /* ... */ } }
},
required: ["status", "results"],
additionalProperties: false
},
{
type: "object",
properties: {
status: { type: "string", const: "not_found" },
reason: { type: "string" }
},
required: ["status", "reason"],
additionalProperties: false
}
]
}If you omit this, every time the tool "successfully returns an empty result," validation will fail and you'll get wasted retries firing in the wrong place.
Setting Retry Limits
No matter how well the self-correction loop works, without an upper bound costs will explode. The problem of infinite loops and premature termination due to missing retry limits is repeatedly flagged in BetterClaw's operational experience writeup — keep in mind that post is a collection of the authors' operational observations rather than an independent statistical study, so use it as a reference alongside your own environment's metrics.
# Conceptual example - actual key names may vary by runtime version
maxIterations: 15
costCeiling: 0.50 # USD per task
retryBackoff: exponentialmaxIterations is the upper bound on total loop iterations; costCeiling is the per-task cost ceiling. Setting only one leaves the other free to run away, so having both is the safer approach.
Branching Failure Types in postToolCall Hooks
If you funnel schema validation failures and transient errors (like network timeouts) into the same retry path, calls that structurally can never succeed will repeat until the budget is exhausted. Branching by type is the core of recovery efficiency.
In the code below, the hook return shape and error type strings are example sketches only — actual field names, error codes, and context structure must be verified in the Plugin hooks documentation for your version. Copying this as-is risks field mismatches that silently neutralize the entire branching logic.
// Conceptual example - see Plugin hooks documentation for actual hook signatures
type HookContext = {
result?: unknown;
error?: { type?: string; message?: string };
toolName: string;
};
type HookResult =
| { escalate: true; reason: string }
| { retry: true; backoff: 'exponential' | 'linear' }
| { continue: true };
const plugin = {
hooks: {
postToolCall: async (context: HookContext): Promise<HookResult> => {
const { error, toolName } = context;
// The strings below are examples; map actual error codes from runtime documentation
if (error?.type === 'schema_validation_failed') {
console.error(`[${toolName}] Structural failure detected, escalating`);
return { escalate: true, reason: error.message ?? 'schema mismatch' };
}
if (error?.type === 'transient_error') {
return { retry: true, backoff: 'exponential' };
}
return { continue: true };
}
}
};Retrying a structural failure with the same arguments won't change the outcome. Because the arguments themselves are misaligned with the schema, the model needs to generate a new call for it to mean anything — so these belong in the escalation/routing path, not the retry path. Transient errors, on the other hand, are generally resolved with exponential backoff retries.
Recovery Flow Within the Agent Loop
Where the earlier flowchart covered state transitions inside the runtime, the sequence diagram below focuses on the order of message exchanges between the model, runtime, and tool.
Tradeoffs: What You Gain and What You Take On
Each item is paired as benefit / consideration to help frame the decision.
| Topic | What You Gain | What to Consider |
|---|---|---|
| Fail-fast | Tools with invalid schemas are blocked from executing, preventing bad results from propagating through the loop | Schema compilation adds to cold start time and can slow tool initialization. For high-frequency tools, consider caching compiled results |
| Automatic recovery | Re-injecting the error message and schema resolves a significant portion of structural mismatches within a few retries | Recovery rates vary widely by environment; measure separately on your own workload. Logging the type distribution of non-recovered cases is essential |
| Model-independent contract | Swapping models or mixing multiple models still has outputSchema enforcing type guarantees |
New models may violate schemas in different ways than old ones, requiring re-analysis of failure type distribution during migration |
| Auditability | Request and completion events are structured, leaving a log trail of the recovery process | Log volume increases, so you'll need a policy decision on whether to store successful retries alongside errors |
| Result variant handling | Using oneOf to enumerate success, partial success, and graceful failure brings all variants within the contract |
Missing a variant causes validation to fail on every normal failure, triggering retry explosions. Compile a per-tool result catalog early in schema design |
| Retry routing | Separating structural failures from transient errors prevents budget from being wasted | Incorrect error type mapping neutralizes the entire branching logic. Verify actual error codes against runtime documentation |
| Loop safety | maxIterations and costCeiling guard against worst-case recovery loop scenarios |
Too low a ceiling prematurely terminates cases that would otherwise resolve. Observe average iteration counts for your tool combinations before setting limits with headroom |
Common Mistakes
The most common pattern is deploying without outputSchema first. Problems aren't obvious early on, but as chains grow longer — passing tool results into other tools — debugging difficulty rises sharply. Declaring it upfront is ultimately faster.
Another is declaring schemas too loosely. If you only set type: "object" with empty properties, validation is effectively a rubber stamp. Make it your default to list required fields in required and close the schema with additionalProperties: false.
A related issue worth knowing about is Issue #45049, which reports the agent loop allowing simulated tool calls instead of enforcing real tool invocation. Schema validation helps partially but isn't a complete fix. Monitoring actual invocation with separate metrics is the safer approach.
repair-then-validate in a Post-Processing Layer
If you have a pipeline that receives tool results outside the OpenClaw runtime and post-processes them, adding a second line of defense with a validation library like Pydantic is a valid choice. It's a stretch to call this pattern an industry standard, but attempting automatic repair locally on fields that frequently violate their schema is something practitioners commonly adopt.
# Conceptual example - based on Pydantic v2
from pydantic import BaseModel, ValidationError
from typing import List
class SearchResult(BaseModel):
title: str
url: str
snippet: str
model_config = {"extra": "forbid"}
class SearchOutput(BaseModel):
results: List[SearchResult]
total_count: int
def repair_and_validate(raw_output: dict) -> SearchOutput:
try:
return SearchOutput.model_validate(raw_output)
except ValidationError as e:
repaired = attempt_repair(raw_output, e)
if repaired is not None:
return SearchOutput.model_validate(repaired)
raise
def attempt_repair(data: dict, error: ValidationError) -> dict | None:
repaired = dict(data)
for err in error.errors():
if err["type"] == "int_parsing" and err["loc"] == ("total_count",):
try:
repaired["total_count"] = int(repaired.get("total_count", 0))
return repaired
except (ValueError, TypeError):
return None
return NoneIt's better to keep the repair scope narrow. As repair rules multiply, the pipeline starts swallowing contract violations from the original tool, and the purpose of enforcing the contract becomes blurred. Always count repaired cases as a separate metric so you can look back and ask "why does this tool consistently violate its contract?"
Closing Thoughts
When adopting outputSchema, the judgment calls practitioners most commonly face tend to split three ways.
First, how tightly to close the schema. Fully closing the contract with additionalProperties: false catches unexpected fields early, but the moment a tool vendor adds a response field, everything fails. For tools that wrap external APIs, consider a compromise that leaves specific sub-objects open.
Second, when to escalate failures. Structural failures that don't resolve after a couple of retries are better sent to a human intervention queue; transient errors are naturally absorbed within budget using exponential backoff. Without separating the two types in a hook, either one just burns budget.
Third, what observations to base retry budgets on. Many teams set maxIterations and costCeiling by intuition, but it's safer to observe average iteration counts per tool combination and cost distribution to normal completion for at least a few days before setting limits with headroom. Too low a ceiling prematurely terminates recoverable cases; too high, and it fails as a safety net.
Because supported dialects and hook signatures can change across OpenClaw runtime versions, when upgrading review the release notes for outputSchema-related changes and audit your existing plugin definitions at the same time.
References
- Agent loop · OpenClaw official documentation
- Tool plugins · OpenClaw official documentation
- Plugin hooks · OpenClaw official documentation
- Building plugins · OpenClaw official documentation
- Agent loop allows simulated tool calls instead of enforcing real tool invocation · Issue #45049
- OpenClaw Agent Stuck in a Loop: 5 Causes and How to Break It · BetterClaw