Human Escalation
How the bot hands conversations to humans — the built-in transferConversation action, what actually happens on handoff, and custom escalation patterns like working-hour gates, department routing, and phone transfers.
Escalation is the moment your AI stops and a person takes over. In Octocom this is called a handoff, and it's a hard state change on the conversation — not a suggestion. Once a conversation is handed off, the bot will not answer the customer again in that conversation.
The short version: the bot calls the built-in transferConversation action, collects an email if required, writes a closing message, and the conversation is routed to your team.
The longer version is where the useful control lives — what a handoff actually triggers, how the reply channel is chosen, and how to replace the built-in behavior with your own logic for working hours, departments, or phone transfers.
The basics
transferConversation is a pre-built action. You don't create it — but it is not active by default. Like every action, it only exists for the AI if a workflow variant enables it. A bot with no workflow that enables transferConversation has no way to hand off on purpose.
A minimal escalation workflow:
- When to follow: "When a customer asks to talk to a human or a supervisor"
- Actions:
transferConversation - Instructions:
- Ask what the customer needs, so the team gets context
- Collect the customer's email address
- Call
transferConversation
The AI is not told which team to route to — it just signals "this needs a human." Routing is a separate, deterministic layer (see Routing to the right team).
What the action asks for
The arguments the AI must provide depend on the channel:
| Channel | Required arguments |
|---|---|
| Web chat, email, social, contact form | email |
phone | |
| Web chat embedded in another help desk | (none — handoff is immediate) |
| Phone calls | (none, or destination — see Phone transfers) |
Requiring an email is the default. It exists because most handoffs end up being answered by email, and a conversation with no reachable address is a dead end for your agents. A valid email is saved to the customer profile so agents can reply.
What happens on a handoff
The bot's final message is sent first, then the handoff fires. The AI is explicitly told: "After your message, the conversation will be transferred. You will not be able to respond to the customer after this message." So the customer always gets a closing message before the bot goes quiet.
What happens next depends on where your agents actually work. Two things happen either way:
- Conversation analysis runs — topics, sentiment, language, and data collection fields are computed from the transcript.
- The
Conversation Handed Offevent fires, running any event handlers you've written. This holds even if you work in an external help desk.
Everything else splits.
If you use an external help desk
Nothing on the Octocom side matters much. We follow your ticket routing configuration and create the ticket (or update an existing one) in the help desk you've chosen. From there, routing, assignment, and status are your help desk's job — Octocom doesn't assign an agent or set a reply channel.
If you use the Octocom help desk
The conversation becomes a live item in your inbox:
- It's marked handed off. If it was closed, it's reopened. Any bot messages still scheduled for later are canceled.
- A timeline event is written with the handoff reason, so your team can see why it happened.
- The reply channel is set — where your team's answer will go. Social conversations stay on their channel; most others resolve to email.
- Auto-close is canceled — a handed-off conversation won't be closed out from under your team.
- Auto-assignment runs — assignment rules pick a team or individual, then round-robin or balanced assignment picks the agent.
Two consequences are worth calling out:
- Analysis runs before assignment. Assignment rules can therefore condition on topics, sentiment, language, and data-collection values that didn't exist a second earlier.
- Anything your action does before handing off is visible to routing. Tags and metadata written before the handoff are already on the conversation when assignment rules evaluate. This is the mechanism behind department routing.
After the handoff
The bot stops answering, but it isn't completely silent:
- Web chat: if the customer keeps typing before an agent arrives, an auto-reply bot acknowledges the message and repeats when the team will respond. It doesn't try to solve anything, and it stops once an agent has replied.
- Email: an auto-responder can send one fixed acknowledgement, optionally closing the conversation.
- Other channels: nothing is sent.
Handoffs you didn't ask for
Not every handoff comes from transferConversation. These fire on their own:
| Trigger | What happens |
|---|---|
| Follow-ups exhausted | With Hand off when follow-ups are exhausted on, an unresolved conversation is handed off once the follow-up budget runs out. See Follow-Ups & Auto-Resolve. |
| Response generation failed | If the bot fails to produce a response three times, it stops retrying and hands off rather than leaving the customer hanging. |
| Agent takeover | An agent replying or taking over from the dashboard hands the conversation off. |
Writing escalation instructions
How hard the bot should resist escalation is a business decision. Three postures cover most setups:
Hand off on request. The bot asks what the customer needs and for their email, then transfers.
If the customer wishes to talk to a human or a supervisor, you must first know the inquiry that they want to be passed on to the team. Make sure the customer provides more information about their inquiry. Don't transfer the conversation if it's not clear what the customer wants to do.
You must also make sure they provide their email. You cannot transfer the conversation without an email.
Once the customer responds with their inquiry details and email, call the
transferConversationaction. Inform the customer that the conversation has been transferred. If you know when the agent will respond, be sure to mention it. End with a polite goodbye message.
Hand off without questions. Fastest path to a human, lowest friction, highest handoff rate.
If the customer wishes to talk to a human or a supervisor, call the
transferConversationaction to transfer the conversation to the relevant team. You must make sure they provide their email first. If the customer already provided their email, transfer immediately. Otherwise, ask them for it.
Resist once, then hand off. The bot attempts the inquiry before escalating. Lowest handoff rate, and the one most likely to annoy a customer who genuinely wants a person.
If the customer asks to talk to a human or supervisor, politely resist and explain that you've been trained to assist with all common inquiries. Ask the customer to provide the details of their inquiry. Explain that you'll be happy to transfer the conversation to a human if you're unable to assist.
If the customer doesn't want to provide details, resist transferring and politely explain that you've been trained to assist with common inquiries.
Only transfer if the customer provided their inquiry details and you're unable to assist them.
Tips that apply to all three:
- Escalate as a fail-safe everywhere else too. In every workflow, end with a step like "If blocked or facing a policy exception, call
transferConversation." Most handoffs shouldn't come from a dedicated escalation workflow — they should come from other workflows hitting a wall. - Don't promise a transfer you can't make. If the bot says "let me transfer you" but doesn't call the action, the customer is left waiting on nothing. A built-in guardrail catches this: when the bot's message promises a transfer but no
transferConversationcall was made, the response is discarded and regenerated. It's available on request if your bot is prone to it. - Don't script the closing line. The action already tells the bot the channel, the response time, and to end with a goodbye. Adding your own wording on top usually produces a worse, doubled-up message.
- Say what to collect, not how to route. Routing is deterministic and happens after the AI is done.
Routing to the right team
transferConversation is deliberately routing-agnostic — it signals "a human is needed," not "send this to Billing." Routing is handled by assignment rules, which run right after the handoff and match on conversation state:
business · channel · priority · tags · email inbox · topics · language · sentiment · data collection values
Each rule has conditions (all must match) and a target — a team or a specific person. Rules are evaluated by priority; the first full match wins. A rule targeting a person assigns them directly; a rule targeting a team narrows the pool, then your round-robin or balanced policy picks the agent.
So the pattern for "different departments get different tickets" is:
- A custom Python action tags the conversation
- The same action hands off
- An assignment rule matches that tag and routes to the right team
Because tags are written before the handoff, they're already in place when the rules evaluate.
def execute_action(context):
args = context["args"]
department = args["department"] # "billing" | "technical" | "sales"
add_conversation_tag(context, f"dept:{department}")
set_conversation_metadata(context, "escalation_reason", args["reason"])
hand_off_conversation(context, reason=args["reason"][:500])
return {
"success": True,
"message": "Tell the customer the right team will follow up shortly.",
}Give the action an argument description that constrains the AI to your exact tag values — the AI picks the department, but only from a list you control:
The
departmentargument MUST be one of these exact values: "billing", "technical", "sales". If you don't know which one the customer needs, ask before calling this action.
Tags aren't the only lever. Topics, language, and sentiment are computed automatically during the handoff, so language is "de" → German team, or sentiment is "angry" → senior team, work without writing any action at all.
Custom escalation actions
Once escalation needs a decision — "are we open?", "is anyone actually there?", "which number do I dial?" — instructions aren't the right place for it. Move it into a custom Python action that calls hand_off_conversation.
The important property of this design: the action decides whether the handoff happens at all. If it returns without calling hand_off_conversation, nothing is handed off, and the return value becomes the AI's instruction for what to tell the customer instead.
Three shapes cover almost everything.
Gate on working hours
The most common custom escalation. tryEscalateToHuman — the name signals to the AI that it might not succeed — checks the clock and public holidays before handing off, and returns a ready-made explanation when it doesn't.
from datetime import datetime
from zoneinfo import ZoneInfo
def execute_action(context):
now = datetime.now(ZoneInfo("Europe/Berlin"))
today = is_holiday("DE", check_date=now.strftime("%Y-%m-%d"))
open_now = (
now.weekday() < 5
and 9 <= now.hour < 18
and not today.get("isHoliday")
)
if not open_now:
return {
"success": False,
"reason": "outside_working_hours",
"message_to_customer": (
"Our team is available Monday to Friday, 9:00–18:00 CET. "
"Leave your question here and we'll reply on the next working day."
),
}
hand_off_conversation(context, reason="Customer requested a human agent")
return {"success": True, "message_to_customer": "Connecting you to an agent now."}Describe the closed-hours behavior in the action description too, so the AI knows what a failure means and doesn't retry in a loop.
Two habits are worth copying here. Be explicit with the AI about what did not happen — if the return value doesn't say the handoff was refused, the bot will cheerfully tell the customer they're being connected to someone. And fail open: if your own gating logic errors, hand off anyway. A transient failure should never be the thing that blocks a customer from reaching a person.
A useful refinement: hours that differ by customer type. Take an argument like is_b2b, store it as metadata (it's then available to assignment rules), and apply a different schedule to each.
Phone transfers
On a phone call, transferConversation still exists, but it's backed by a phone-specific implementation that shadows the standard one at runtime. Which behavior you get depends on whether transfer endpoints are configured for the phone number:
| Endpoints configured | What transferConversation does |
|---|---|
| One | Forwards the live call to it. No arguments — the AI just calls the action. |
| Several | Forwards the live call. The AI must pass destination, restricted to the endpoint names you configured, so it can't invent one. |
| None | The call is not forwarded. It's a normal handoff — the customer stays with the bot until the call ends and an agent follows up separately, framed as "someone will be in touch." |
Each endpoint is a name the AI sees plus the number the call is forwarded to. For a fixed menu of departments this is all you need, and it needs no code.
Reach for a custom action when the destination depends on logic rather than a fixed list — routing by opening hours, by which agent owns the account, or by something an API tells you. Pass phone_destination to hand_off_conversation and the live call is forwarded to that number:
DESTINATIONS = {
"Sales": "+15551230001",
"Support": "+15551230002",
"Accounting": "+15551230003",
}
def execute_action(context):
destination = context["args"].get("destination")
if destination not in DESTINATIONS:
valid = ", ".join(f'"{d}"' for d in DESTINATIONS)
return {
"success": False,
"message": f"Invalid destination. Valid values are: {valid}. Try again.",
}
hand_off_conversation(context, phone_destination=DESTINATIONS[destination])
return {"success": True}Two things make this work well in practice:
- Put the menu in the action description, listing every valid destination verbatim, and tell the AI to ask the customer if it isn't sure. Rejecting an invalid value with the list in the error message lets the AI recover on the next turn instead of guessing. (With configured endpoints you get this for free — the argument is already restricted to your endpoint names.)
phone_destinationis ignored outside phone calls, so the same action can back both a voice bot and a chat bot — on chat it degrades to a normal handoff. Pair it with a working-hours gate so out-of-hours calls become a regular handoff (a callback queued for the next day) rather than ringing an empty office.
Overriding the built-in action
If a custom Python action is named transferConversation, it replaces the built-in one. Precedence is: Python actions → API actions → built-in.
Override when you want the built-in's role but different mechanics — extra arguments, your own validation, tags applied on the way out — without retraining every workflow that already references transferConversation by name.
def execute_action(context):
email = context["args"].get("email")
reason = context["args"].get("reason")
if not email:
return {"success": False, "message": "Email is required to transfer the conversation."}
upsert_customer(context, email=email)
hand_off_conversation(context, reason=reason)
return "The team will reach out by email as soon as possible."Be aware of what you give up. The built-in does real work that your override will not inherit:
- Channel-aware argument requirements (phone on WhatsApp, no arguments on embedded web chat)
- Reply-channel resolution
- The response-time expectation and closing-message guidance passed back to the AI
Handing the AI a useful instruction in your return value covers most of the last point. If you only need to add behavior — a tag, a metadata field, a Slack ping — prefer a separate action with its own name, or an event handler on Conversation Handed Off, and leave the built-in intact.
Choosing an approach
| You need | Use |
|---|---|
| A human when the customer asks | transferConversation in an escalation workflow |
| A handoff when a workflow hits a wall | A transferConversation fail-safe step in that workflow |
| Different teams for different issues | Tag in a custom action, then an assignment rule |
| Escalation only during opening hours | A custom tryEscalateToHuman action |
| A live call routed to a fixed set of departments | Transfer endpoints on the phone number |
| A live call routed by your own logic | hand_off_conversation(phone_destination=...) |
| Handoff triggered by what an API returned | hand_off_conversation inside your existing action |
| Something to happen after every handoff | An event handler on Conversation Handed Off |
FAQ
Why isn't the bot handing off? In order of likelihood: no workflow enables transferConversation; the workflow isn't being selected for that kind of message; the customer never gave an email and email collection is on; or transferConversation is disabled for the bot. The conversation timeline and the tool-call log show which of these it is.
The bot said it would transfer, but nothing happened. It promised in prose without calling the action. Make the action call an explicit numbered step in your instructions, and ask us to enable the guardrail that catches this and regenerates the response.
Can I hand off without collecting an email? Yes, it's a setting — contact us. Consider what your agents will do with an unreachable conversation first. Note that web chat embedded in another help desk already skips the email requirement.
Does a handoff close the conversation? No — the opposite. On the Octocom help desk a closed conversation is reopened, and auto-close is canceled so it stays open until a human deals with it.
Can a handed-off conversation go back to the bot? Not automatically.
How do I see why a conversation was handed off? The reason is recorded on the conversation timeline. Handoff Topics shows which subjects drive escalations over time.