How to Implement Multi-Agent Workflows with Agent-to-Agent Delegation in openai-agents-python

Use the handoff() helper to expose one agent as a tool on another, then wire the Runner with nest_handoff_history=True to automatically transfer context and conversation state between agents.

The openai-agents-python SDK provides a first-class handoff mechanism for building multi-agent workflows. By treating agents as callable tools, you can implement complex delegation patterns such as triage → specialist → resolution while preserving conversation context across agent boundaries.

Core Handoff Concepts

The delegation system relies on several interconnected components defined in src/agents/handoffs/__init__.py and src/agents/handoffs/history.py:

  • Handoff dataclass (lines 42‑56): Describes the handoff tool, including the name, description, JSON schema, and the function that creates the target agent instance.
  • handoff() helper (lines 88‑135): Validates input schemas, builds the Handoff object, and wires the tool into the model’s tool list.
  • HandoffInputData (lines 44‑71): A dataclass that carries the transcript, pre‑handoff items, new items, and optional input_filter results to the next agent.
  • nest_handoff_history (history.py lines 71‑104): Automatically injects a summarized conversation snippet into the next agent’s input, preventing token bloat while maintaining context.
  • input_filter: An optional callable (lines 27‑34) that prunes or reshapes items before the receiving agent sees them, useful for privacy or deduplication.

Step 1: Define Specialized Agents

Create distinct Agent instances, each with its own tools and system instructions. In src/agents/agent.py, the Agent class accepts a tools list that can include both function tools and handoff tools.


# agents.py

from agents import Agent, function_tool

# -------------------------------------------------

# Triage agent – decides which specialist to call

# -------------------------------------------------

def triage_logic(request: str) -> str:
    """Simple rule‑based triage: keywords → specialist name."""
    if "billing" in request.lower():
        return "billing"
    if "technical" in request.lower():
        return "tech"
    return "general"

triage = Agent(
    name="triage",
    description="Routes the user to the appropriate specialist.",
    tools=[
        function_tool(triage_logic, name="triage", description="Return the specialist name")
    ],
)

# -------------------------------------------------

# Billing specialist

# -------------------------------------------------

billing = Agent(
    name="billing",
    description="Handles billing‑related questions.",
    tools=[],
)

# -------------------------------------------------

# Technical specialist

# -------------------------------------------------

technical = Agent(
    name="technical",
    description="Handles technical support queries.",
    tools=[],
)

# -------------------------------------------------

# General fallback agent

# -------------------------------------------------

general = Agent(
    name="general",
    description="Catches all other requests.",
    tools=[],
)

Step 2: Configure Agent-to-Agent Handoffs

Use the handoff() helper (defined in src/agents/handoffs/__init__.py lines 88‑135) to create delegation tools. Then attach these tools to the orchestrator agent (triage).


# handoffs.py

from agents import handoff
from agents import triage, billing, technical, general

# -------------- Billing handoff --------------

billing_handoff = handoff(
    billing,
    tool_name_override="transfer_to_billing",
    nest_handoff_history=True,          # include concise transcript summary

)

# -------------- Technical handoff --------------

tech_handoff = handoff(
    technical,
    tool_name_override="transfer_to_technical",
    nest_handoff_history=True,
)

# -------------- General handoff (fallback) --------------

general_handoff = handoff(
    general,
    tool_name_override="transfer_to_general",
    nest_handoff_history=True,
)

# Attach the handoffs as *tools* on the triage agent

triage.tools.extend([billing_handoff, tech_handoff, general_handoff])

When the LLM calls transfer_to_billing, the SDK executes Handoff._invoke_handoff, returns the target Agent object, and automatically prepares the next turn’s input using HandoffInputData.

Step 3: Execute the Multi-Agent Workflow

Instantiate Runner (from src/agents/run.py) with RunConfig to control history nesting and safety limits.


# run_workflow.py

from agents import Runner, RunConfig
from agents import triage  # import the triage agent defined earlier

from handoffs import billing_handoff, tech_handoff, general_handoff

run_cfg = RunConfig(
    nest_handoff_history=True,  # Enable global history nesting

    max_turns=10,               # Prevent infinite loops

)

runner = Runner(agent=triage, config=run_cfg)

# Example user request that triggers the billing specialist

