Bulk deleting Zoho CRM records using Deluge, COQL and CRM API

Bulk deleting Zoho CRM records using Deluge, COQL and CRM API

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.


The approach:
  1. Retrieve record IDs using COQL with pagination.
  2. Split the IDs into batches of 100.
  3. Execute the CRM bulk delete API.
  4. Process the response and track successful and failed deletions.

This avoids manually deleting records from the CRM UI and can be reused for different modules by changing the module API name.


Example:

/* 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
  • Delete permission for the target module

Notes for production usage:

  • COQL pagination should be used for large datasets. The example processes records in pages instead of trying to retrieve everything at once.
  • The CRM bulk delete API supports multiple IDs per request, so deleting 100 records per API call significantly reduces the number of requests.
  • If the cleanup operation involves very large datasets and you hit Deluge execution limits, consider:
    • Running the same logic through a scheduled function.
    • Implementing the process externally (for example Node.js) where you can control delays, retries, and execution flow more easily.

This pattern can be useful for:

  • Removing test data after implementations
  • Cleaning integration/staging modules
  • Post-migration cleanup
  • Managing temporary records

Sharing in case it helps other developers who need to perform bulk operations in Zoho CRM.