> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Handoffs

> Delegate tasks between specialised agents so each handles its area of expertise

Handoffs let one agent transfer a conversation to a specialist agent based on the request.

<Note>
  Replaces the deprecated `allow_delegation=True` — see [Legacy Agent Parameters](/docs/features/agent-legacy-params).
</Note>

<Tabs>
  <Tab title="Python">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    billing = Agent(name="Billing", instructions="Handle billing inquiries.")
    refund = Agent(name="Refunds", instructions="Process refund requests.")
    triage = Agent(
        name="Triage",
        instructions="Route customer inquiries to the right specialist.",
        handoffs=[billing, refund],
    )

    triage.start("I need a refund for my last order")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import { Agent } from 'praisonai';

    const billing = new Agent({ name: 'Billing', instructions: 'Handle billing inquiries.' });
    const refund = new Agent({ name: 'Refunds', instructions: 'Process refund requests.' });
    const triage = new Agent({
      name: 'Triage',
      instructions: 'Route customer inquiries to the right specialist.',
      handoffs: [billing, refund],
    });

    await triage.start('I need a refund for my last order');
    ```
  </Tab>
</Tabs>

The user describes their issue; the triage agent delegates to a specialist via a handoff tool call.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Agent Handoffs"
        User[👤 User] --> Triage[🤖 Triage Agent]
        Triage -->|billing| Billing[💳 Billing Agent]
        Triage -->|refund| Refund[🔄 Refund Agent]
        Billing --> Response[✅ Response]
        Refund --> Response
    end

    classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef triage fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef specialist fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class User user
    class Triage triage
    class Billing,Refund specialist
    class Response result
```

<Note>
  Handoffs are secure by default — the target agent only inherits tools shared with the source agent. See [Handoff Tool Policy](/docs/features/handoff-tool-policy).
</Note>

## Quick Start

<Steps>
  <Step title="Pass agents directly">
    Pass specialist agents to `handoffs` and the routing agent gets a transfer tool for each.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    billing = Agent(name="Billing", instructions="Handle billing inquiries and payments.")
    refund = Agent(name="Refunds", instructions="Process refund requests.")

    triage = Agent(
        name="Triage",
        instructions="Route customer inquiries to the right specialist.",
        handoffs=[billing, refund],
    )
    triage.start("I need a refund for my order")
    ```
  </Step>

  <Step title="Configure the handoff tool">
    Use `handoff()` to rename the transfer tool or steer when the agent should call it.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, handoff

    escalation = Agent(name="Manager", instructions="Handle escalations.")

    triage = Agent(
        name="Triage",
        instructions="Route inquiries; escalate hard cases.",
        handoffs=[
            handoff(
                escalation,
                tool_name_override="escalate_to_manager",
                tool_description_override="Escalate complex issues to a manager.",
            ),
        ],
    )
    triage.start("This is my third failed delivery — I want a manager.")
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Triage
    participant Specialist

    User->>Triage: Request
    Triage->>Triage: Decide which specialist fits
    Triage->>Specialist: transfer_to_<agent> tool call
    Specialist->>Specialist: Run full chat pipeline
    Specialist-->>Triage: Response
    Triage-->>User: Final answer
```

When you set `handoffs`, PraisonAI converts each target into a `transfer_to_<agent>` tool, adds routing instructions to the agent's prompt, and passes conversation history when control transfers.

| Phase       | What happens                                     |
| ----------- | ------------------------------------------------ |
| 1. Detect   | The routing agent decides a specialist is needed |
| 2. Transfer | The matching handoff tool is called              |
| 3. Run      | The target agent runs its full `chat()` pipeline |
| 4. Return   | The specialist's response flows back to the user |

***

## Which Handoff Setup to Use?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start([How should routing work?]) --> Q1{Need custom naming or criteria?}
    Q1 -->|No, simple routing| A["Pass Agent directly<br/>handoffs=&#91;billing, refund&#93;"]
    Q1 -->|Steer the choice| B["handoff&#40;&#41; with<br/>tool_description_override"]
    Q1 -->|Need typed input| C["TypedHandoff with<br/>a Pydantic schema"]

    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff

    class Start start
    class Q1 question
    class A,B,C answer
