AI coding

Routing Misses to a Human Queue: Fields and Timers

Antonio Evans10 min read

A support engineer's desk seen from above: a laptop shows a queue table with columns for message, candidates, priority and timer.
Key points

Key points

  1. 1Every request that matches no rule gets its own row in a queue table, carrying the original message, the candidate routes considered, a priority score, an owner field and a deadline timestamp.
  2. 2Priority is computed from signals such as customer tier, request class and wait time, so a long-waiting low-tier item can overtake a fresh high-tier one.
  3. 3A timer that fires on an unclaimed item raises its priority and notifies the next tier, so silence is never treated as ownership.
  4. 4Each human resolution is converted into a new routing rule, which shrinks the unmatched pool over time.

Why the unmatched path needs its own table

A router that returns nothing looks like an edge case, but it is the normal state of any catalog that keeps growing. In a benchmark built from roughly 80,000 candidate skills, the best retrieve-and-rerank pipeline reached 68.0 percent top-1 accuracy, which means roughly one request in three landed somewhere other than the correct first choice (SkillRouter paper). When each individual decision is that uncertain, a fallback has to be a designed destination rather than an error log.

The same problem appears in support tooling. Microsoft's unified routing documentation describes fallback queues specifically for interactions that fall outside the criteria an administrator has defined (fallback queue walkthrough). Zendesk's omnichannel routing has an equivalent: a default queue picks up tickets that match no custom queue (Zendesk routing documentation).

The difference between a good and a bad fallback comes down to what it records. A tag on a ticket saying "unrouted" does not tell the person who opens it what the system was thinking, who should own it, or when to act. A queue row does.

Here is the working rule: if a request cannot be answered by a rule, it becomes an object with a life cycle, not a log line.

The queue record: fields that earn their place

A minimal but sufficient schema fits in one table. Each field solves a specific failure.

FieldTypeWhy it existsFailure if missing
item_idstring, primary keyStable handle for every mention, timer and audit entryDuplicate work, lost threads
created_attimestampStart of the wait clockNo SLA measurement possible
channelenumEmail, chat, phone, API, internalTriage cannot set the response format
raw_messagetextThe user's words, unsummarizedReviewer has to reconstruct context
normalized_summarytextOne or two sentences for scanningReviewers read everything, slow triage
candidatesJSON arrayTop routes with scores and the reason each lostNo feedback loop is possible
priority_scorefloatOrders the queueOrdering is whoever shouts loudest
tierenumCustomer or account tier used in scoringPriority is opaque and disputed
stateenumNew, claimed, working, resolved, closed, reopenedItems disappear without trace
owner_idstring, nullableThe person who owns it right nowEveryone assumes someone else did
deadline_attimestampWhen the timer firesItems sit indefinitely
escalation_levelintegerWhich tier is currently responsibleEscalation is ad hoc
resolution_codeenumWhy it resolved, used for rule generationLearning loop never closes
rule_id_createdstring, nullableThe routing rule the resolution producedThe same miss repeats

Two of these deserve emphasis. The candidates array is the field most teams omit, and it is the one that turns a queue into a training set. Store the route name, the score, and one line on why the router ranked it: too low a score, two candidates tied, no candidate at all. When a reviewer later says the right answer was candidate three, that single record is a labelled example.

The resolution_code is the other. Free-text notes are useful to a human and useless to a router. A small closed list (wrong intent, missing information, catalog gap, policy exception, duplicate) tells you whether to add a rule, add a route to the catalog, or change the form that collects the request.

Microsoft's intent-driven routing documentation shows the same concept from the platform side: classification rules read intent fields and attach skills or priority to the work item before it reaches a queue (intent-driven routing). Your schema should do that work explicitly so that you can see and change it.

A workable row looks like this:

{
  "item_id": "q-2025-04812",
  "created_at": "2025-06-02T09:14:11Z",
  "channel": "email",
  "raw_message": "Our invoice shows a charge for a plan we cancelled...",
  "normalized_summary": "Disputed charge after cancellation",
  "candidates": [
    {"route": "billing.refunds", "score": 0.41, "note": "below threshold 0.55"},
    {"route": "billing.cancellations", "score": 0.38, "note": "lexical match only"},
    {"route": "account.status", "score": 0.21, "note": "no supporting terms"}
  ],
  "tier": "enterprise",
  "state": "new",
  "owner_id": null,
  "priority_score": 78.5,
  "deadline_at": "2025-06-02T09:59:11Z",
  "escalation_level": 0,
  "resolution_code": null,
  "rule_id_created": null
}

