Hello everyone,
During CRM implementations, data cleanup is a common task, especially after testing, migrations, imports, or integration development.
The Zoho CRM UI allows deleting records in batches of 100, which is not practical when dealing with thousands of records.
I created a reusable Deluge function that performs bulk deletion using the Zoho CRM API.
This avoids manually deleting records from the CRM UI and can be reused for different modules by changing the module API name.
/* CONFIRM before running: DC (.eu here) · module API name · connection scopes:
ZohoCRM.coql.READ + delete scope for the target module
· context: standalone function; for very large volumes run as a scheduled function.
· WARNING: "where id is not null" drains the ENTIRE module — replace with a
real filter (Created_Time, tag, custom field) unless a full wipe is intended.
Deleted records land in the Recycle Bin. */
// ---- Config ----
apiDomain = "https://www.zohoapis.eu";
moduleApiName = "Accounts";
batchSize = 100;
// ---- State ----
totalDeleted = 0;
totalFailed = 0;
hasMoreRecords = true;
// Bounded loop = max 5 pages x 2000 records = 10,000 deletions per execution.
// Add more iterations or reschedule the function for bigger cleanups.
for each pageIteration in {1,2,3,4,5}
{
if(hasMoreRecords)
{
// Offset stays 0 on purpose: every pass deletes what it fetched,
// so the next page of surviving records always starts at the top.
// Incrementing the offset while deleting would SKIP records.
coqlPayload = Map();
coqlPayload.put("select_query","select id from " + moduleApiName + " where id is not null limit 2000");
coqlResponse = invokeurl
[
url : apiDomain + "/crm/v8/coql"
type : POST
parameters : coqlPayload.toString()
headers : {"Content-Type":"application/json"}
connection : "crmfullaccess"
];
records = ifnull(coqlResponse.get("data"),List());
if(records.size() == 0)
{
hasMoreRecords = false;
}
else
{
hasMoreRecords = ifnull(coqlResponse.get("info"),Map()).get("more_records") == true;
// Split IDs into batches of 100 (bulk delete API maximum)
batches = List();
currentBatch = List();
for each record in records
{
currentBatch.add(record.get("id"));
if(currentBatch.size() == batchSize)
{
batches.add(currentBatch);
currentBatch = List();
}
}
if(currentBatch.size() > 0)
{
batches.add(currentBatch);
}
// Progress guard: if nothing in this page gets deleted, stop instead
// of re-fetching the same undeletable records on the next iteration.
deletedBeforePage = totalDeleted;
for each batch in batches
{
deleteResponse = invokeurl
[
url : apiDomain + "/crm/v8/" + moduleApiName + "?ids=" + batch.toString(",") + "&wf_trigger=false"
type : DELETE
connection : "crmfullaccess"
];
deleteResults = ifnull(deleteResponse.get("data"),List());
if(deleteResults.size() == 0)
{
// Whole batch rejected (auth/permission/limit) — log once and count
totalFailed = totalFailed + batch.size();
info "Batch delete failed: " + deleteResponse;
}
else
{
// Bulk delete returns per-record status — count each individually
for each result in deleteResults
{
if(ifnull(result.get("status"),"") == "success")
{
totalDeleted = totalDeleted + 1;
}
else
{
totalFailed = totalFailed + 1;
info "Delete failed for " + ifnull(result.get("details"),Map()).get("id") + ": " + ifnull(result.get("message"),"");
}
}
}
}
if(totalDeleted == deletedBeforePage)
{
info "No progress on this page — stopping to avoid re-fetching undeletable records.";
hasMoreRecords = false;
}
}
}
}
info "Finished. Deleted: " + totalDeleted + " · Failed: " + totalFailed;
Required connection scopes:
ZohoCRM.coql.READ
Notes for production usage:
This pattern can be useful for:
Sharing in case it helps other developers who need to perform bulk operations in Zoho CRM.