The support queue is backing up
How the desk console orders work, why "waiting" is computed from the conversation and not the status column, and the order to drain a backlog in.
Support carries the tightest target on the platform — 24 hours, against seven days for a withdrawal — and it is the only core queue whose health check fires on priority rather than on age. If the Operations inbox is amber on Support, or the system health card says "N high priority tickets open", this is the page.
The work happens on Users → Customer Support (/admin/crm/support).
Everything else — search across the whole desk, export, bulk verbs, closed
tickets — is on the archive at /admin/crm/support/tickets, which has no menu
entry of its own and is reached from the console's bar.
The console
/admin/crm/support is a console, not a page. It has no site header and no
footer (the route is on the admin layout's chromeless list), and it fills exactly
one viewport with three panes:
| Pane | What it holds |
|---|---|
| Left — the queue | One row per conversation: subject, customer, the last thing said, a HIGH-priority chip, the age, the message count, and who owns it |
| Centre — the conversation | The whole thread, customer messages on one side and staff on the other, with a composer at the foot |
| Right — the case | Ownership first, then status, priority, assignment and the customer's details. Below xl it is a slide-over instead of a column |
::kbd[J] moves down the queue and ::kbd[K] moves up, bound to the rows you can
currently see — so they agree with the filter you have on rather than jumping to
something off screen. Both are suppressed while a text field has focus, so j is
still a letter in the composer.
The queue polls every 20 seconds; the open conversation holds a websocket, so
a customer's reply lands without a refresh. The ticket id is written into the URL
as ?ticket=<id>, which makes a conversation pasteable to a colleague.
The composer only appears for edit.support.ticket. The route itself is gated on
access.support.ticket, so a read-only agent gets the queue and the thread and no
buttons — deliberately, rather than a row of controls that 403.
There is no create action on the desk, none on the archive table, and no POST route behind either. Tickets are raised by customers, through the help centre or the live-chat widget. If you need a conversation with a customer that they have not started, use the notification and email tooling, not this desk.
"Waiting" is not a status
This is the one idea the whole screen is built on, and it is the thing that makes the counters look like they disagree.
Waiting on us means the customer spoke last. The queue route reads each ticket's message thread, drops the system chips, looks at who wrote the last real message, and orders the desk by that:
- Waiting on us — the customer spoke last, oldest wait first
- Waiting on them — we spoke last
- Closed — only when you ask for it
A ticket whose thread is empty also counts as waiting on us: a ticket raised through the form carries its body on the ticket rather than as a message, and nobody has answered it.
The status column is a separate axis with four values:
| Status | Means |
|---|---|
PENDING |
No agent has ever replied |
OPEN |
The customer spoke last |
REPLIED |
An agent spoke last |
CLOSED |
Done |
Those normally agree with the thread — the customer's reply route writes OPEN,
the agent reply route writes REPLIED. They can still disagree, because status
is also writable by hand from the case pane's status control and from the
archive's bulk actions. Set a ticket to REPLIED while the customer's message is
the last one in it and the column says "answered" while the thread says
"waiting". The thread wins on the console; the column drives every counter.
PUT /api/admin/crm/support/ticket/assign writes status: OPEN on every ticket
it assigns (and PENDING on every one it unassigns), across rows currently
PENDING, OPEN or REPLIED. CLOSED rows are left alone.
So bulk-assigning a batch that includes answered tickets moves them to OPEN.
The console's Waiting filter will not move — it reads the thread — but the
Operations inbox counts PENDING + OPEN, and the system health check counts
OPEN, so both climb. Select the tickets you mean, not the page.
If you are looking at an install that was upgraded rather than freshly seeded,
note that REPLIED used to be written by both reply routes, for two opposite
events. The migration 20260803000001-support-ticket-status-repair re-derives
every non-closed ticket's status from its own thread (OPEN if the customer spoke
last, REPLIED if an agent did, PENDING if nothing was said) and normalises the
double-encoded messages blobs it finds on the way. It never touches CLOSED.
The five filters
The tab strip above the queue is the filter. Four of them are views of the same fetched set; Closed is a different query.
| Filter | Shows | Count on the tab |
|---|---|---|
| Waiting | The customer spoke last, oldest first | Yes — amber while non-zero |
| Mine | Assigned to your account | Yes |
| Free | No agent assigned | Yes |
| All | Everything the queue fetched | Yes |
| Closed | status = CLOSED, fetched separately |
No — the closed set is not in memory until you ask for it, so any number here would be a guess |
The search box beside them matches subject, customer name, customer email and the message preview, on the rows already loaded.
Empty live-chat shells are excluded from all of it. The widget mints a LIVE
ticket the moment a customer opens the bubble, before a word is typed; the desk's
scope requires a LIVE ticket to have at least one message before it counts.
The badge in the header counts differently
The Operations inbox in the admin header polls once a minute and its Support
queue counts status IN ('PENDING', 'OPEN') — no agent has replied yet, or the
customer spoke last. REPLIED means an agent has already answered, so it is not
on your desk.
The age it shows is the age of the oldest row in that set, measured against
SLA_HOURS.support = 24:
- Amber at half the budget — 12 hours.
- Red once anything is past 24 hours, with an overdue count beside it.
The Age column on the archive table is computed from the same number and only
tints rows in PENDING or OPEN, so a settled ticket from last year is never
painted red.
The health check fires on priority, not age
GET /api/admin/system/health/batch has a Support Queue entry, and it is
unusual:
| Condition | Result |
|---|---|
Any ticket with status = OPEN and importance = HIGH |
warning — "N high priority tickets open" |
| Otherwise | up — "N open tickets" |
There is no age term in it at all. One high-priority ticket opened a minute ago
turns the card amber, and a hundred ordinary tickets three days old do not. It is
also critical: false, so it never turns the overall health verdict red.
Treat it as a triage signal: it is telling you that something someone marked
HIGH is unanswered. Priority is set on the case pane (LOW, MEDIUM, HIGH),
so it is a claim a human made about the ticket, and it is worth checking that the
claim is right before you reorder your day around it.
Draining a backlog
Work it in this order. Assignment first, because the queue puts the oldest waiting ticket at the top of every agent's screen — without owners, two agents answer the same customer and one of them wastes the reply.
-
Take stock on the archive, not the console. Open
/admin/crm/support/tickets. The KPI strip there describes the whole desk: total, awaiting response (pending + open), unassigned, resolved, and the average first-response time. The console's queue is capped, so its counts describe a page, not the desk. -
Hand out the unowned work. Still on the archive, filter to the tickets you want, select them, and use Assign to me or Unassign selected from the bulk menu. Assignment deliberately takes no reason — handing work to a colleague is not a decision the customer needs explained. Remember it rewrites
status; see the warning above. -
Work the console on Waiting, top down. Back on
/admin/crm/support, choose Waiting. The top row is the customer who has been waiting longest. Answer, set the status, ::kbd[J] to the next one. The screen never changes. -
Sweep Free afterwards. Anything left under Free is work nobody has claimed. If it is still there at the end of a shift, it is the queue that will breach first.
-
Close in bulk from the archive, with a reason. Close selected and Reopen selected both route through the decision dialog and both require a reason, because the endpoint appends it to each ticket's own conversation — the customer is told why their ticket was closed instead of it silently vanishing.
The console is capped; the archive is not
GET /api/admin/crm/support/inbox returns 60 rows by default and 200 at most,
selected newest-activity-first before the waiting-order is applied. That keeps the
console on the live end of the desk. Its response carries a capped flag, and its
total is the size of the page it returned — not the size of your desk.
/admin/crm/support/tickets is the only screen that reaches every ticket ever
raised. It is a paged table over the same population with per-column filters,
search across the whole desk, CSV export, the analytics tabs, and the four bulk
verbs. It opens filtered to status in (PENDING, OPEN) sorted oldest-first, so
its default view is the same work the console shows — widen the status filter to
find anything older.
Rows link through to the single-ticket page at /admin/crm/support/{id}.
Where the AI Support add-on fits
The desk console carries no AI: no drafted replies, no retrieval evidence, no confidence meters. That is deliberate — those need data this route does not have, and half of them would 403 for an operator who has not bought the add-on.
The one slot that knows the add-on exists sits at the foot of the queue pane, and it has two states:
- Installed and enabled — a link to
/admin/ai/support/inbox, the Live Inbox, where the same tickets carry the assistant's reasoning, its citations and a takeover button. That screen is gated onview.support.ticket, the key your desk agents already hold, so it appears on upgrade with no grant step. - Not installed — a description of what the add-on does and a link to its
extension page. It is hidden entirely from operators without
access.extension, since a locked door on every shift is noise they cannot act on.
Either way, if the assistant has answered a ticket, its replies appear in this console's thread drawn with the AI badge and a bot glyph. That is a disclosure requirement, not a feature.
Related
- The admin panel — the Operations inbox and the daily routine around it.
- The scheduler console — where to check that background work is running at all.
- Roles and permissions — how
access.support.ticketandedit.support.ticketare granted.