Store the candidates as written, not as a regenerated list you compute later. Routing logic changes, and a replay against a newer router will not reproduce what the reviewer actually saw.

Scoring priority so it can be defended

Most queues default to a five-level priority field set by hand. That breaks the moment volume rises, because nobody has time to hand-tag. A numeric score computed from a handful of signals holds up better and is auditable.

One practical formulation:

score = (tier_weight * 40)
      + (intent_risk * 25)
      + (min(wait_minutes, 240) / 240 * 25)
      + (repeat_contact * 10)

Each term is bounded so no single input dominates. A worked example using two requests arriving at the same second:

SignalRequest A, enterprise, first contactRequest B, self-serve, third contact in a week
Tier weight (1.0 vs 0.4)40.016.0
Intent risk (payment issue 1.0 vs how-to 0.2)25.05.0
Wait term at 0 minutes0.00.0
Repeat contact (1.0 vs 0.0)0.010.0
Total now65.031.0

Ninety minutes later, with nobody claiming either, the wait term adds 9.4 points to each. Request A sits at 74.4 and Request B at 40.4. The ordering has not changed, which is the point: the wait term should bend the order, not invert it in the first hour.

The decision rule for the wait term is to cap it. If wait is uncapped, every old item eventually outranks every new emergency, and the queue becomes a backlog sorted by age. A 240-minute cap means an item at four hours scores the same as one at eight, which is correct if your target first response is under four hours. Set the cap to your target response time and no lower.

Where the risk term comes from matters. Microsoft's routing model lets classification rules attach attributes such as priority based on the identified intent before the item is queued (intent-driven routing). The same move works here: a small map from intent to risk weight, versioned in code so you can see when it changed. If you have an LLM classifier already, its confidence is a usable input, but only after you have checked its calibration, because a raw model score is not a probability unless it has been calibrated on your traffic (guardrail calibration guide).

Above all: the score is a sort key, not a promise. Round it to one decimal and never show more precision than the inputs support.

Timers, re-escalation and what "claimed" means

The timer is the mechanism that stops a fallback queue from becoming a graveyard. Two fields do the work: deadline_at and escalation_level.

Set the first deadline from the priority band rather than from a single global value. A simple version:

BandScore rangeFirst deadlineSecond deadlineThird deadline
P180 and above15 minutes30 minutes60 minutes
P260 to 79.930 minutes60 minutes2 hours
P340 to 59.960 minutes2 hours4 hours
P4below 404 hours8 hoursnext business day

On each fire, three things happen in a fixed order: raise escalation_level by one, add the escalation bump to priority_score, and notify the next tier's channel. Do not send the escalation to the original queue. If nobody claimed it at level zero, the same people seeing it again is not escalation, it is a reminder.

The escalation policy literature makes two points that transfer cleanly. First, escalation needs to have no gaps: every step must name a specific person or rota, not a team alias. Second, timing needs to account for handoffs between time zones and for the difference between a request that can wait and one that cannot (escalation policy best practices). For a support queue, the time-zone point matters most: an item created at the end of a shift must land on the next shift's rota, not on a sleeping individual.

Define "claimed" precisely. A claim is an explicit action that writes owner_id and moves state to claimed in the same transaction. Reading, opening, or being assigned by a round-robin are not claims. If reading counted, the most diligent reader would silently own the whole queue. The claim should also write an expiry: if the owner goes silent for longer than the band's first deadline, the item returns to the queue with a bumped escalation level, and the original owner is recorded in an audit field for later review.

This matters because contact centre practice already separates "who can take it" from "who should". Skills-based routing guidance notes that language and compliance are hard constraints while other skills are preferences, and that rigid segmentation into small pools makes wait times worse even when total staffing has not changed (skills-based routing design). The same trap applies to a fallback queue: do not create twelve specialist queues for twelve intents. Use one queue with owners drawn from a shared pool, and let the candidate list tell the owner who else might help.

Closing the loop: from resolution to routing rule

A fallback queue that only drains is a cost centre. A fallback queue that produces rules is an investment. Three outcomes should emerge from each resolution, matched to the resolution_code:

  • Wrong intent: the router had the right candidates and scored them below threshold. Fix the threshold for that intent or add a distinguishing term. Do not add a new route.
  • Missing information: the request needed a field the form never collected. Change the intake form, which is cheaper than any model change.
  • Catalog gap: no candidate was close. Add a route and a rule, with the resolved item as its first example.
  • Policy exception: the rule exists and was correctly skipped. Record it so the exception does not get "fixed" into a rule that fires wrongly.

