All cases
Backend, request distribution

Automatic ticket routing: a mini-CRM with operator competence weights

The service distributes incoming tickets across operators by competence weights and current load, with no manual assignment. Source code is open and covered by tests.

Scale
Async REST API
Stage
Open source
Delivered
November 2025

TL;DR. Incoming tickets usually get assigned to operators by hand or round-robin. This service does it itself: each operator has configurable competence weights per ticket source and a concurrent load limit, and routing is a weighted random draw among those available. Async REST API on FastAPI, code is open.

Situation: assigning tickets by hand

Tickets arrive from different sources — a website form, a messenger, the phone, a partner channel. Operators are not equally good at all of them: one is stronger on technical questions, another on billing.

Manual assignment needs a dispatcher. Round-robin needs no dispatcher but ignores both competence and current load: a ticket goes to someone already holding five open conversations while a colleague sits free.

Task

  1. Route tickets automatically, without a dispatcher.
  2. Account for who is stronger at which type of ticket.
  3. Never load an operator past their configured concurrency limit.
  4. Build it as a service with a REST API rather than a script inside a monolith.

What we built

An async FastAPI service. The data model describes operators, ticket sources and weights: each operator-source pair carries a competence weight reflecting how well that person fits that type of ticket.

When a new ticket arrives, the service selects operators who still have capacity and picks the assignee by a weighted random draw against the competence weights for that source. The randomness is deliberate: deterministically choosing the maximum would send every ticket to the single strongest operator until they hit the limit.

Database access goes through async SQLAlchemy. The project is covered by tests and published at github.com/pimenoffd.

Result

Key technical decisions

  1. Weighted random routing rather than picking the maximum. Always giving the ticket to the most competent person means a queue at one desk and idle time everywhere else. The weight shifts probability rather than dictating the assignee.
  2. The load limit as a filter before routing. An operator at capacity is excluded from the pool before weighting, rather than penalised in their weight. That keeps the limit a guarantee instead of a preference.
  3. Weights per operator-source pair. Competence isn't abstract: someone can be strong on tickets from one channel and weak on another. A single overall operator rating loses that difference.
  4. Async all the way down. FastAPI and SQLAlchemy in async mode: routing runs on every incoming ticket, and blocking database access hits a ceiling as volume grows.

FAQ

Is this a ready-made CRM?

No, it's a routing service — one job, solved completely. It demonstrates the architecture of automatic ticket assignment and plugs into an existing CRM rather than replacing it.

Why random routing instead of always picking the best operator?

Deterministically picking the best creates a queue at one desk while colleagues sit free. Weighted randomness keeps competent operators favoured while spreading the load.

Can I use the code in my own project?

The code is open on GitHub. Fitting it to a specific process takes configuring the weight model and integrating your ticket sources.

What would come next

The natural continuation is ticket priorities and response-time SLAs, reassigning stalled conversations, and tuning weights from operators' actual outcomes rather than by hand.

If incoming tickets are routed manually or round-robin, let's talk it through in 30 minutes.

Stack

  • Python
  • FastAPI
  • SQLAlchemy (async)
  • SQLite
  • pytest

Same process in your company? Cortex IT writes a mini-audit in 2-3 days, free of charge.

Request a mini-audit