- Human handoff pauses the bot in one chat while a person (an agent, a CRM inbox) takes over.
- Blacklist / whitelist drops messages from given senders or chats before the bot even stores them.
Bot (and on pluginCtx.bot inside plugins).
Human handoff
ctx.from): a handoff in a group pauses the bot
for every member of that group, but not their DMs.
What happens while a chat is handed off
- Messages are persisted,
before/after('message')hooks run (abeforehook can stillabort()), and the'message'event fires, so an inbox integration sees everything. - Commands, conversations, dormant checkpoints, matchers and the catch-all
do not run, and
'command:unknown'is not emitted. Interactions (button and list taps) are blocked too. - Calls into the chat are blocked completely: no
before/after('call')hooks, no'call'event and noCALLtrigger (counted inmetrics().dropped.handoff). - Escape commands (
handoff.allowCommands): with or without the prefix, compared like commands (case and accents ignored). They only pass when abot.command()with that name is registered; otherwise they stay blocked and never reach the catch-all. Make that command callbot.resume(...)rather than start a flow. bot.onIdletimers are cancelled and suspended for the chat until it is resumed.
When the handoff starts
- Every active conversation in the chat is cancelled silently (no
cancelledMessage): all participants, whatever theconversationScope. Its dormant checkpoints are deleted from memory and from the store. - If
ctx.handoff()is called inside a flow.do(), the flow ends: later steps do not run. In flow stepsctxis typed asRuntimeMessage, so usebot.handoff(ctx.from)there.
Return values
handoff()/resume()returnPromise<boolean>:truewhen the state changed. They are idempotent: a second handoff neither changes the reason nor emits an event.- The state changes synchronously, so you do not need to
awaitit before the next line. - They never reject because of store errors; those go to the
'error'event. They throw aTypeErrorsynchronously on an empty chat id. ctx.handoff()resolvesfalsewhen the context was not built by a bot.- An agent’s
bot.send()goes through thebefore/after('send')hooks and the outbound ledger like any other send. It does not resume the bot.
Persistence
- The state is stored in
StoredChat.handoff = { since, reason?, meta? }(set tonullon resume), plus a small index of handed-off chat ids kept under one store cursor key. bot.start()restores it with one read per handed-off chat (stores without cursors fall back tolistChats()). No'handoff'event fires on restore. It also drops the dormant checkpoints of every handed-off chat, so an old flow never resumes afterbot.resume().- A failing restore is reported on
'error'and never failsstart(). Aresume()called while the restore is still running waits for it. - It survives restarts with a persistent store (SQLite, Redis, Postgres,
MySQL, Mongo, or
MemoryStorewithfile).handoff: { persist: false }keeps it in memory only.metamust be JSON-serializable. bot.deleteSubject(userId)forgets the handoff of that user’s chat without events.
Blacklist and whitelist
Access rules
- Both lists are checked against the sender id and the chat id, with exact equality: the core normalizes nothing. On WhatsApp (Baileys) use the full JID, not the bare phone number.
- The blacklist wins. An enabled but empty whitelist blocks everyone. The whitelist is disabled by default.
- A whitelisted user passes in any group; a whitelisted group lets all its members through.
What happens to a blocked message
It is dropped before persistence: the message, chat and contact are not stored, no hooks, commands, conversations or dormant resumes run (the checkpoint stays as it is), and'message' is not emitted. The drop is
debug-logged with { gate, chatId, senderId }, never the body. It applies
to messages, interactions and calls (a blocked call runs no 'call' hooks,
emits no 'call' event and reaches no CALL trigger); reactions, poll
votes, presence and group events are not filtered. Blocked messages and
calls count in metrics().dropped.blocked.
Blocking someone does not cancel a conversation already in progress
(their replies just stop arriving). Use bot.abortConversation(...) if you
need that.
Saving the lists
The lists live in memory; persisting them is up to you:add/removetake one id or an array and throwTypeErroron an empty or non-string id (validated before anything changes).onChangeonly reports effective changes;loadalways reports.loadAccessListsalso accepts plain arrays of ids for each list.
Next steps
- Core concepts: where each gate runs
- Hooks
- Control plane