The insight that justifies the investment: adding a route is not free. A Medium analysis of skill catalogs argues that installing a new skill can make an agent less reliable without changing the old skill at all, because the new entry shifts the decision boundary between existing options (agent skill routing at scale). That is the strongest argument for a queue with candidates attached: you can measure whether the new rule took traffic away from a correct old one.

Two guards keep the loop honest. First, keep a held-out sample of unmatched items you resolve but do not convert into rules, so you have an unbiased measure of how many misses the catalog still produces. Second, re-run the stored candidate sets against the current router before publishing a rule cluster. If the new rule would have fired on historical items in other intents, it is too broad.

The literature on tool and agent selection in production makes the same point: selection is a pipeline with a frontend and a backend, and the backend components (retrieval, routing, evaluation, human-in-the-loop) need their own instrumentation rather than being inferred from task success (tool and agent selection survey). The queue is that instrumentation.

On the router side, if you are classifying before dispatch, a narrow classifier that routes to a small set of specialists and defers anything out of distribution to humans is a well-trodden pattern with documented economics, since embedding-based routing is dramatically cheaper per query than asking a large model to pick from a long list (intent classification layer). The queue is the out-of-distribution sink that makes a narrow classifier safe to ship.

A worked run through one unmatched request

Take the disputed-charge email from earlier. It arrives at 09:14, matches nothing above the 0.55 threshold, and gets a row.

At 09:14 the score is 65.0 (enterprise, payment risk, no wait yet). Because 65.0 lands in P2, the first deadline is set to 09:44.

Both owners are in a meeting. At 09:44 the timer fires. Escalation level goes to one, the score picks up a bump, and the on-call billing lead is paged with the summary and the three candidates.

The billing lead claims it at 09:51, writes owner_id, moves state to claimed, and resolves it at 10:06 with resolution_code set to wrong intent: the charge was a proration, and the right route was billing.proration. The reviewer marks candidate one, billing.refunds, as the tempting-but-wrong option and notes that both descriptions mention the word "charge".

The rule generated from that item is small: if the message mentions cancellation and the account is still within a proration window, route to billing.proration before billing.refunds. The queue then replays the stored candidates of the last 200 unmatched items. If eleven of them would now match the new rule and all eleven are billing disputes, publish it. If two are unrelated, tighten the condition.

Time to first human touch was 37 minutes against a 30-minute target. That is a useful failure: either the P2 first deadline is too tight for a two-person team, or the rota needs a backup. Either answer is better than the item sitting unseen.

If you run this pattern across several intents and want a single API to reach the text, image or audio models that classify and summarize the incoming requests, Quanta Inference covers them under one key; the routing decisions stay in your code and your rules.

FAQ

FAQ

How many candidate routes should I store per unmatched item?

Store between three and ten, ordered by score, with the reason each fell through. Three is enough for a reviewer to judge the router's thinking, and ten is enough to replay against future rules. Beyond that you are storing noise, and the row becomes unreadable on screen.

Should the queue record contain the full original message?

Yes, verbatim, alongside a short normalized summary. Redact secrets and personal data at write time rather than dropping the body, because reviewers routinely need the exact wording to spot the missing detail that caused the miss. The summary exists for scanning, not for replacing the source.

What happens when the last escalation level fires and nobody responds?

Route it to a named duty manager and mark the state as breached rather than closing it. A breached item stays visible and counts against the queue's health metric. Closing it silently is the one option that guarantees you learn nothing from the miss.

How do I stop the escalation timer from waking people at night?

Split the deadline by working hours rather than stretching it. Compute deadline_at in business minutes for lower priority bands, and keep wall-clock timers for the top band only. When handing to another time zone, point the escalation at the next shift's rota, since escalation chains with time-zone gaps are a known failure mode (escalation policy best practices).

How do I know the fallback path is getting better rather than just busier?

Track two numbers monthly: the share of all requests that land in the queue, and the share of queued items that required no escalation. A falling first number means rules are absorbing traffic, and a rising second number means the queue is being worked. Track both together, because one without the other hides confident misrouting.

Sources

Sources

  1. SkillRouter: Skill Routing for LLM Agents at Scale
  2. Unified Routing Fallback Queues | #PowerPlatformTV 063
  3. Understanding how omnichannel routing uses queues to ...
  4. Configure intent-driven routing (preview)
  5. Guardrail Calibration | 0DIN Defense Docs
  6. Escalation policy best practices: designing ...
  7. Skills-based routing design, getting the right agent ...
  8. Agent Skill Routing: Why Catalog Reliability Breaks at Scale
  9. Tool and Agent Selection for Large Language Model ...
  10. The Intent Classification Layer Most Agent Routers Skip
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles