Admin tries to mass-update 80,000 contacts with a new region code. Hits the UI mass-update cap at 50,000. Splits into two batches. The second batch errors out partway. They retry. The retry double-updates 12,000 records. Reports are wrong for a day. Customer success calls in confused. This is a story I’ve watched four times. The fix is the same every time.
Mass update at scale is a different discipline than mass update of 500 rows. Treat it like a backfill, not a button click.
The native limits worth knowing
- UI mass update: caps at 50,000 records per operation (subject to your edition).
- Bulk Update API: 100 records per call, up to 25,000 records per job in some editions, with per-day org credit costs.
- Daily API credit ceiling: shared across all integrations. Mass updates can starve other systems.
- Workflow triggers on mass update: configurable. If on, every updated record fires workflows. If you update 80k, you fire 80k workflows. Plan accordingly.
That last one is the killer. People forget to disable trigger-on-mass-update and discover the cascade two hours later when their assignment rules have reassigned 50k records to the wrong owners.
Pre-flight checklist
Before any mass update over 5,000 records:
- Snapshot the affected records to CSV. If something goes wrong, you have rollback data.
- Confirm whether workflow rules should fire on this update. Disable explicitly if not.
- Confirm whether validation rules might reject the update. Run a sample of 100 first.
- Check API credits remaining today. Don’t start a 50k update with 20k credits left.
- Communicate to revops + integrations team. Inbound webhooks may overlap.
- Schedule for off-hours — weekday 11 PM org time is best. Avoid month-end, quarter-end, fiscal close.
If any item is unchecked, don’t start. The rollback is harder than the wait.
The chunking pattern
Don’t push the cap. Process in chunks of 500–1,000, with deliberate spacing.
// scheduled_mass_update_driver
// Reads from a queue table, processes 1000 per run, 5-minute cadence
batch_size = 1000;
pending = zoho.crm.searchRecords(
"Mass_Update_Queue",
"(Status:equals:pending)",
1, batch_size
);
if(pending.size() == 0) { return; }
// Build bulk update payload (chunks of 100 per API call)
updates_payload = List();
ids_in_batch = List();
for each row in pending
{
target_id = row.get("Target_Record_Id");
new_value = row.get("New_Value");
updates_payload.add({
"id": target_id,
"Region_Code": new_value
});
ids_in_batch.add(row.get("id"));
// Flush every 100
if(updates_payload.size() == 100)
{
result = zoho.crm.bulkUpdate("Contacts", updates_payload);
// Mark queue rows done or failed
for(i = 0; i < ids_in_batch.size(); i = i + 1)
{
ok = result.get(i.toString()).get("status") == "success";
zoho.crm.updateRecord("Mass_Update_Queue", ids_in_batch.get(i), {
"Status": ok ? "done" : "failed",
"Processed_At": zoho.currenttime,
"Error": ok ? null : result.get(i.toString()).get("message")
});
}
updates_payload.clear();
ids_in_batch.clear();
// Brief pause between chunks — prevents burst credit drain
thread.sleep(1000);
}
}
// Flush remainder
if(updates_payload.size() > 0)
{
// ... same pattern
}
This pattern handles:
- Resumable: if the function dies, the queue still has pending rows. Next run picks up.
- Idempotent: a row marked
donewon’t be reprocessed. - Auditable: every row has a processed_at timestamp and error message if applicable.
- Throttled: 1-second pause between 100-record chunks. Not optimal for speed but kind to the org.
Loading the queue
Don’t build the update list in memory. Write it to the queue table first. Then drain.
// load_queue
// Run once, manually, after planning
target_set = zoho.crm.searchRecords(
"Contacts",
"(Country:equals:Singapore)and(Region_Code:equals:null)",
1, 100000
);
for each c in target_set
{
zoho.crm.createRecord("Mass_Update_Queue", {
"Target_Module": "Contacts",
"Target_Record_Id": c.get("id"),
"Field": "Region_Code",
"Old_Value": "",
"New_Value": "APAC_SG",
"Status": "pending",
"Created_At": zoho.currenttime
});
}
Now the scheduled driver drains over hours instead of trying to do it all at once. Org load stays flat. Other systems keep working.
Disabling workflows for the duration
If the update should not fire workflows, you have two options:
- Temporarily disable the workflow rules that match. Note them in a Cliq message so you remember to re-enable.
- Update via API with
trigger=workflow:falseif supported on your edition. Cleaner but less obviously visible.
Option 1 is more auditable. Use it for big runs.
Back-pressure: knowing when to slow down
The driver should respect org-wide pressure. If API credit burn is high or function error rate spikes, slow down.
// check before each batch
credits_remaining = zoho.crm.getOrgAttribute("api_credits_remaining_today");
if(credits_remaining < 5000)
{
zoho.cliq.postToChannel("revops-alerts", {
"text": "Mass update driver pausing — only " + credits_remaining.toString() + " credits left today."
});
return; // Resume tomorrow
}
You’ll need to track credits remaining yourself or via the API’s response headers. Don’t trust a fixed budget — what’s spent depends on the day.
Rollback plan
You snapshotted the affected records to CSV. If something goes wrong:
- Stop the driver immediately (disable the schedule).
- Identify the affected range — queue rows marked
doneafter the time the problem started. - Load a rollback queue with the old values from your CSV.
- Run the driver against the rollback queue.
Don’t try to manually fix individual records. You’ll miss some. The same machinery you used to break it can fix it.
Common failure modes
- Validation rules fail mid-batch. The API returns success/failure per record. Track per record. Don’t assume batch-level success.
- Cascading workflows update other records. Your 80k update becomes 200k actions. Watch credits closely or disable workflows.
- Duplicate runs. The queue’s
Statusfield is your idempotency guard. Trust it. - Time-zone display drift. If you’re updating date fields, set them as ISO strings, not display strings. Avoid implicit zone conversion.
- Picklist values not in the master list. The update fails silently for those records. Pre-validate the new values against the picklist.
When to escalate to a real ETL
If you find yourself doing 100k+ mass updates more than once a quarter, push the data manipulation upstream. Run it in Analytics or Catalyst or even a Python script that bulks via API. CRM is for the resulting writes, not the computation.
For the broader API discipline this sits on, see Zoho Deluge rate limit survival guide. For the broader automation context, see Zoho CRM automation deep dive.
Bottom line
Big mass updates are backfills, not button presses. Snapshot first. Queue the work. Drain in chunks of 100 with deliberate pauses. Disable workflows if cascades aren’t wanted. Watch API credits. Make every queue row idempotent. Have a CSV rollback ready before you start. The hour you spend setting this up saves a day of cleanup and a week of trust rebuilding.