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.

Pattern

≈ 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 their role. Set kind: "agent" for another agent.
  • Give the reason in one plain line. A handoff is an escalation, not an error.
  • Keep context to the facts the receiver needs, from real data. List done in the past tense and left as next steps.
  • Write note as the message the receiver will read, in the first person.
  • Use keepLabel to 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 returned note exactly. On keep, keep the task and do what keepLabel promised.

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") and confirmLabel (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

app/api/chat/route.ts
import { tool } from "ai"
import { handoffTool } from "@/components/genui/handoff.schema"

const tools = {
  [handoffTool.name]: tool({
    description: handoffTool.description,
    inputSchema: handoffTool.inputSchema,
  }),
}
components/message.tsx
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

PropTypeNotes
fromstring?Who is handing off. Default "the agent"
to{ name, role?, kind }The receiver. kind is person (default) or agent. Data-bound
titlestringWhat is handed off
reasonstringWhy, in one line. Up to 140 characters
context{ label, value }[]?Up to 6. Values are data-bound
done, leftstring[]?Up to 6 each. done is data-bound
notestring?The editable note for the receiver. Up to 600 characters
confirmLabel, keepLabelstring?Button labels

Host props: status (streaming, ready, submitting, confirmed, kept), onResult, result and className.

Source

On this page