Handoff to Human Agents (Mbox Agent Handoff)
Overview
"Mbox" is the live chat system that lets human agents take over a conversation with a LINE user in place of the bot. Under the hood it is Chatwoot. This feature is the logic that decides whether a given message should be answered by the bot or escalated to a person — and it has to live in webhook-go because the decision must be made before the message reaches the bot.
The core of the mechanism is the Redis key agent_mode:{lineOaId}:{userId}. Its presence means
the user is currently talking to a human agent, and during that time the bot must stay
completely silent, meaning nothing is published to the line_webhook queue.
All of this only runs when webhook_config.mboxEnabled == "1". When disabled, events flow
through the normal default path.
Business Flow
A. Message events
Only the first message event is considered, and its message.text is trimmed and
lowercased.
- Derive
lineOaIdfrom the config anduserIdfromevent.source.userId. - Read
agent_modewithHGETALL; a non-empty hash means the user is in agent mode. A read error is logged and halts the whole pipeline. - Parse two keyword sets from the config —
mboxExitKeywordsandmboxAgentKeywords. Both are JSON arrays of strings, lowercased on load. If parsing fails, the value falls back to a comma-separated list with whitespace trimmed and empty entries dropped.
Case A1 — the user is already in agent mode
- If the message matches an exit keyword, publish to the
mbox_handoffqueue withtype: "exit". worker-go then closes the Chatwoot conversation and deletes theagent_modekey. - Otherwise, forward the message to
mboxLineWebhookUrl(see Forwarding Webhooks to Customer Systems) and writelastActivityas an epoch timestamp in milliseconds intoagent_mode, with no TTL. - Either way, the handler returns immediately without publishing to
line_webhook, which is what keeps the bot quiet.
Case A2 — the user is not yet in agent mode
- If the message matches an agent keyword (for example "talk to an agent"), the handoff
path begins:
- When
mboxDepartmentPickerEnabledis not"1", publish withtype: "handoff"and the standardmboxConfig. - When
mboxDepartmentPickerEnabledis"1", publish withtype: "department_picker"and anmboxConfigthat includes adepartmentPickerblock containing enabled, headerText, generalLabel, and departments. The departments list is parsed from the JSON string inmboxDepartments; a parse failure yields an empty array. - Then return, bypassing the bot.
- When
- If no keyword matches, the event falls through to the normal path and is published to
line_webhookfor the bot to handle.
B. Postback events — department selection
- Read
postback.datafrom the first postback event. - If it starts with
mbox_team:, take the value after the colon asteamId. - If
source.userIdis present, publish tombox_handoffwithtype: "handoff"plus theteamId, then return. - If there is no userId, the handler does not return; it falls through to the
appt_cancel:check (see Cancelling Appointments via Postback).
Shape of the mbox_handoff payload
{
"type": "handoff | department_picker | exit",
"lineOaId": 123,
"userId": "LINE user id",
"teamId": "optional, mbox_team only",
"webhookPayload": { "webhookId": "...", "headers": {}, "body": {} },
"mboxConfig": {
"enabled": true,
"baseUrl": "...",
"apiToken": "...",
"accountId": "...",
"inboxId": "...",
"lineWebhookUrl": "...",
"timeoutMinutes": 30,
"warningMinutes": 25,
"greetingMessage": "...",
"warningMessage": "...",
"endMessage": "...",
"timeoutMessage": "...",
"exitKeywords": [],
"agentKeywords": [],
"departmentPicker": {}
}
}
:::warning Unintentional inconsistency (parity note)
The handoff branch triggered by an agent keyword sends BodyRaw, the original bytes, along
with the payload. The exit and mbox_team branches do not set BodyRaw, so their outgoing
body is re-marshalled from the map with reordered keys, which invalidates the signature in
those two cases (see Forwarding the Raw Body and Signature).
:::
Key Files & Functions
All the code lives in internal/line/service.go — there is no separate package — and is
reached through the POST /api/line/:id endpoint (see
LINE Webhook Gateway).
| Function | Responsibility |
|---|---|
Service.ProcessLine | The main dispatcher, covering both the message and postback stages |
buildMboxConfig | Maps the Redis hash into a config struct |
Service.deptPickerConfig | Spreads mboxConfig and attaches the departmentPicker block |
parseKeywords | Converts a JSON array into a lowercased string slice, with a comma-separated fallback |
contains, messageText, postbackData, sourceUserID, splitSecond | Helpers |
Types mboxHandoffPayload, mboxConfig, webhookPayload | Payload structures |
Connections to Other Services
- Redis —
webhook_configsupplies all mbox settings andagent_modeholds session state (see Redis Cache for Webhook Config). - RabbitMQ — the
mbox_handoffqueue, configured viaRABBITMQ_QUEUE_MBOX_HANDOFF. - worker-go — consumes
mbox_handoff, creating and closing Chatwoot conversations, creating and deleting theagent_modekey, sending the greeting, warning, end, and timeout messages, and managing session timeouts. - Chatwoot (Mbox) — an external system. Its credentials (baseUrl, apiToken, accountId, inboxId) travel with every payload; this service never calls the Chatwoot API itself.
- The return path — Chatwoot calls back into
POST /api/mbox/callback/:oaHash(see Receiving Chatwoot Callbacks). - cms-api — the per-OA mbox settings screen, covering keywords, messages, and departments,
lives in the
line-oa-managementmodule.