Skip to main content

Inbox conversation push-routing (auto-assignment)

By default the Inbox is a pull queue: a conversation a routing rule did not directly assign sits unassigned until an operator opens it and clicks Claim. Push-routing is an optional per-organization scheduler that flips that queue to push — it watches the unassigned backlog and hands the oldest waiting conversations to the best available operator, so a routed-to-nobody conversation still reaches a human without anyone clicking anything. This page explains the model: what the unassigned queue is, how to enable the scheduler, how it picks an operator, what its atomicity guarantees are, and how it compares to the voice ACD queue and to manual claiming. The step-by-step Inbox setup lives in the Inbox setup guide; the ownership model the scheduler writes to (assigned_to) is described on the conversations concept page. Push-routing assigns only inbound conversations waiting in the Inbox. It never sends messages and never touches outbound routing — outbound messaging and voice exit only through the Devotel softswitch, and every control on this page is tenant-owned configuration.

The default: an unassigned pull queue

A conversation enters the Inbox either pre-assigned (a routing rule matched an assign_user, assign_team, assign_round_robin, or assign_skill_based action) or unassigned — status pending, no owner. Unassigned work is visible to the whole team, and an operator takes it by clicking Claim on the conversation row. Until someone does, it waits. Pull semantics work when a team triages deliberately — a supervisor dividing the morning backlog, or agents picking work that matches their shift. It breaks when nobody remembers to claim: first-response SLA clocks keep running against an ownerless conversation, and the breach surface blames no one. Push-routing exists to close that gap for teams that want the backlog drained automatically while keeping the pull model for everything else.

The opt-in gate

Push-routing is disabled until the organization turns it on. The gate lives in the organization settings object at settings.conversation_push_routing:
Only enabled: true is required; every other field falls back to its default. The scheduler re-reads opted-in organizations within minutes of a change, so turning it on takes effect on the next sweep without a deploy.

The 30-second sweep

Every 30 seconds the scheduler runs one pass over the opted-in organizations. For each, it:
  1. Snapshots the routable operator slate — organization members who are not marked offline, do-not-disturb, or away, with their current open-conversation count attached. A member whose presence is offline, DND, or away never receives a push.
  2. Claims the oldest waiting conversations — pending and unassigned, oldest first, bounded by max_per_tick. The claim uses row-locking that skips rows another worker replica already holds, so horizontally scaled replicas never double-process the same conversation.
  3. Applies the skip_channels carve-out and the capacity gate — conversations on a skipped channel stay unassigned, and once every routable operator is at the max_per_agent ceiling the pass stops (the remainder is revisited on the next sweep, capacity permitting).
  4. Assigns each conversation to the best operator (next section), stamps the conversation open, publishes the same real-time conversation-update event the inbox consumes, and fires the normal notification — exactly what a manual assignment produces.
The pass is tenant-scoped end to end: candidates and assignments are read and written inside the organization’s own tenant schema.

How an operator is picked: skill match, then round-robin

For each claimed conversation the scheduler first looks for a required-skills hint — the tags on the contact the conversation is with. It then ranks the routable operators with the same skill-affinity matcher the routing-rule engine’s assign_skill_based action uses:
  • Affinity is the fraction of required skills the operator also carries on their own skill list. The operator with the highest affinity wins.
  • Ties break toward the more specialized operator (smaller total skill list), then alphabetically, so the outcome is deterministic.
  • No required skills, or no overlap — when the contact has no usable tags, or no routable operator carries any of them, the scheduler falls back to round-robin: the operator with the fewest open conversations wins, ties broken alphabetically. A conversation with no skill hint still gets assigned whenever at least one non-offline operator exists; it never waits for a match that cannot exist.
An operator with an empty skill list still participates in the round-robin fallback — an empty list means “no recorded specialty,” not “ineligible.” Skills on this page are the same per-operator tags the skill catalog and the routing rules’ assign_skill_based action use, so one skill vocabulary drives the voice queue, the routing rules, and the push-routing scheduler.

Atomicity guarantees

Two operators — or an operator and the scheduler — can target the same unassigned conversation at the same moment. The scheduler defers to whoever gets there first, with two guards:
  • Skip-locked candidate claim. The scheduler locks the candidate rows it is about to consider in a way that lets concurrent replicas skip each other’s rows. Two scheduler replicas never evaluate the same conversation, so no duplicate assignment work.
  • Race-safe assignment. The assignment write succeeds only if the conversation is still unassigned and still pending. If an operator clicked Claim — or a bulk-assign ran — in the milliseconds between the scheduler’s read and its write, the write matches zero rows and the scheduler skips the conversation. A manual claim always wins; push-routing never clobbers an operator’s own pickup, and it never overwrites an existing owner.
The same “only-if-unassigned” predicate is what the manual Claim endpoint uses, so the scheduler and the UI race on identical, mutually exclusive terms.

Worked example: two operators, one skilled

An organization enables push-routing with defaults (max_per_tick: 25, max_per_agent: 5). Two operators are signed in:
  • Amara — skills ["billing", "english"], 1 open conversation
  • Theo — skills [], 0 open conversations
Three conversations arrive unassigned:
  1. A WhatsApp conversation with a contact tagged billing. Amara’s affinity is 1 (she carries billing); Theo’s is 0. The scheduler assigns it to Amara; her open count rises to 2.
  2. An email conversation whose contact has no tags. No skill hint, so round-robin applies: Theo has fewer open conversations and receives it. Theo, open count now 1.
  3. An SMS conversation, but the organization set skip_channels: ["sms"]. It stays unassigned in the pull queue for the team to claim manually.
If Amara hits 5 open conversations mid-sweep, the capacity gate removes her from the slate and remaining pushes go to Theo — or wait, if Theo is also at the ceiling.

Push-routing versus the alternatives

The closest sibling is the ACD queue model: the voice queue pushes call legs to available agents in real time, while push-routing brings the same push behaviour to the inbox backlog on a sweep cadence — same skill vocabulary, same “assignment never clobbers” discipline, one fifth of the machinery (no ringing, no no-answer timeout, no overflow action). If a waiting conversation must land on the right agent immediately rather than the best available agent within one sweep, route it explicitly with an assign_skill_based or assign_user routing rule; push-routing is the backstop for everything the rules leave unassigned.
  • Inbox setup — channels, routing rules, macros, SLA, and AI drafts end to end.
  • Conversations concept — the thread model and the ownership this scheduler writes.
  • ACD queue model — the voice-side push machinery this page compares against.
  • Skill catalog — the per-operator skills the match consumes.
  • Contact reasons — tagging contacts so the skill match has something to match.