Prompt
How do I build round-robin scheduling for a sales team?
Latest observation
A good round-robin scheduling system for a sales team usually means:
- leads are assigned evenly
- assignment is predictable and fair
- reps can be excluded when unavailable
- rules are flexible by region, team, product, or lead type
- it works in real time without race conditions
Here’s a practical way to build it.
1) Define the assignment rules
First, decide what “fair” means.
Common rules:
- Strict rotation: each new lead goes to the next rep in order
- Weighted rotation: some reps get more leads than others
- Availability-aware: skip reps who are out of office, at capacity, or disqualified
- Segmentation-based: separate queues by territory, product, channel, or customer tier
Example:
- Team A handles inbound SMB leads
- Team B handles enterprise leads
- Within each team, rotate leads among available reps
2) Choose what state you need to store
At minimum, you need to persist:
- Rep list: active reps in each queue
- Current pointer: who should get the next lead
- Rep status: active, inactive, on vacation, overloaded
- Lead assignment history: for auditing and fairness
- Optional weights/caps: if some reps should get more/less
A simple data model:
sales_reps
idnameteam_idactiveweightmax_open_leadscreated_at
round_robin_queues
idnameteam_idcurrent_rep_indexorlast_assigned_rep_idupdated_at
queue_members
queue_idrep_idpositionactive
lead_assignments
lead_idrep_idqueue_idassigned_atreason
3) Use a deterministic assignment algorithm
A basic round-robin algorithm:
- Get the list of eligible reps in order
- Start from the current pointer
- Move forward until you find an eligible rep
- Assign the lead
- Update the pointer to the next position
Pseudo-code:
def assign_lead(queue_id, lead):
reps = get_active_reps(queue_id) # ordered list
if not reps:
raise Exception("No eligible reps")
pointer = get_queue_pointer(queue_id) # index of next rep
for offset in range(len(reps)):
idx = (pointer + offset) % len(reps)
rep = reps[idx]
if is_eligible(rep, lead):
create_assignment(lead, rep, queue_id)
update_queue_pointer(queue_id, (idx + 1) % len(reps))
return rep
raise Exception("No eligible reps found")
4) Handle concurrency correctly
This is the part people usually miss.
If two leads arrive at the same time, both could get assigned to the same rep unless you lock the queue.
Ways to handle this:
- Database row lock on the queue record
- Transaction with serializable/for-update locking
- Atomic increment if using a counter-based pointer
- Distributed lock if multiple services/processes assign leads
Recommended approach:
- wrap assignment in a DB transaction
- lock the queue row
FOR UPDATE - read pointer, assign, update pointer, commit
That keeps assignments consistent.
5) Support skip logic
Often you need to skip reps based on:
- out of office
- exceeded daily cap
- not licensed for region
- not trained on product
- closed status or temporary pause
Build a function like:
def is_eligible(rep, lead):
return (
rep.active
and not on_vacation(rep)
and open_leads(rep) < rep.max_open_leads
and matches_region(rep, lead)
and matches_product(rep, lead)
)
6) Decide what happens when no one is eligible
Options:
- leave unassigned
- assign to a fallback queue
- notify a manager
- place in a retry queue
- assign to a team inbox
For example:
- if all reps are at capacity, put the lead into a “waiting” status
- retry every few minutes or after capacity frees up
7) Consider weighted round robin
If senior reps should get more leads, use weights.
Example:
- Rep A weight 3
- Rep B weight 2
- Rep C weight 1
You can implement this by:
- expanding the rotation list proportionally, or
- maintaining a score system, or
- using smooth weighted round robin
For most sales systems, a simple weighted list is enough unless the team is large.
8) Make it auditable
Sales ops will eventually ask:
- Why did this lead go to that rep?
- Why did someone get skipped?
- Was the assignment fair?
Store:
- assigned rep
- queue used
- timestamp
- eligibility checks or skip reason
- pointer before/after
This makes troubleshooting much easier.
9) Add admin controls
You’ll want tools for:
- adding/removing reps from queues
- pausing a rep
- changing weights
- changing queue order
- viewing assignment stats
- manually reassigning leads
10) Test edge cases
Make sure you test:
- one rep in queue
- no reps in queue
- rep removed mid-rotation
- multiple leads assigned simultaneously
- rep becomes inactive during assignment
- all reps skipped due to capacity
- queue order changes
Example simple implementation strategy
If you’re building this in a web app with a SQL database:
- Store queue membership and pointer in the DB
- When a lead arrives:
- start transaction
- lock queue row
- fetch eligible reps in order
- select next eligible rep
- create assignment
- update pointer
- commit
- Log assignment details
That’s usually enough for a reliable first version.
If you want a more scalable design
For high-volume systems:
- put new leads into a queue
- have an assignment worker process them
- use a database or Redis lock
- ensure idempotency so the same lead isn’t assigned twice
If you want, I can also give you:
- a database schema
- a SQL implementation
- a Python/Node.js example
- a weighted round-robin version