Draft — awaiting review. This article is written from the source repository and has not yet been reviewed and approved by Sajid Islam. It is served with noindex so search engines do not list it, and it is deliberately absent from sitemap.xml and llms.txt. Everything below describes code that exists in the linked repository; anything not yet built is labelled proposed.

Designing Reliable Human Handoff for Customer-Service AI Agents

Draft written Est. 7 min read

The short answer: handoff is a piece of persistent state, not a button. In the WhatsApp Commerce Copilot, whether the AI or a human owns a conversation is a column on the conversation row — is_ai_controlled — and every inbound message reads it before anything else happens. Because the flag lives in the database rather than in process memory, a restart, a second worker, or two messages arriving at once cannot resurrect a bot that a shop owner has just switched off.

The problem: the bot is confidently unhelpful and a person is watching

Every customer-service agent eventually meets a conversation it should not be handling — an angry customer, a bespoke request, a question the catalogue cannot answer. The failure that matters is not the agent being wrong. It is the agent being wrong while a human is trying to take over, so the customer receives two contradictory replies and stops trusting either.

This is why "add a handoff button" is the easy half of the problem. The hard half is making the transfer atomic and durable.

The approach: control is a column, not a runtime variable

The copilot models the conversation itself as the unit of control. The conversations table carries is_ai_controlled alongside the conversation's status, its current product and variant, and a serialised context blob. Ownership is therefore a fact about the conversation that any process can read, not a value held in whichever worker happened to handle the last message.

That single decision buys several properties for free:

In the dashboard, this surfaces as Human Mode: a one-click toggle that disables the AI so the owner can reply personally, and one click back when they are done.

Recording why, not just that

A separate human_handoffs table records each transfer with its conversation_id, store_id, a reason, a summary and a status. Two things follow from keeping this separate from the boolean.

First, the reason is preserved. A toggle tells you the AI is off; a handoff row tells you it went off because confidence was low on a returns question — which is the input you need to decide what to fix.

Second, the summary means the human does not start cold. They inherit the conversation with context attached rather than scrolling to reconstruct it.

Handoff is triggered from two directions: the owner takes control from the dashboard, or the system escalates when confidence is low. The escalation path is the interesting one, because it means the agent has to know what it does not know.

Tradeoffs

Limitations

What I would take to the next system

Put the control flag where the data is. Almost every subtle bug in an AI agent that shares work with humans comes from ownership being tracked in one place and acted on in another — cached in a worker, held in a session, assumed from the last event. Making it a column turned a class of race conditions into a non-issue, and it cost one migration.

Related reading

All writing Projects