Leads flowing into a circular round-robin queue and being dealt evenly to three team members, with a marker showing where the rotation stopped.
CRM/AI Operations

Round-Robin Lead Assignment in Zoho CRM, Without the Restart-at-the-Top Problem

Zoho CRM's built-in assignment rules do round robin on paper, but the rotation position is not documented as persistent and there is no assignment log. I built a Deluge replacement with a persistent pointer, per-member eligibility checks, and a log that proves why every lead went where it went.

Disclosure: I build Zoho systems for a living, so I have a commercial interest in this topic. The system below is a sample I built in my own Zoho org with synthetic data — not client work — and I say plainly what I did not verify.

The short answer: Zoho CRM's assignment rules deal leads round robin, but the rotation position is not documented as persistent, hand-created records are not covered, and nothing logs why a lead went where it went. I built a Deluge replacement around three ideas — a pointer stored in a record, a claim stamped on the lead before assignment, and an eligibility snapshot saved with every assignment — and tested it against a 23-case plan, including the cases it failed first and I had to fix.

Zoho CRM has assignment rules built in. On paper they do round robin: incoming leads are dealt to a list of users in order. In practice, teams keep rebuilding this in Deluge. After checking the official documentation against the complaints, I understood why - and built the version I would want to defend in front of a client.

This is what the built-in rules actually do, what they do not do, and the design of a replacement that keeps a persistent pointer, skips people who are away, and can prove afterwards why every lead went where it went.

What the standard assignment rules do not promise

Four gaps, each checked against Zoho's own docs or the behaviour they describe there:

  • The rotation position is not documented as persistent. Nothing in the official documentation says the pointer survives between imports, and community reports describe the order restarting from the first user on each import. If that is your intake pattern, the people at the top of the list quietly collect more leads.
  • The trigger coverage is narrow. Zoho's FAQ is explicit: assignment rules fire for records coming in through import, web forms, and APIs. Records a rep creates by hand, or a workflow creates, are not covered.
  • The fallback is a single default user. When nobody in the rule is available, everything lands on one person, and nothing records why.
  • There is no assignment log. A week later, "why did this lead go to her?" has no answer beyond memory.

None of this makes the built-in feature bad. It makes it a default - fine until distribution fairness is something you have to show, not just believe.

The design: pointer, claim, snapshot

The replacement is three Deluge functions and four small custom modules. It runs on a create-workflow, so it fires for every new lead, however the lead was created.

The pointer lives in a record, not in the code. Each pool (a group of reps selected by, say, a territory field) has one state row holding the last sequence number used. The update that advances it is conditional - it carries the record's last-modified timestamp, and a stale write comes back as HTTP 412 instead of overwriting a newer value. The pointer is just data: it survives imports, batches, and days. In testing, the rotation continued 2 -> 3 -> 1 across an overnight gap without any reset.

Every assignment starts with a claim on the lead itself. The engine first stamps the lead with a unique operation ID and a "processing" status. If a second trigger fires for the same lead - a workflow re-run, an impatient button click - it sees the claim and exits as "already assigned", writing a log row that says so. Duplicate triggers are normal in CRM automation; the design treats them as expected, not as errors.

Eligibility is evaluated member by member, and the evaluation is kept. A member can be skipped for being inactive, not accepting new leads, absent that day, outside their shift window, off that weekday, or at their daily cap - or because their CRM user is no longer active. The log row stores a snapshot of the whole evaluation, in rotation order: 2:Shift,3:OK,1:OK. That one string answers most "why did this lead go there" questions a month later.

The fallback is a decision, not an accident. If nobody is eligible, the lead goes to the pool's configured fallback user and the log says Fallback. If no fallback is configured either, the lead is marked unassigned with the reason - never silently dropped, never dumped on whoever happens to be first in the list.

A detail-page button re-runs the engine for one lead, with a 5-minute debounce so it cannot be used to spin the rotation.

Zoho CRM assignment log list: every assignment recorded with its result, sequence and pool Every assignment is logged with its result, the sequence used and the pool it came from.

Assignment log detail for one lead: Result Assigned, Sequence_Used 3, Pool_Key kanto One log record in detail: the outcome, the pointer position used, and the eligibility snapshot taken at assignment time.

What I verified, and what I did not

I tested this against a 23-case plan in my own org. Rotation order and wrap-around, continuation across batches and days, territory routing with a default pool, every skip reason, fallback with and without a fallback user, duplicate execution, a member row deleted mid-rotation, missing and duplicated state rows (both stop cleanly with a configuration error instead of mis-assigning), duplicate sequence numbers (also a configuration error - this one failed on the first run and I fixed the engine), and shift boundaries on both sides: a shift starting exactly now counts as in, a shift ending exactly now counts as out.

Two honest limits:

  • My org has one user. Every member row pointed at me, so rotation is proven at the level of sequences, pointers, and logs - the owner update and read-back ran on every assignment, but I did not watch leads land with three different people. Treat the multi-user behaviour as designed and sequence-tested, not as observed in a live team.
  • I did not run a true parallel burst. The conditional update the design relies on is verified directly (a stale write returns 412), and sequential bursts cycle cleanly, but simultaneous claims were not reproduced.

Three Deluge gotchas this build surfaced

  • zoho.currenttime is not your org's time. In my org it returned a wall clock 17 hours behind Tokyo time, and the offset moves with US daylight saving. Any shift or absence logic built on the raw value is wrong in a way that looks right. The engine corrects it explicitly; check yours before copying anything.
  • zoho.crm.deleteRecord does not exist as a built-in (neither does zoho.crm.getUsers). Code written from general API habits fails at save time. Deletes go through invokeurl, and user state comes from the Users API.
  • A doubled backslash in a replaceAll regex compiles fine and throws at runtime. Single backslash is what the editor actually runs.

The point

Round robin sounds like a small feature until distribution is disputed - and in sales teams, distribution is always eventually disputed. The built-in rules are a fine default. What they do not give you is a pointer you can inspect and a log you can argue from.

If your team assigns leads in Zoho CRM and the fairness of that assignment matters - across territories, shifts, or part-time schedules - this pattern is straightforward to implement, and I am happy to talk through how it would fit your setup.

The full code, setup steps and verification notes are on GitHub: zoho-crm-round-robin-assignment-sample