Guides
Handoff to a person
A customer-facing agent that cannot resolve a request has to pass it to the business's staff. Tether records the handoff, tells the customer how they will be contacted in your words, and tells your system which conversation needs a person. A handoff is not an action: it stages nothing, needs no approval and changes nothing in any system.
What triggers one
| Trigger | When | Model call |
|---|---|---|
customer_request | The customer asks for a person: "მინდა ადამიანთან საუბარი", "minda adamiantan saubari", "Хочу поговорить с человеком", "I want to talk to a person". Recognised in Georgian script, transliterated Georgian, Russian and English, on intent phrasing only, before anything else in the turn. | None. The turn is answered at once. |
agent | The model concludes it cannot help and calls tether_handoff, a capability Tether offers beside your tools on every chat turn that offers tools. | The turn's own. |
unverified_outcome | A write was executed, but its verify read did not show the effect, failed, or did not answer in time. Raised when the action is confirmed, outside any turn, so a person checks what actually happened. | None. |
Whichever fires first is the one recorded. tether_handoff is reserved: a catalog that names a tool
tether_handoff is refused at registration, so nothing you register can take its place.
What the customer sees
The chat response carries handoff beside the blocks:
{
"blocks": [
{ "type": "text", "id": "handoff_1", "variant": "body",
"markdown": "A member of the venue's staff will reply here or call you." }
],
"session_id": "0f2c8b41-6d3e-4a19-9c77-1b2a3e4f5d60",
"pending_actions": [],
"handoff": {
"trigger": "customer_request",
"message": "A member of the venue's staff will reply here or call you.",
"language": "en"
}
}
message is the handoffMessage from your domain pack. Without one it is
a neutral default in the language the request was recognised in (ka, ka-Latn, ru or en);
for an agent handoff that is the language Tether read in the prompt, Georgian typed in Latin
letters included, and English for any other. The default follows the turn's
reply language, so a customer who asked for English, or a
pack that answers everyone in one language, gets it in that language. The default promises no time
or channel, because Tether cannot know them. Set your own:
curl -X PUT $TETHER_URL/v1/domain-pack \
-H "X-Api-Key: $TETHER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "handoffMessage": "ჩვენი თანამშრომელი დაგიკავშირდებათ. Our staff will contact you." }'
On a customer_request handoff the message is also the answer's only text block, so a renderer
that does not know handoff still shows it. The widget shows it once, set
apart from the answer, and raises tether-handoff on the element so your page can open its own
contact channel. On the stream, handoff is inside the complete frame's
raw document.
What your system receives
A signed conversation.handoff event, posted to your tool endpoint with the
same signature, timestamp and idempotency headers as a tool call:
{
"event": "conversation.handoff",
"session_id": "0f2c8b41-6d3e-4a19-9c77-1b2a3e4f5d60",
"trigger": "agent",
"language": "ka",
"tenant": { "organization_id": 42, "scope_id": null, "user_id": 0 },
"customer": { "id": "G1" },
"request_id": "6ad75e9b73bd464ba811bf88e449495a",
"reason": "The guest wants a refund to their card, which no capability offers."
}
session_id is the conversation to pick up. customer is the
verified customer, or null when the turn had none; it is never
taken from the conversation. reason is the model's one sentence on an agent handoff, and like
any model output it is a hint, not an instruction. There is no action_id, no tool and no
arguments: acknowledge it with a 2xx and run nothing. The SDKs hand it to onEvent with the
action events.
On an unverified_outcome handoff, session_id is the conversation that staged the write, or
null when it was staged directly with POST /v1/actions; language is null, because the
customer was shown no message; customer is the action's customer; and reason names the action
and what the read found. The action.unverified event for the same action arrives beside it,
with its action_id.
tether.createHandler({
secret: process.env.TETHER_SECRET,
tools,
onEvent: async (event) => {
if (event.event === 'conversation.handoff') await openTicket(event.session_id, event.customer)
},
})
What is recorded
The turn's telemetry records the trigger and the language (meta.explain.handoff with
X-Tether-Explain: 1), and the conversation history stores the answer with its handoff. A
customer_request turn records result_path: "handoff" and model none. An unverified_outcome
handoff is recorded as the action's unverified event in the audit, and is on
the confirm response and GET /v1/actions as verification.handoff.
Related
- Chat · Domain pack
- The request envelope — every event your endpoint receives
- The widget