Documentation

Queue routing

Route incoming chats to staff with priority queues, skill matching, and rolling ETA estimates.

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-next or takeover-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

POST/chat/queueJWT

Used 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" }
}
FieldTypeDescription
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 chatSessionId changed.

queuestring

Queue name, defaults to default. Use multiple queues to model departments.

skillstring

Required agent skill. pick-next with the same skill will match.

prioritynumber

Higher numbers are picked first. Same priority orders by FIFO.

contextobject

Free-form metadata shown to the picking agent (AI summary, intent, etc.).

Reading the queue

GET/chat/queueJWT

Lists waiting entries. Optional ?name=billing to scope to a single queue.

GET/chat/queue/statsJWT

Counters and rolling averages: queue size, available agents, ETA, average handle time.

GET/chat/queue/position/{chatId}JWT

Position 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:

  1. Pops the highest-priority entry that matches the optional skill filter
  2. Joins the agent socket to chat:{orgId}:{chatId}
  3. Joins the customer socket to the same room (cross-pod via Socket.IO Redis adapter)
  4. Saves a system "agent has joined" message
  5. Emits agent-assigned to the customer and queue-updated to every agent
  6. Arms the inactivity timeouts (5 min response, 30 min global)
  7. Increments the agent's activeChats counter

Removing a chat

DELETE/chat/queue/{chatId}JWT

Admin 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.

Skill matching is opt-in

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-all
  • billing, support, sales — by department
  • vip — priority customers, often paired with priority: 100
  • after-hours — populated by the after-hours IVR flow when no agents are online