Fix Structured Output and Tool Calling Errors in OpenAI Agents SDK
Structured output and tool calling errors in the OpenAI Agents SDK most often come from three sources: Pydantic models that produce schemas the API rejects, tool functions with incorrect type annotations, and guardrail or handoff configurations that conflict with the output type. The fix is almost always to tighten your Pydantic model (use Literal instead of Enum, avoid Optional unions at the top level, and set strict=True in your model config), then make sure that every tool function has fully annotated parameters with docstrings the SDK can parse.
Correct Structured Output Setup
The output_type parameter on Agent must be a Pydantic BaseModel subclass with a JSON Schema that OpenAI's API accepts. The API enforces strict mode, which means no Optional fields unless you wrap them properly, no default values, and additionalProperties must be false. The following snippet shows the minimal working pattern.
from pydantic import BaseModel
from agents import Agent, Runner
import asyncio
class AnalysisResult(BaseModel):
# Strict mode requires every field to be explicitly typed.
summary: str
sentiment: str
confidence: float
analysis_agent = Agent(
name="Analyzer",
instructions="Analyze the user's text and return structured results.",
output_type=AnalysisResult,
)
result = asyncio.run(Runner.run(analysis_agent, input="I love this product!"))
# final_output is now an AnalysisResult instance, not raw text.
print(result.final_output.summary)
The Most Common Schema Error: Optional and Union Types
The error Invalid schema for response_format: ... is not allowed almost always means that your Pydantic model contains a type the strict schema rejects. The top offenders are Optional[str] (which becomes an anyOf union), bare dict, and Enum subclasses. The API expects you to use Literal for constrained strings. If a field truly can be null, you must use the | None syntax with a default of None only on non-top-level fields.
from typing import Literal
from pydantic import BaseModel
# BAD: This will fail strict schema validation.
class BadResult(BaseModel):
# Optional creates a union that strict mode rejects at the top level.
category: str | None = None
# GOOD: Use Literal for constrained values, plain types otherwise.
class GoodResult(BaseModel):
category: Literal["positive", "negative", "neutral"]
explanation: str
Tool Calling: Function Tools That Actually Work
The SDK converts Python functions into tool schemas automatically through @function_tool. The most frequent error here is ModelBehaviorError: Tool call malformed or missing, which happens when the model can't figure out how to call your function because parameter types are ambiguous or the docstring is missing. Every tool function needs a docstring (the SDK uses it as the tool description) and fully annotated parameters. Complex parameter types like nested dicts cause silent failures, so use flat Pydantic models instead.
from agents import Agent, Runner, function_tool
import asyncio
@function_tool
def lookup_order(order_id: str, include_history: bool) -> str:
"""Look up an order by its ID. Set include_history to true for full timeline."""
# Simulate a database lookup.
return f"Order {order_id}: shipped on 2025-01-15, status=delivered"
order_agent = Agent(
name="OrderBot",
instructions="Help users check their order status.",
tools=[lookup_order],
)
result = asyncio.run(Runner.run(order_agent, input="Where is order ABC-123?"))
print(result.final_output)
Combining Structured Output with Tools
You can use both tools and output_type on the same agent. The agent calls tools as needed, then produces the final structured response. The gotcha is that the model must know when to stop calling tools and emit the structured output. If your instructions are vague, the model might loop on tool calls indefinitely. Be explicit in your instructions: tell the agent to gather data through tools first, then return the final answer in the specified format.
from pydantic import BaseModel
from agents import Agent, Runner, function_tool
import asyncio
class OrderReport(BaseModel):
order_id: str
status: str
estimated_delivery: str
@function_tool
def fetch_order(order_id: str) -> str:
"""Fetch order details from the database."""
return '{"id": "ABC-123", "status": "shipped", "eta": "2025-07-20"}'
report_agent = Agent(
name="Reporter",
instructions="Use fetch_order to get data, then return an OrderReport.",
tools=[fetch_order],
output_type=OrderReport,
)
result = asyncio.run(Runner.run(report_agent, input="Report on ABC-123"))
print(result.final_output.status)
Debugging Malformed JSON and Whitespace Errors
If you see OutputGuardrailTripwireTriggered or a raw JSON parse error, the model returned text that doesn't match your schema. This happens more often with smaller models like gpt-4o-mini than with gpt-4o. Three strategies help. First, simplify your schema by using fewer fields and no nesting. Second, add an explicit example in the instructions. Third, use the model_settings parameter to lower temperature to zero for maximum determinism. For strict parsing, the SDK already sends response_format with type: json_schema under the hood, but the model can still refuse or truncate if the context window overflows.
from agents import Agent, ModelSettings
# Lower temperature and set a generous max_tokens to prevent truncation.
strict_agent = Agent(
name="StrictBot",
instructions=(
"Return the analysis as structured output. "
"Do not include any text outside the JSON object."
),
output_type=AnalysisResult,
model_settings=ModelSettings(temperature=0.0, max_tokens=1024),
)
Handoffs and Output Type Conflicts
A subtle bug appears when you use handoffs between agents that have different output_type values. If Agent A hands off to Agent B, whichever agent finishes last determines the final output type. If your runner expects AgentAResult but Agent B returns AgentBResult, you get a type mismatch at runtime. The fix is to either give all agents in a handoff chain the same output_type, or check result.last_agent to know which type you received.
from agents import Agent, Runner
import asyncio
# Both agents share the same output type to avoid mismatch.
refund_agent = Agent(
name="RefundBot",
instructions="Handle refund requests and return an OrderReport.",
output_type=OrderReport,
)
triage_agent = Agent(
name="Triage",
instructions="Route refund questions to RefundBot.",
handoffs=[refund_agent],
output_type=OrderReport,
)
result = asyncio.run(Runner.run(triage_agent, input="I want a refund for ABC-123"))
# Check which agent actually produced the output.
print(f"Handled by: {result.last_agent.name}")
Quick Reference: Error Messages and Fixes
Invalid schema for response_format. Your Pydantic model uses Optional, Union, Enum, or dict in a way that strict mode rejects. Simplify the model, use Literal for enums, and avoid top-level optional fields.
ModelBehaviorError: Tool call malformed. Your @function_tool function is missing a docstring or has untyped parameters. Add type annotations to every parameter and include a one-line docstring.
OutputGuardrailTripwireTriggered. An output guardrail rejected the response. Check your guardrail logic; the model's output might have been valid JSON but failed your custom validation.
MaxTurnsExceeded. The agent looped on tool calls without producing a final output. Set max_turns in Runner.run() and write your instructions to tell the agent explicitly when to stop calling tools and return the structured answer.
General debugging tip: Enable tracing by setting OPENAI_AGENTS_TRACING_ENABLED=true as an environment variable. The SDK logs each step (tool calls, model responses, handoffs) so you can see exactly where the chain breaks.