Most first attempts at handoff wrap the agent call in a try/except and send the user to a human when something throws. That produces a handoff record only in the failure case, so you cannot count escalations you never saw, cannot distinguish a frustrated user from a broken tool, and cannot route anything because you did not capture why the escalation happened.
A better design makes escalation a first-class tool in the agent's toolset, alongside search, lookup, and update. The agent calls it deliberately, the runtime validates the arguments against a schema, and every call writes a structured row. The short answer format is a function with a fixed signature:
{
"name": "escalate",
"description": "Transfer the conversation to a human agent. Use when the user asks for a person, when you cannot resolve the request in two attempts, or when the topic is on the restricted list.",
"parameters": {
"type": "object",
"properties": {
"reason": {"enum": ["user_request", "low_confidence", "repeated_failure", "policy_risk"]},
"priority": {"enum": ["p1", "p2", "p3"]},
"summary": {"type": "string", "maxLength": 1200},
"topic": {"type": "string"},
"blocking_facts": {"type": "array", "items": {"type": "string"}}
},
"required": ["reason", "priority", "summary", "topic"]
}
}
The reason enum is the most valuable field in the whole schema: it turns every escalation into a category you can count and chart. Capping summary forces the model to compress what matters for the human instead of dumping the transcript, and blocking_facts (account ID, order number, error code) lets the assignee start working without a discovery call.
The runtime, not the model, owns the side effects: it persists the record, runs the routing function, starts the queue timer, and emits the telemetry. Keep the tool idempotent on a conversation ID so a model that calls it twice in one turn does not open two tickets.