```

***

## Configuration Options

`handoff()` builds a configured transfer tool for a target agent.

<Card icon="code" href="/docs/features/handoff-tool-policy">
  Handoff Tool Policy — tool security boundary options
</Card>

| Option                      | Type                                 | Default       | Description                                                                                                                              |
| --------------------------- | ------------------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `agent`                     | `Agent`                              | required      | Target agent to hand off to                                                                                                              |
| `tool_name_override`        | `str \| None`                        | `None`        | Custom tool name (default `transfer_to_<agent>`)                                                                                         |
| `tool_description_override` | `str \| None`                        | `None`        | Custom tool description shown to the LLM                                                                                                 |
| `on_handoff`                | `Callable \| None`                   | `None`        | Callback run when the handoff is invoked                                                                                                 |
| `input_type`                | `type \| None`                       | `None`        | Expected input type for structured data                                                                                                  |
| `input_filter`              | `Callable \| list[Callable] \| None` | `None`        | Function (or chain) to filter/transform the history the target sees. The filtered result is now seeded onto the target's `chat_history`. |
| `tool_policy_mode`          | `"intersect" \| "passthrough"`       | `"intersect"` | Which tools the target inherits                                                                                                          |
| `blocked_tools`             | `list[str] \| None`                  | `None`        | Tools always stripped from the target                                                                                                    |

<Note>
  **Handoff context threading.** The messages produced by the configured `context_policy` and `input_filter` are seeded onto the target agent's `chat_history` for the duration of the handoff, then the target's original history is restored on exit. Seeding is invocation-scoped: it does not leak into later handoffs or ordinary chats that reuse the same target, and sequential handoffs do not accumulate context. See [Handoff Filters](/docs/features/handoff-filters) and [Handoff Config](/docs/configuration/handoff-config).
</Note>

***

## Common Patterns

### Pattern 1 — Callback on handoff

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, handoff

def log_handoff(from_agent, to_agent, context):
    print(f"Transfer: {from_agent.name} -> {to_agent.name}")

billing = Agent(name="Billing", instructions="Handle billing inquiries.")

triage = Agent(
    name="Triage",
    instructions="Route billing questions to the specialist.",
    handoffs=[handoff(billing, on_handoff=log_handoff)],
)
triage.start("Why was I charged twice?")
```

### Pattern 2 — Filter passed history

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, handoff
from praisonaiagents import handoff_filters

technical = Agent(name="TechSupport", instructions="Solve technical problems.")

triage = Agent(
    name="Triage",
    instructions="Route technical issues to support.",
    handoffs=[handoff(technical, input_filter=handoff_filters.remove_all_tools)],
)
triage.start("The app crashes when I upload a file.")
```

### Pattern 3 — Type-safe handoff

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.agent.handoff import TypedHandoff
from pydantic import BaseModel

class TaskData(BaseModel):
    priority: int
    description: str

specialist = Agent(name="Specialist", instructions="Resolve prioritised tasks.")

triage = Agent(
    name="Triage",
    instructions="Route tasks to the specialist with priority and description.",
    handoffs=[TypedHandoff(agent=specialist, input_schema=TaskData)],
)
triage.start("Escalate: priority 1, database is down.")
```

***

## Running handoffs in parallel

`parallel_handoffs` runs several handoffs at once with concurrency control.

<Steps>
  <Step title="List the targets">
    Pass `(agent, prompt)` tuples — each runs as its own task.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, parallel_handoffs

    billing = Agent(name="Billing", instructions="Handle billing inquiries.")
    refund = Agent(name="Refunds", instructions="Process refund requests.")
    tech = Agent(name="TechSupport", instructions="Solve technical problems.")
    triage = Agent(name="Triage", instructions="Route inquiries to specialists.")
    ```
  </Step>

  <Step title="Run them concurrently">
    Await `parallel_handoffs` and inspect each `HandoffResult`.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import asyncio

    async def main():
        results = await parallel_handoffs(
            triage,
            targets=[
                (billing, "Why was I charged twice?"),
                (refund, "I want a refund for order #42"),
                (tech, "The app crashes on upload"),
            ],
            max_concurrent=5,
        )
        for r in results:
            print(r.target_agent, r.success)

    asyncio.run(main())
    ```
  </Step>
</Steps>

<Note>
  Sibling tasks spawned by `parallel_handoffs` (or any `asyncio.gather` over `handoff_to_async`) each get an isolated handoff chain. Cycle detection and `max_depth` are enforced **per task**, not shared across siblings. See [Handoff chain isolation under `asyncio.gather`](/docs/docs/features/thread-safety#handoff-chain-isolation-under-asyncio-gather).
</Note>

<Note>
  A handoff rejected by a safety check (e.g. `allowed_agents` block, cycle guard, `max_depth`) does **not** consume a chain slot. The parent agent's cycle detection and `max_depth` counters remain accurate for any subsequent handoffs it attempts in the same turn.
</Note>

The same fan-out is available in TypeScript via `parallel_handoffs` (aliased `parallelHandoffs`), imported top-level from `praisonai`:

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, parallel_handoffs } from 'praisonai';

const billing = new Agent({ name: 'Billing', instructions: 'Handle billing inquiries.' });
const refund = new Agent({ name: 'Refunds', instructions: 'Process refund requests.' });
const tech = new Agent({ name: 'TechSupport', instructions: 'Solve technical problems.' });
const triage = new Agent({ name: 'Triage', instructions: 'Route inquiries to specialists.' });