result = runner.run("I have a question about my last invoice.")
print("Final response:", result.output)

Execution flow:

  1. Turn 1: triage analyzes the request and generates a tool call transfer_to_billing.
  2. Turn 2: Runtime executes the handoff, invokes nest_handoff_history() (from src/agents/handoffs/history.py lines 71‑104) to summarize prior turns, and injects the summary into the billing agent’s input.
  3. Turn 3: billing receives the summarized transcript plus the original user request, runs its logic, and returns the final answer.

Advanced Customization Options

Conditional Handoff with is_enabled

Control visibility of handoff tools using the is_enabled parameter (handled in src/agents/handoffs/__init__.py lines 14‑23).

def billing_enabled(ctx, agent):
    import datetime
    now = datetime.datetime.utcnow().hour
    return 9 <= now <= 17  # UTC business hours only

billing_handoff = handoff(
    billing,
    is_enabled=billing_enabled,
)

Input Filtering for Privacy

Use input_filter (defined in src/agents/handoffs/__init__.py lines 27‑34) to sanitize data before the next agent receives it.

def strip_sensitive_data(input_data):
    filtered = tuple(
        item for item in input_data.new_items
        if not (hasattr(item, "content") and "ssn" in str(item.content))
    )
    return input_data.clone(new_items=filtered)

billing_handoff = handoff(
    billing,
    input_filter=strip_sensitive_data,
    nest_handoff_history=True,
)

Custom History Summarization

Override the default mapper by passing a history_mapper callable to customize how conversation history is compressed.

def bullet_summary(transcript):
    lines = [f"* {item['role']}: {item.get('content', '')}" for item in transcript]
    summary = "\n".join(lines) or "(no previous turns)"
    return [{
        "role": "assistant",
        "content": f"The conversation so far:\n{summary}",
    }]

billing_handoff = handoff(
    billing,
    nest_handoff_history=True,
    history_mapper=bullet_summary,
)

Summary

  • Create separate agents by instantiating Agent objects with distinct tool sets and instructions.
  • Expose handoff tools using handoff(target_agent, ...) and optionally override tool_name_override for clearer LLM prompts.
  • Attach handoffs to the orchestrator agent (e.g., triage) by extending its tools list.
  • Preserve context by setting nest_handoff_history=True in RunConfig or individual handoffs, which invokes nest_handoff_history() from src/agents/handoffs/history.py.
  • Filter sensitive data by providing an input_filter callable that returns a cloned HandoffInputData with sanitized items.
  • Control availability using is_enabled to dynamically hide handoff tools based on runtime conditions like time of day or user permissions.
  • Execute workflows by passing the entry agent to Runner with RunConfig(max_turns=...) to prevent infinite loops.

Frequently Asked Questions

How does conversation history transfer between agents?

The SDK automatically manages history transfer through the nest_handoff_history function in src/agents/handoffs/history.py. When enabled, it summarizes prior conversation turns using default_handoff_history_mapper and injects the summary into the receiving agent's input wrapped in <CONVERSATION HISTORY> markers. This prevents token bloat while ensuring the specialist agent retains necessary context.

Can I prevent a handoff from appearing based on runtime conditions?

Yes. Pass an is_enabled parameter to the handoff() function—either a boolean or a callable receiving RunContextWrapper and the target Agent. If the callable returns False, the handoff tool is hidden from the LLM for that specific turn. This is implemented in src/agents/handoffs/__init__.py and enables time-based routing or feature-flagging specific agents.

What is the difference between input_filter and nest_handoff_history?

nest_handoff_history is a boolean flag that controls whether a summarized conversation snippet is prepended to the next agent's input using the history mapper. input_filter is an optional callable that receives the full HandoffInputData object—containing new_items, pre_handoff_items, and the transcript—and returns a modified version before the next agent receives it. Use input_filter to remove PII, deduplicate tool calls, or reshape payloads while keeping the full history intact in the session store.

How do I debug a multi-agent workflow?

Set max_turns in RunConfig to prevent infinite loops during development. Inspect the HandoffInputData objects inside your input_filter functions to verify exactly what content passes between agents. Enable detailed logging around the handoff() helper and Runner.run() calls. Finally, review the source implementation in src/agents/handoffs/__init__.py and src/agents/run.py to understand the exact orchestration logic and error handling paths.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →