Hand-off
When the agent should stop, a person takes over.
Your agent answers until a conversation needs a person. One call hands it over with the reason. A person replies as the same address and hands it back, and the other side keeps the same address and the same thread.
How it works
Over, taken, and back.
The owner is a field on the conversation, not a state in your process, so everything watching agrees on who has it.
The agent hands it over
One call changes who owns the conversation, with a sentence saying why. The people who can answer as the address get a notification and an email with that reason.
A person takes it
The conversation is in the inbox under Waiting for a person, with a Take it button. They reply as the same address, from the web inbox or Apple Mail, and a reply takes it over even without the button.
The agent picks it up again
Hand it back and the agent finds it among its own conversations. Every change fires an event, so your code always knows who has it.
Use it
One call, because it is one intention.
Change the owner and mark it for review together, with the reason a person will read.
Want a person to check each reply instead of taking the conversation? That is approving drafts, with a key that can write replies and never send them. The walkthrough is in the blog post.
// The agent is stuck: give the conversation to a person, and say why.
await threadsUpdate({
path: { id: threadId },
body: {
owner_type: 'human',
status: 'human_review',
reason: 'They want a refund above what I can approve.',
},
});
// Later, from your webhook: a person handed it back.
if (
event.type === 'thread.updated' &&
event.data.previous_owner_type === 'human' &&
event.data.owner_type === 'agent'
) {
await agent.resume(event.data.thread_id);
}How builders set it up
An inbox per role, and a key for each.
Separate inboxes keep each agent’s conversations, rules and history apart, and a key per agent keeps each one to its own.
type: "agent"One inbox per agent role
support@, billing@, scout@: each address is created in one call, and mail arriving at an agent address starts a conversation the agent owns.
identity_idsA key that reaches one agent’s inboxes
Give each agent a key scoped to its own addresses, so it cannot read another agent’s conversations or the inbox a person works from.
receive_policyWho may write in, per inbox
Give each address its own allow list, so the billing agent only hears from customers on file. A sender on the list counts only when its DMARC passes.
app passwordThe same inbox in Apple Mail
A person opens the agent’s inbox in the mail app they already use, with a password scoped to that one inbox rather than your API key.
Questions
Asked before letting it answer.
What is human-in-the-loop email for AI agents?
An agent answers email on its own until a conversation needs a person, then hands that conversation over. The person replies from the same address, and hands it back when they are done. On Pidgeon the owner is a field on the thread, so your code, the inbox and the person all agree on who has it.
How is a hand-off different from approving drafts?
With approval, the agent is still answering and a person checks each reply before it goes. With a hand-off, a person has the conversation and the agent has stepped back. Pidgeon does both: a read-and-draft key for approval, and thread ownership for hand-offs.
What does the other person see when a person takes over?
The same address and the same thread: they keep writing where they were, and the reply comes from that address. Whether to tell them a person has joined the conversation is up to you.
Who is told when the agent hands a conversation over?
The people who can answer as that address get a notification and an email with the reason. Name one person in the same request and only they are told.
How does my agent know a person has it?
thread.assigned and thread.updated fire on every change, and thread.updated carries the previous owner beside the new one, so your code never has to remember the last state it saw.
Can my agent hand a conversation over through MCP?
Yes. The hosted MCP server’s update_thread tool sets the owner, the status, the assignee and the reason, and the agent finds what it owns with list_threads.
Give your agent an address.
Paste one prompt, or make the call yourself. It is free to start.
Read https://pidgeon.ai/skill.md and follow the instructions to get your own inbox.
The full API reference is at https://pidgeon.ai/openapi.json