ChatQueueService holds customers waiting for a live agent. It's Redis-backed (sorted by priority, then enqueue time) and survives pod restarts.
When chats enqueue
A customer message ends up in the queue when none of these are true:
- A specific AI assistant is selected for the chat config
- A human agent is already assigned to this chat (from a previous
pick-nextortakeover-chat) - The chat was explicitly transferred to an agent
If all three fail, ChatGateway.handleChatMessage calls ChatQueueService.enqueue() and emits a queued event back to the customer plus a queue-notification to relevant agents.
Manual enqueue
/chat/queueJWTUsed by AI handoff (the transfer_chat_to_agent tool) and any custom integration that wants to drop a chat into the queue.
{
"chatId": "customer:[email protected]",
"customerEmail": "[email protected]",
"customerName": "Alice",
"queue": "billing",
"skill": "spanish",
"priority": 10,
"context": { "summary": "Refund request, $240, order #ABC-12" }
}
| Field | Type | Description |
|---|---|---|
| chatId* | string | The conversation identifier the agent will pick up. |
| customerEmail* | string | Used for dedup — the same customer can't be queued twice even if their |
| queue | string | Queue name, defaults to |
| skill | string | Required agent skill. |
| priority | number | Higher numbers are picked first. Same priority orders by FIFO. |
| context | object | Free-form metadata shown to the picking agent (AI summary, intent, etc.). |
Reading the queue
/chat/queueJWTLists waiting entries. Optional ?name=billing to scope to a single queue.
/chat/queue/statsJWTCounters and rolling averages: queue size, available agents, ETA, average handle time.
/chat/queue/position/{chatId}JWTPosition and ETA for one chat. The same data is pushed to the customer every 15s as a queue-update event while they wait.
Picking next
Agents pick from the gateway, not from REST.
socket.emit('pick-next', { queue: 'billing', skill: 'spanish' }, (res) => {
if (!res.success || !res.chat) return;
// Server has joined this socket to chat:{orgId}:{chatId}
// Customer has been notified via 'agent-assigned'
});
pick-next does several things atomically:
- Pops the highest-priority entry that matches the optional
skillfilter - Joins the agent socket to
chat:{orgId}:{chatId} - Joins the customer socket to the same room (cross-pod via Socket.IO Redis adapter)
- Saves a system "agent has joined" message
- Emits
agent-assignedto the customer andqueue-updatedto every agent - Arms the inactivity timeouts (5 min response, 30 min global)
- Increments the agent's
activeChatscounter
Removing a chat
/chat/queue/{chatId}JWTAdmin override — drop a chat without picking it. Optional ?name=billing.
ETA model
The estimate is intentionally simple:
ETA = ceil(position / availableAgents) * avgHandleTimeSeconds
avgHandleTimeSeconds is a rolling average of the last 100 closed chats per queue, stored under queue:{orgId}:{queueName}:stats. Until 100 samples accumulate, the default is 180 seconds.
A queued chat with skill: 'spanish' will be left in the queue until an agent calls pick-next with that skill (or no skill, since unfiltered pick-next ignores the field). Make sure your agent UI sets the right skill or your specialty queues stagnate.
Multi-queue patterns
A single org can run any number of queues. Common patterns:
default— catch-allbilling,support,sales— by departmentvip— priority customers, often paired withpriority: 100after-hours— populated by the after-hours IVR flow when no agents are online