Handoff
The agent hands a task to a person or another agent, with the reason, the context it passes on, what's done and what's left, and a note the user can edit.
≈ 1,283 tokens per use (tool 726 · guidance 329 · typical payload 228)How token costs are estimated
Example: Handoff
Can you finish the refund for Acme?
Acme Co. refund request
The $3,900 refund is above my $2,500 approval cap.
From Alpha to Dana RuizBilling lead
- Customer
- Acme Co. (acme.example)
- Invoice
- INV-20417
- Refund
- $3,900.00
- Cause
- Duplicate annual charge
Done
- Confirmed the duplicate charge
- Checked there are no open disputes
- Drafted the reply to Acme
Left
- Approve the $3,900 refund
- Send the drafted reply
Note to Dana
Hi Dana, Acme was charged twice for the annual plan on Oct 3. I've verified the duplicate and drafted the reply; it only needs your approval on the refund.
When to use
- The agent has reached the limit of what it may or can do on a task (an approval cap, a missing permission, a judgement call) and a specific person or agent should take it from here.
- The user asks the agent to pass work to a teammate or another agent, and the receiver needs context to pick it up.
When not to use
- The agent can finish once the user approves one action. Use the Approval Request.
- Something failed and the user needs a way forward. Use the Recovery Panel.
- The agent only needs an answer from the user. Ask, or use the Generated Form.
- The agent doesn't know who should take it. Ask first; the receiver is data, not a guess.
How the agent should use it
- Name the real receiver in
to, from a tool result or the user, with theirrole. Setkind: "agent"for another agent. - Give the
reasonin one plain line. A handoff is an escalation, not an error. - Keep
contextto the facts the receiver needs, from real data. Listdonein the past tense andleftas next steps. - Write
noteas the message the receiver will read, in the first person. - Use
keepLabelto say what keeping it means, e.g. "Raise my cap and let me finish". - Nothing is sent until the result arrives. On
confirm, hand off with the returnednoteexactly. Onkeep, keep the task and do whatkeepLabelpromised.
Interaction
- The receiver sits in a filled tile at the top (initials, or a bot mark for an agent), followed by the context rows, then Done and Left side by side (stacked on narrow screens).
- The note shows as a filled tile with an Edit button (Add a note when there is none). Editing swaps in a text area with a character count; Done or Escape closes it, and the label shows "edited".
- The footer has two pill buttons:
keepLabel(default "Keep it here") andconfirmLabel(default "Hand off to" plus the receiver's first name). - After confirm, the footer records "Handed to Dana Ruiz." and the note that was sent stays visible. After keep, it records "Kept with Alpha." and the note is hidden.
- While streaming, sections appear as they arrive and the buttons are disabled.
Accessibility
- The card is a region labelled by its title. The receiver is read as "From Alpha to Dana Ruiz, Billing lead".
- Context is a description list. Done and Left are labelled lists; their icons are decorative.
- The text area is labelled by the note heading, takes focus when editing starts, and gives focus back to Edit when it closes.
- Buttons are at least 44px tall with visible focus rings. The record line is in a polite live region.
Installation
npx shadcn@latest add https://your-site.com/r/handoff.json
Usage
import { tool } from "ai"
import { handoffTool } from "@/components/genui/handoff.schema"
const tools = {
[handoffTool.name]: tool({
description: handoffTool.description,
inputSchema: handoffTool.inputSchema,
}),
}import { Handoff } from "@/components/genui/handoff"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-hand_off") return null
return (
<Handoff
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "hand_off",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The result tells the agent what the user decided. note is the text to send (absent when empty); edited is set when the user changed it:
{ action: "confirm", note: "Hi Dana, Acme was charged twice…", edited: true }
{ action: "keep" }Schema
| Prop | Type | Notes |
|---|---|---|
from | string? | Who is handing off. Default "the agent" |
to | { name, role?, kind } | The receiver. kind is person (default) or agent. Data-bound |
title | string | What is handed off |
reason | string | Why, in one line. Up to 140 characters |
context | { label, value }[]? | Up to 6. Values are data-bound |
done, left | string[]? | Up to 6 each. done is data-bound |
note | string? | The editable note for the receiver. Up to 600 characters |
confirmLabel, keepLabel | string? | Button labels |
Host props: status (streaming, ready, submitting, confirmed, kept), onResult, result and className.