const results = await parallel_handoffs(
  triage,
  [
    [billing, 'Why was I charged twice?'],
    [refund, 'I want a refund for order #42'],
    [tech, 'The app crashes on upload'],
  ],
  { maxConcurrent: 5 },
);

for (const r of results) {
  console.log(r.handedOffTo, r.success);
}
```

<Note>
  `HandoffTimeoutError` is always retryable in TypeScript too — `error.isRetryable === true` — because a timeout may succeed on a second attempt. Cycle and depth guards (`HandoffCycleError`, `HandoffDepthError`) carry `context.cycle_path` / `context.max_depth`. The handoff context accepts the back-compat aliases `chain`, `depth`, and `max_depth`. As of PR #4836, TypeScript honours `timeoutSeconds`, `maxDepth`, and `detectCycles` per handoff — the same knobs Python exposes. See [TypeScript Handoffs](/docs/docs/js/handoffs).
</Note>

***

## Per-Handoff concurrency

`max_concurrent` is enforced by a semaphore private to each `Handoff` instance, not shared across the process.

Two `Handoff`s with different `max_concurrent` values each get their own limit — the earlier one no longer clamps the later one.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, Handoff, HandoffConfig

triage = Agent(name="Triage")
billing = Agent(name="Billing")
refunds = Agent(name="Refunds")

to_billing = Handoff(billing, config=HandoffConfig(max_concurrent=10))
to_refunds = Handoff(refunds, config=HandoffConfig(max_concurrent=1))

triage.handoffs = [to_billing, to_refunds]
```

The billing lane runs up to 10 concurrent handoffs; the refunds lane serializes to one at a time. Each semaphore also rebinds automatically to the current event loop, so a process that calls `asyncio.run()` more than once keeps working.

The same per-instance limit works in TypeScript — pass `maxConcurrent` in each handoff's config:

```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { Agent, Handoff } from 'praisonai';

const triage = new Agent({ name: 'Triage' });
const billing = new Agent({ name: 'Billing' });
const refunds = new Agent({ name: 'Refunds' });

const toBilling = new Handoff({ agent: billing, config: { maxConcurrent: 10 } });
const toRefunds = new Handoff({ agent: refunds, config: { maxConcurrent: 1 } });

triage.handoffs = [toBilling, toRefunds];
```

<Warning>
  `RuntimeError: bound to a different event loop` used to appear when a process called `asyncio.run()` a second time — the shared semaphore stayed bound to the first loop. The per-instance semaphore now rebinds to the current loop automatically. If you still hit this on an older version, run `pip install -U praisonaiagents`.
</Warning>

***

## Handoff Results

`handoff_to()` returns a `HandoffResult` describing the outcome.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

billing = Agent(name="Billing", instructions="Handle billing issues.")
support = Agent(name="Support", instructions="Front-line support.", handoffs=[billing])

result = support.handoff_to(billing, "Handle this billing issue")

if result.outcome.status == "success":
    print(result.outcome.output)
elif result.outcome.status == "timeout":
    print(f"Timed out: {result.outcome.error}")
else:
    print(f"Failed: {result.outcome.error}")
```

| Field              | Type              | Description                                        |
| ------------------ | ----------------- | -------------------------------------------------- |
| `success`          | `bool`            | Whether the handoff completed successfully         |
| `response`         | `str`             | Response from the target agent                     |
| `target_agent`     | `str`             | Name of the agent that received the handoff        |
| `source_agent`     | `str`             | Name of the agent that initiated the handoff       |
| `duration_seconds` | `float`           | Time taken for the handoff                         |
| `error`            | `str`             | Error message if the handoff failed                |
| `outcome`          | `AgentRunOutcome` | Typed outcome with `.status` and `.is_retryable()` |

***

## Best Practices

<AccordionGroup>
  <Accordion title="Give each agent one clear responsibility">
    A specialist with a focused role routes cleanly. Overlapping responsibilities make the routing agent's tool choice ambiguous.
  </Accordion>

  <Accordion title="Filter history to cut tokens">
    Pass `input_filter=handoff_filters.remove_all_tools` or `handoff_filters.keep_last_n_messages(5)` so the target agent gets only the context it needs.
  </Accordion>

  <Accordion title="Use TypedHandoff for structured data">
    When a specialist needs typed fields, use `TypedHandoff(agent=..., input_schema=Model)` — the framework validates the payload at the boundary.
  </Accordion>

  <Accordion title="Keep a fallback path">
    Let the routing agent answer requests it cannot route rather than failing silently.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Handoff Tool Policy" icon="shield-check" href="/docs/features/handoff-tool-policy">
    Secure tool boundaries during handoff
  </Card>

  <Card title="Typed Handoffs" icon="filter" href="/docs/features/typed-handoffs">
    Schema-validated handoffs with Pydantic models
  </Card>
</CardGroup>
