Reply, book and hand off when the AI goes off script
The agent answers and books alone; the handoff to a person is what matters.
A published AI agent answers, books an appointment and, when it should, hands the conversation to a person.
The handoff has an extra safety layer. A second model reviews every reply the agent sends. If the agent told the customer a person would follow up but did not call the handoff tool, the platform detects it and performs the handoff itself. Step 3 shows what that looks like.
Before you start
- A published agent.
ai_agents:writeto configure it; see Configure the agent that talks. - A connected channel, with the agent assigned from the app.
- A saved scheduling policy, if the goal is letting the agent book on its own (step 1).
conversations:write, to hand off or return a conversation by hand.
1. Check whether the agent can book on its own
Booking a slot over the API works with nothing configured. With no saved policy, the calendar falls back to the workspace's general business hours. That is not the same as letting the agent book on its own: that decision has its own switch, read-only from the public API.
curl https://api.vitrinadev.com/api/v1/appointments/config \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"configured": true,
"timezone": "America/Santiago",
"business_hours": {
"mon": [["09:00", "13:00"], ["14:30", "18:00"]],
"tue": [["09:00", "13:00"], ["14:30", "18:00"]]
}
}
}configured decides whether the booking tool is offered to the model. While it reads false, a person or your own server can book through Calendar and appointments, but the agent cannot. Saving the policy is a PUT /appointments/config with at least one field, documented in that same recipe.
2. Let it answer, and let it ask for help when it should
The agent calls its handoff tool when it cannot resolve something: a decision reserved for a person, a fact it does not have, a customer asking to speak to someone. The same operation is published for manual use too, for example to take over a conversation the agent is still handling:
curl -X POST https://api.vitrinadev.com/api/v1/conversations/01a0ce15-c122-7148-98b3-4dfefe93756e/handoff \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Asks for a cash-payment discount, a decision I cannot make" }'{
"data": {
"id": "01a0ce15-c122-7148-98b3-4dfefe93756e",
"handler": "human",
"ticket_id": null,
"assignee_user_id": null,
"awaiting_human_since": "2026-09-23T11:45:44.101Z"
}
}handoff works differently from assign. You do not choose who keeps the conversation here: the workspace's assignment rules decide (documented in Conversations and tickets). In this capture, no rule matched. The conversation lands in the human queue with no owner (assignee_user_id reads null), but it is already out of the AI's hands. reason is required: it is the first thing whoever picks up the thread reads.
The same endpoint that answers "why is the AI silent" confirms the effect:
curl https://api.vitrinadev.com/api/v1/conversations/01a0ce15-c122-7148-98b3-4dfefe93756e/ai-status \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"silence_reason": "handler_is_human",
"explanation": "A teammate replied, so this conversation is handled by a human. The AI stays silent until it is explicitly returned to the AI."
}
}POST /conversations/{id}/return-to-ai undoes the handoff when it should.
3. Read what gets written when the system performs the handoff
Every handoff leaves an event message on the same thread you are already reading. It does not matter who asked for it: you by hand, the agent through its tool, or the automatic review described at the top.
curl "https://api.vitrinadev.com/api/v1/conversations/01a0ce15-c122-7148-98b3-4dfefe93756e/messages?limit=2" \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": [
{
"id": "61ea125e-e3cb-4468-8f68-0db340a07d28",
"sender_type": "system",
"content": "An advisor took over the conversation · Reason: Asks for a cash-payment discount, a decision I cannot make",
"metadata": { "reason": "Asks for a cash-payment discount, a decision I cannot make", "source": "operator", "event_kind": "handoff" }
}
]
}That is the shape of a handoff requested by hand, with source: "operator". When the automatic review applies it instead, the same mechanism leaves a message with a fixed text, always the same, meant to be recognised without guessing:
Handoff applied automatically: the AI told the customer an advisor would
reach out, but never completed the handoff. The system did it in her place.That exact sentence, in a thread, means the platform performed the handoff. GET /conversations/{id}/messages shows it exactly like any other message, with the same sender_type: "system".
4. Understand why the agent does not wake up on its own at window start
A channel can carry an availability window for the AI: always, outside business hours, or custom. That is configured in the app, not through the public API. The AI only answers an inbound message. Nothing is sent when the window opens, and the inbox is not scanned for unanswered threads. To reach a customer who has not written first, use an outbound template.
In the app: the per-channel agent, the availability window and the handoff destination are configured from Settings → AI agents.
When it fails
A handoff on a conversation that is already human works the same way. The call is idempotent over the state: repeating it twice in the same turn does not create two different owners. It is worth watching for a ticket_id that comes back null after a handoff, as in step 2. That means no assignment rule matched. The thread waits in the general queue until someone claims it by hand, with POST /conversations/{id}/claim.
If the agent seems to never answer, and the cause is not a handoff, Find out why the AI went quiet walks through the fifteen possible reasons.