By Sagar Shankaran, Founder of CallSphere
Deep dive into HandoffInputData, input_filter functions, built-in handoff filters, custom filtering strategies, and input_type with Pydantic models for structured handoff metadata in the OpenAI Agents SDK.
Key takeaways
When Agent A hands off to Agent B, the entire conversation history travels with it by default. This is often not what you want. The conversation might contain irrelevant context, sensitive data that Agent B should not see, or tool call results that confuse the target agent.
Input filters give you precise control over what the target agent receives. They sit between the handoff trigger and the target agent's first inference call, transforming the conversation history before it arrives.
Every input filter receives a HandoffInputData object. This is the complete package of information being transferred during a handoff:
flowchart LR
INPUT(["User input"])
AGENT["Agent<br/>name plus instructions"]
HAND{"Handoff to<br/>another agent?"}
SUB["Sub-agent<br/>specialist"]
GUARD{"Guardrail<br/>passed?"}
TOOL["Tool call"]
SDK[("Tracing<br/>OpenAI dashboard")]
OUT(["Final output"])
INPUT --> AGENT --> HAND
HAND -->|Yes| SUB --> GUARD
HAND -->|No| GUARD
GUARD -->|Yes| TOOL --> AGENT
GUARD -->|Block| OUT
AGENT --> OUT
AGENT --> SDK
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style SDK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass
@dataclass
class HandoffInputData:
input_history: list # The conversation messages up to the handoff
pre_handoff_items: list # Items generated before the handoff in the current run
new_items: list # Items generated during the handoff itself
The three fields represent different segments of the conversation:
Runner.run(), plus any accumulated conversation from prior turnsUnderstanding this split is critical for writing effective filters. For example, you might want to keep the original user messages (input_history) but strip out all intermediate agent reasoning (pre_handoff_items).
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
The SDK provides a useful built-in filter for the most common case:
This filter strips all tool call and tool result messages from the conversation history before passing it to the target agent:
from agents import Agent, handoff, handoff_filters
specialist_agent = Agent(
name="SpecialistAgent",
instructions="Handle complex technical issues.",
model="gpt-4o",
)
triage_agent = Agent(
name="TriageAgent",
instructions="Route issues to specialists.",
model="gpt-4o",
handoffs=[
handoff(
specialist_agent,
description="Technical specialist",
input_filter=handoff_filters.remove_all_tools,
),
],
)
When to use this: When the target agent does not need to see what tools the source agent called. Tool calls in the history can confuse the target agent or waste context window tokens on irrelevant information.
When not to use this: When the target agent needs to understand what the source agent already tried. For example, if the source agent ran a diagnostic tool and found an error, the target agent should probably see that result.
An input filter is any callable that takes a HandoffInputData and returns a HandoffInputData:
from agents import Agent, handoff, HandoffInputData
def keep_only_user_messages(data: HandoffInputData) -> HandoffInputData:
"""Strip everything except user messages from the history."""
filtered_history = [
msg for msg in data.input_history
if hasattr(msg, 'role') and msg.role == 'user'
]
return HandoffInputData(
input_history=filtered_history,
pre_handoff_items=[], # Drop all pre-handoff items
new_items=data.new_items, # Keep the handoff trigger
)
specialist_agent = Agent(
name="Specialist",
instructions="Help the customer based on their messages.",
model="gpt-4o",
)
triage_agent = Agent(
name="Triage",
instructions="Route to specialist.",
model="gpt-4o",
handoffs=[
handoff(
specialist_agent,
description="Specialist for complex issues",
input_filter=keep_only_user_messages,
),
],
)
A common production requirement is stripping PII or sensitive data before a handoff:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import re
from agents import HandoffInputData
def redact_sensitive_data(data: HandoffInputData) -> HandoffInputData:
"""Redact credit card numbers, SSNs, and email addresses."""
def redact_text(text: str) -> str:
# Redact credit card numbers (basic pattern)
text = re.sub(r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '[REDACTED_CC]', text)
# Redact SSNs
text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[REDACTED_SSN]', text)
# Redact email addresses
text = re.sub(r'\b[\w.-]+@[\w.-]+\.\w+\b', '[REDACTED_EMAIL]', text)
return text
def redact_message(msg):
if hasattr(msg, 'content') and isinstance(msg.content, str):
msg = msg.model_copy(update={"content": redact_text(msg.content)})
return msg
return HandoffInputData(
input_history=[redact_message(m) for m in data.input_history],
pre_handoff_items=[redact_message(m) for m in data.pre_handoff_items],
new_items=[redact_message(m) for m in data.new_items],
)
For conversations that span many turns, passing the full history to the target agent wastes tokens and can degrade performance. You can truncate or summarize:
from agents import HandoffInputData
def truncate_history(max_messages: int = 10):
"""Return a filter that keeps only the last N messages."""
def _filter(data: HandoffInputData) -> HandoffInputData:
truncated = data.input_history[-max_messages:]
return HandoffInputData(
input_history=truncated,
pre_handoff_items=data.pre_handoff_items,
new_items=data.new_items,
)
return _filter
# Usage
triage_agent = Agent(
name="Triage",
instructions="Route to specialist.",
model="gpt-4o",
handoffs=[
handoff(
specialist_agent,
description="Specialist",
input_filter=truncate_history(max_messages=5),
),
],
)
You can chain filters together for complex filtering pipelines:
from agents import HandoffInputData
def compose_filters(*filters):
"""Chain multiple input filters together."""
def _composed(data: HandoffInputData) -> HandoffInputData:
result = data
for f in filters:
result = f(result)
return result
return _composed
# Combine redaction with truncation
combined_filter = compose_filters(
redact_sensitive_data,
truncate_history(max_messages=10),
)
triage_agent = Agent(
name="Triage",
instructions="Route to specialist.",
model="gpt-4o",
handoffs=[
handoff(
specialist_agent,
description="Specialist",
input_filter=combined_filter,
),
],
)
The input_type parameter lets you define a Pydantic model that the source agent must fill in when triggering the handoff. This serves two purposes:
on_handoff callback typed data to work withfrom agents import Agent, handoff, RunContextWrapper
from pydantic import BaseModel, Field
class RoutingDecision(BaseModel):
"""Metadata the triage agent must provide when routing."""
reason: str = Field(description="Why this customer is being routed to this department")
priority: str = Field(description="low, medium, high, or critical")
customer_sentiment: str = Field(description="positive, neutral, frustrated, or angry")
issue_summary: str = Field(description="One-sentence summary of the customer's issue")
async def on_handoff_callback(
context: RunContextWrapper[dict],
input_data: RoutingDecision,
) -> None:
# Log the structured routing decision
print(f"Routing: {input_data.reason}")
print(f"Priority: {input_data.priority}")
print(f"Sentiment: {input_data.customer_sentiment}")
# Store for analytics
context.context["routing_decision"] = input_data.model_dump()
support_agent = Agent(
name="Support",
instructions="Handle technical issues.",
model="gpt-4o",
)
triage_agent = Agent(
name="Triage",
instructions="""Route customers to support. When routing, assess:
- The reason for routing
- Priority level (low/medium/high/critical)
- Customer sentiment (positive/neutral/frustrated/angry)
- A one-sentence issue summary""",
model="gpt-4o",
handoffs=[
handoff(
support_agent,
description="Technical support",
input_type=RoutingDecision,
on_handoff=on_handoff_callback,
),
],
)
from pydantic import BaseModel, Field
from enum import Enum
class Department(str, Enum):
BILLING = "billing"
SUPPORT = "support"
SALES = "sales"
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class BillingHandoffInput(BaseModel):
issue_type: str = Field(description="Type: charge_dispute, refund_request, plan_change, invoice_question")
amount_involved: float | None = Field(default=None, description="Dollar amount if mentioned")
account_verified: bool = Field(description="Whether the customer provided account details")
priority: Priority
summary: str
class SupportHandoffInput(BaseModel):
product_area: str = Field(description="Which product area: api, dashboard, mobile, integrations")
error_code: str | None = Field(default=None, description="Error code if the customer mentioned one")
steps_to_reproduce: str | None = Field(default=None, description="Steps if the customer described them")
priority: Priority
summary: str
# Each handoff gets its own input type
triage_agent = Agent(
name="Triage",
instructions="""Gather necessary information and route to the right department.
For billing: determine issue type, amount, and verify account.
For support: determine product area, error codes, and reproduction steps.""",
model="gpt-4o",
handoffs=[
handoff(
billing_agent,
description="Billing department",
input_type=BillingHandoffInput,
on_handoff=on_billing_handoff,
),
handoff(
support_agent,
description="Technical support",
input_type=SupportHandoffInput,
on_handoff=on_support_handoff,
),
],
)
You can use both input_filter and input_type on the same handoff. The input_type controls what metadata the source agent provides, while the input_filter controls what conversation history the target agent receives:
triage_agent = Agent(
name="Triage",
instructions="Route customers effectively.",
model="gpt-4o",
handoffs=[
handoff(
support_agent,
description="Technical support",
input_type=SupportHandoffInput, # Source agent provides this
input_filter=redact_sensitive_data, # History gets filtered
on_handoff=on_support_handoff, # Callback receives SupportHandoffInput
),
],
)
The execution order is:
SupportHandoffInput fieldson_handoff callback fires with the structured input datainput_filter processes the conversation historyfrom agents import HandoffInputData
from datetime import datetime
import json
def audit_trail_filter(data: HandoffInputData) -> HandoffInputData:
"""Add an audit entry at the start of the history for the target agent."""
audit_message = {
"role": "system",
"content": f"[AUDIT] Handoff received at {datetime.now().isoformat()}. "
f"History contains {len(data.input_history)} messages and "
f"{len(data.pre_handoff_items)} pre-handoff items."
}
return HandoffInputData(
input_history=data.input_history,
pre_handoff_items=data.pre_handoff_items,
new_items=data.new_items,
)
def inject_department_context(department: str, knowledge_base: dict):
"""Inject department-specific context into the handoff."""
def _filter(data: HandoffInputData) -> HandoffInputData:
context_message = {
"role": "system",
"content": f"Department: {department}\n"
f"Available actions: {json.dumps(knowledge_base.get(department, {}))}"
}
# Prepend department context to pre_handoff_items
return HandoffInputData(
input_history=data.input_history,
pre_handoff_items=data.pre_handoff_items,
new_items=data.new_items,
)
return _filter
Input filters and callbacks are the precision tools of the handoff system. Use input_filter to control what conversation history the target agent sees — strip tools, redact PII, truncate long histories, or inject context. Use input_type with Pydantic models to force the source agent into structured reasoning about the handoff. Use on_handoff callbacks for logging, analytics, and state initialization. Together, these three mechanisms give you production-grade control over every agent transition in your multi-agent system.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
OpenAI's Frontier platform makes model-native orchestration the default. What that means for agent builders, voice/chat buyers, and the build-vs-buy decision.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
May 2026's biggest agent-architecture shift: planning, tool selection, and self-correction move inside the model. Framework code shrinks. Here is what changes.
A three-way comparison of Gemini Enterprise, Anthropic managed agents and OpenAI Frontier Platform after Cloud Next 2026 — strengths, gaps, buyer fit.
Anthropic's May 2026 push positions Claude as a vertical platform for financial services. The strategic positioning versus OpenAI and Google.
Anthropic's Mythos is not alone. Compare Mythos against OpenAI's cybersec offerings, Google's Big Sleep lineage, and open-source alternatives in 2026.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI