---
title: "Customizing Handoffs with Input Filters and Callbacks"
description: "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."
canonical: https://callsphere.ai/blog/customizing-handoffs-input-filters-callbacks-openai-agents-sdk
category: "Learn Agentic AI"
tags: ["OpenAI", "Handoffs", "Input Filters", "Callbacks", "Python"]
author: "CallSphere Team"
published: 2026-03-14T00:00:00.000Z
updated: 2026-05-18T01:11:29.662Z
---

# Customizing Handoffs with Input Filters and Callbacks

> 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.

## Why Input Filters Matter

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.

## The HandoffInputData Structure

Every input filter receives a `HandoffInputData` object. This is the complete package of information being transferred during a handoff:

```mermaid
flowchart LR
    INPUT(["User input"])
    AGENT["Agent
name plus instructions"]
    HAND{"Handoff to
another agent?"}
    SUB["Sub-agent
specialist"]
    GUARD{"Guardrail
passed?"}
    TOOL["Tool call"]
    SDK[("Tracing
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
```

```python
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:

- **input_history**: The original input messages passed to `Runner.run()`, plus any accumulated conversation from prior turns
- **pre_handoff_items**: Messages and tool calls generated by agents before this handoff occurred in the current run
- **new_items**: The handoff tool call itself and any immediately associated messages

Understanding 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).

## Built-in Handoff Filters

The SDK provides a useful built-in filter for the most common case:

### handoff_filters.remove_all_tools

This filter strips all tool call and tool result messages from the conversation history before passing it to the target agent:

```python
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.

## Writing Custom Input Filters

An input filter is any callable that takes a `HandoffInputData` and returns a `HandoffInputData`:

```python
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,
        ),
    ],
)
```

### Filtering Sensitive Information

A common production requirement is stripping PII or sensitive data before a handoff:

```python
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],
    )
```

### Summarizing Long Histories

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:

```python
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),
        ),
    ],
)
```

### Composing Multiple Filters

You can chain filters together for complex filtering pipelines:

```python
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,
        ),
    ],
)
```

## input_type: Structured Handoff Metadata

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:

1. It forces the source agent to provide structured reasoning about the handoff
2. It gives the `on_handoff` callback typed data to work with

### Basic input_type Example

```python
from 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,
        ),
    ],
)
```

### Complex input_type for Multi-Department Routing

```python
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,
        ),
    ],
)
```

## Combining Input Filters with input_type

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:

```python
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:

1. Source agent decides to hand off and fills in the `SupportHandoffInput` fields
2. The `on_handoff` callback fires with the structured input data
3. The `input_filter` processes the conversation history
4. The filtered history is passed to the target agent

## Production Patterns

### Audit Trail Filter

```python
from 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,
    )
```

### Department-Specific Context Injection

```python
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
```

## Summary

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.

---

Source: https://callsphere.ai/blog/customizing-handoffs-input-filters-callbacks-openai-agents-sdk
