top of page
codestringers-logo-header.webp

HOW TO EXPLORE FIT

See whether we're the right partner — before you commit to anything.

No-Risk Discovery is a short, practical conversation that gets you a clear view of your options — with no obligation to keep working with us.

We Learned These 7 Zoho Deluge Lessons the Hard Way So You Don’t Have To

  • Jun 20
  • 10 min read

Updated: Jun 22


When we started building our BrokerageOS solution for commercial real estate brokers, I knew we would learn a lot about the brokerage industry. That was the point. We had to understand how brokers manage sellers, buyers, listings, NDAs, CIMs, deal rooms, due diligence, purchase agreements, and all the other moving parts that make commercial real estate brokerage much more complicated than it looks from the outside.

What I did not fully appreciate was that we were also signing up for a fairly intense course in Zoho Deluge. Not the neat tutorial version where someone updates a CRM field in eight lines of code and then calmly closes their laptop. I mean the real version, where you are connecting Zoho CRM, Zoho Writer, Zoho Sign, WorkDrive, custom modules, blueprints, buttons, scheduled functions, and workflows, and then trying to figure out why something that worked yesterday is now returning a response that looks like it came from a vending machine having a stroke.

This article is not meant to be a comprehensive Deluge tutorial. It is more like the list I wish someone had handed us before we started building something serious inside Zoho CRM. If you are a developer building real Zoho CRM custom functions, here are seven mistakes we can probably help you avoid.

1. Assuming Deluge knows what you meant

The first mistake is assuming Deluge will interpret data the way a normal human being would. It will not. Deluge is extremely literal, especially when you are dealing with lookup fields, null values, empty strings, booleans, maps, lists, and CRM API responses.

In BrokerageOS, one of our core features is called “Waiting On.” The idea is simple: if a deal is stuck, the system should tell the broker what is blocking progress and who is responsible for the next action. In English, that is easy. In Deluge, it means looking at the transaction stage, related checklist items, contracts, signature statuses, tasks, buyers, sellers, and sometimes attorneys. One missing lookup or unexpected null can wreck the whole thing.

What we thought would work:

deal = zoho.crm.getRecordById("Transactions", transactionId);
sellerId = deal.get("Seller").get("id");

if(deal.get("Stage") == "Due Diligence")
{
    // calculate waiting-on logic
}

That looks fine until Seller is null, or the lookup field does not return the shape you expected, or Stage is blank, or the record is missing because the function was called with a bad ID. Then your clean little function becomes a tiny production incident.

What actually worked better:

deal = zoho.crm.getRecordById("Transactions", transactionId);

if(deal == null || deal.isEmpty())
{
    info "No transaction found for ID: " + transactionId;
    return;
}

stage = ifnull(deal.get("Stage"),"");

sellerId = "";
sellerLookup = deal.get("Seller");

if(sellerLookup != null && sellerLookup.get("id") != null)
{
    sellerId = sellerLookup.get("id").toString();
}

if(stage == "Due Diligence")
{
    // calculate waiting-on logic safely
}
else
{
    info "Skipping Waiting On calculation. Stage is: " + stage;
}

The lesson is not that every function needs to become 900 lines of defensive sludge. The lesson is that Deluge functions should assume that CRM data will be incomplete, oddly shaped, or just not there. If the function is important, validate the inputs before you do anything clever.

2. Trusting searchRecords too much

This one hurt. In theory, zoho.crm.searchRecords should let you write a nice search criteria string and get exactly the records you need. Sometimes it does. But when you start dealing with lookup fields, complex criteria, multiple conditions, or OR logic, things can quickly become brittle.

For example, in BrokerageOS we needed to check whether certain related records already existed. Did this potential buyer already exist for this transaction? Did this person already have a signed NDA? Did this checklist item already create a task? The theoretical answer was to write precise search criteria. The practical answer was often to retrieve records and filter them ourselves.

What we thought would work:

criteria = "(Transaction:equals:" + transactionId + ") and (Person:equals:" + personId + ")";
matches = zoho.crm.searchRecords("Potential_Buyers", criteria);

if(matches.size() == 0)
{
    // create potential buyer
}

Sometimes this kind of pattern works. Sometimes it does not, especially if the field is a lookup and Deluge/CRM does not interpret the criteria the way you expect. Then you spend an hour wondering whether your field name is wrong, your ID is wrong, your criteria syntax is wrong, or Zoho is just in one of those moods.

What actually worked better in tricky cases:

page = 1;
perPage = 200;
found = false;

while(page <= 10 && found == false)
{
    buyers = zoho.crm.getRecords("Potential_Buyers", page, perPage);

    if(buyers == null || buyers.isEmpty())
    {
        break;
    }

    for each buyer in buyers
    {
        buyerTransactionId = "";
        transactionLookup = buyer.get("Transaction");

        if(transactionLookup != null && transactionLookup.get("id") != null)
        {
            buyerTransactionId = transactionLookup.get("id").toString();
        }

        buyerPersonId = "";
        personLookup = buyer.get("Person");

        if(personLookup != null && personLookup.get("id") != null)
        {
            buyerPersonId = personLookup.get("id").toString();
        }

        if(buyerTransactionId == transactionId.toString() && buyerPersonId == personId.toString())
        {
            found = true;
            break;
        }
    }

    page = page + 1;
}

if(found == false)
{
    // create potential buyer
}

Is this more verbose? Yes. Does it offend my aesthetic preferences? Also yes. But it is easier to debug, easier to log, and more predictable when CRM search criteria start acting weird. The broader rule is simple: use searchRecords when the criteria are simple and reliable. When the criteria get complex, stop trying to win a beauty contest. Pull the records and filter them intentionally.

3. Hardcoding values you will regret later

Every developer knows hardcoding is bad, and every developer does it anyway when they are trying to make something work before lunch. In Zoho projects, this can get ugly quickly because so many values are environment-specific: template IDs, folder IDs, connection names, email addresses, document types, signer roles, and module-specific configuration.

In BrokerageOS, we had document workflows for NDAs, listing agreements, LOIs, purchase agreements, and other transaction documents. At first, it is very tempting to write the function around the template you are testing. That is fine for a proof of concept. It is a terrible idea for a product.

What we thought would work:

templateId = "abc123456789";
fromEmail = "[email protected]";
workDriveFolderId = "xyz987654321";

mergeData = Map();
mergeData.put("Seller_Name", sellerName);
mergeData.put("Business_Name", businessName);

// generate document using hardcoded template

This works right up until you have a second client, a second template, a different sender address, a new folder structure, or a production environment. Then line 287 contains some old demo value, and nobody remembers why.

What actually worked better:

settingsList = zoho.crm.getRecords("System_Settings", 1, 1);
settings = settingsList.get(0);

defaultFromEmail = ifnull(settings.get("Default_From_Email"),"");
rootFolderId = ifnull(settings.get("WorkDrive_Root_Folder_ID"),"");

criteria = "(Document_Type:equals:NDA)";
docDefs = zoho.crm.searchRecords("Document_Definitions", criteria);

if(docDefs == null || docDefs.isEmpty())
{
    info "No Document Definition found for NDA";
    return;
}

docDef = docDefs.get(0);
templateId = ifnull(docDef.get("Writer_Template_ID"),"");
requiresSignature = ifnull(docDef.get("Requires_Signature"),false);

The better pattern is to move client-specific and document-specific rules into configuration records. Deluge should execute the process, not become the permanent storage location for every business rule you were too lazy to model properly. That sounds harsh, but I am mostly yelling at past-us.

4. Treating Zoho Writer merge fields like regular CRM fields

Zoho Writer merge automation was one of those areas where the concept sounded simple and the implementation had opinions. If you are using Deluge to send merge data into Writer, the fields in the Writer template need to match the payload you are sending. That is not the same thing as assuming Writer will magically understand every CRM field name you type into the document.

This matters when you are generating documents like NDAs, listing agreements, LOIs, or purchase agreements. The user sees a template and thinks, “I’ll just put the field here.” The developer sees a payload and thinks, “Writer should obviously map this.” Writer, meanwhile, is quietly preparing to disappoint both of you.

What we thought would work:

mergeData = Map();
mergeData.put("Deal_Name", deal.get("Deal_Name"));
mergeData.put("Seller_Name", deal.get("Seller").get("name"));
mergeData.put("Purchase_Price", deal.get("Purchase_Price"));

// assume Writer template fields match these names
response = zoho.writer.mergeAndSend(templateId, mergeData);

The problem is that the Writer template may not have fields named Deal_Name, Seller_Name, or Purchase_Price in the way your payload expects. Maybe someone manually typed merge-field-looking text into the template. Maybe the field was inserted from a different data source. Maybe the role fields for signing do not match the document definition. Any of these can lead to a document that generates incorrectly or fails in a way that is not immediately obvious.

What actually worked better:

mergeData = Map();
mergeData.put("transaction_name", transactionName);
mergeData.put("seller_legal_name", sellerLegalName);
mergeData.put("buyer_legal_name", buyerLegalName);
mergeData.put("business_name", businessName);
mergeData.put("effective_date", effectiveDate);

signerData = List();

sellerSigner = Map();
sellerSigner.put("recipient_name", sellerLegalName);
sellerSigner.put("recipient_email", sellerEmail);
sellerSigner.put("role", "Seller");
signerData.add(sellerSigner);

buyerSigner = Map();
buyerSigner.put("recipient_name", buyerLegalName);
buyerSigner.put("recipient_email", buyerEmail);
buyerSigner.put("role", "Buyer");
signerData.add(buyerSigner);

// send payload that matches the Writer template's imported merge fields

The key is to design the payload first, import or map those fields properly in Writer, and then treat the payload contract as sacred. Do not manually type fake merge fields into a Writer template and hope for the best. Hope is not a document automation strategy.

5. Forgetting that automations can fire more than once

Zoho CRM automation can be incredibly useful, but it is very easy to accidentally create duplicates if your functions are not idempotent. That is a fancy developer word meaning the function can run more than once without creating a mess.

BrokerageOS has extensive automation for checklist items, tasks, buyer matches, potential buyers, contracts, and stage changes. A transaction entering a stage might create checklist items. A checklist item might create a task. Updating that task might update the checklist item. Updating the checklist item might trigger another workflow. You can see where this is going.

What we thought would work:

taskMap = Map();
taskMap.put("Subject", checklistItem.get("Name"));
taskMap.put("What_Id", transactionId);
taskMap.put("$se_module", "Transactions");
taskMap.put("Status", "Not Started");

createResp = zoho.crm.createRecord("Tasks", taskMap);

This creates a task. Great. It may also create the same task again tomorrow when the workflow fires again because something related has changed. Now the user has three identical tasks and begins to lose faith in your competence, which is rude but not entirely unfair.

What actually worked better:

existingTaskId = ifnull(checklistItem.get("Related_Task_ID"),"");

taskMap = Map();
taskMap.put("Subject", checklistItem.get("Name"));
taskMap.put("What_Id", transactionId);
taskMap.put("$se_module", "Transactions");
taskMap.put("Status", "Not Started");

if(existingTaskId != "")
{
    updateResp = zoho.crm.updateRecord("Tasks", existingTaskId, taskMap);
    info "Updated existing task: " + existingTaskId;
}
else
{
    createResp = zoho.crm.createRecord("Tasks", taskMap);
    newTaskId = createResp.get("id");

    updateChecklist = Map();
    updateChecklist.put("Related_Task_ID", newTaskId);
    zoho.crm.updateRecord("Checklist_Items", checklistItemId, updateChecklist);

    info "Created new task: " + newTaskId;
}

The pattern is simple: before creating a record, check whether the record already exists or whether the source record already has a related ID. If it exists, update it. If it does not, create it and store the relationship. This is not glamorous, but it is the difference between automation and automated littering.

6. Waiting too long to build real error logs

For early development, info statements are fine. In fact, they are indispensable. But once you have button, workflow, scheduled, Writer, Sign, and WorkDrive functions, info statements alone are not enough. You need persistent logging.

In BrokerageOS, we eventually added an Automation Error Logs module. The point was not to create more CRM clutter. The point was to have a place where important failures could be stored with enough context to debug them later.

What we thought would work:

try
{
    // do important automation
}
catch(e)
{
    info e;
}

That is better than nothing, but only barely. It helps the developer who happens to be watching the execution at the right moment. It does not help the admin who finds the problem tomorrow. It does not help to support understanding which record failed. It does not help you see whether the same function has failed ten times this week.

What actually worked better:

try
{
    // do important automation
}
catch(e)
{
    errorMap = Map();
    errorMap.put("Name", "Contract Signature Sync Failed");
    errorMap.put("Source_Function", "fn_sync_contract_signature_status");
    errorMap.put("Related_Module", "Contracts");
    errorMap.put("Related_Record_ID", contractId.toString());
    errorMap.put("Severity", "High");
    errorMap.put("Error_Message", e.toString());

    details = Map();
    details.put("contract_id", contractId);
    details.put("transaction_id", transactionId);
    details.put("timestamp", zoho.currenttime.toString());

    errorMap.put("Details_JSON", details.toString());

    zoho.crm.createRecord("Automation_Error_Logs", errorMap);
    info "Logged automation error for contract: " + contractId;
}

You do not need to log every little thing forever. But if a function affects documents, signatures, deal rooms, stage progression, or anything the client will actually notice, give yourself a durable error trail. Future you will be tired and appreciate it.

7. Trying to make Deluge do too much

This is the architectural mistake. Deluge is very good at coordinating Zoho. It can update CRM records, create tasks, call APIs, trigger document workflows, move data between modules, and keep related records in sync. That makes it perfect for a lot of business automation.

But there is a point where a Deluge function becomes too clever for its own good. You know the function I mean. It loads seven modules, calculates three statuses, creates two records, updates five others, calls an external API, handles six branches, logs errors inconsistently, and contains one comment from three weeks ago that says // temporary fix.

We have all written that function. We are not proud of it.

What we thought would work:

// One giant function to process everything
deal = zoho.crm.getRecordById("Transactions", transactionId);

// calculate stage guidance
// create checklist items
// create tasks
// generate documents
// send for signature
// create folders
// grant deal room access
// update buyer status
// calculate waiting-on
// update dashboards

This seems efficient at first because everything is in one place. Then something fails halfway through, and you do not know which part created which side effect. Or you need to reuse a piece of logic elsewhere. Or a client wants to customize one part of the process but not the rest. Suddenly, “one function to rule them all” becomes “one function to ruin your afternoon.”

What actually worked better:

// Button or workflow entry point
deal = zoho.crm.getRecordById("Transactions", transactionId);

if(deal == null || deal.isEmpty())
{
    info "No transaction found.";
    return;
}

// Keep orchestration readable
setupChecklistResp = thisapp.fn_create_stage_checklist(transactionId);
docStatusResp = thisapp.fn_prepare_required_documents(transactionId);
buyerStatusResp = thisapp.fn_update_buyer_pipeline_status(transactionId);
waitingOnResp = thisapp.fn_calculate_waiting_on(transactionId);

info "Stage processing completed for transaction: " + transactionId;

The point is not that every function has to be tiny.

The point is that each function should have a job that a normal person can explain without taking a drink of water in the middle.

Load configuration. Create checklist items. Sync task status. Generate document. Calculate Waiting On. Those are jobs. “Process the entire deal universe” is not a job. It is a cry for help.

What we learned

The biggest Deluge lesson we learned from building BrokerageOS is that the language itself is not the hard part. The hard part is building reliable business logic within a living CRM, where users, workflows, related records, integrations, and scheduled functions are all changing data simultaneously.

For developers, the practical lessons are pretty clear:

  • Validate lookup fields before using them.

  • Normalize nulls, booleans, IDs, and strings.

  • Use searchRecords carefully, especially with complex criteria.

  • Move client-specific rules into configuration records.

  • Treat Writer merge payloads as a real contract.

  • Make automations idempotent.

  • Build persistent error logs before you desperately need them.

  • Keep Deluge functions boring, focused, and easy to debug.

BrokerageOS ended up much stronger because we learned these lessons. It is now a Zoho-powered transaction operating system for managing sellers, buyers, CIMs, NDAs, signatures, deal rooms, due diligence, purchase agreements, and closing workflows. But getting there required a lot of trial and error, a lot of weird Deluge responses, and more than a few moments where we stared at the screen wondering whether we had offended Zoho personally.

So if you are building serious Zoho CRM custom functions, our advice is simple: assume the data will be messy, assume the automation will run twice, assume the template field is not mapped correctly, and assume future you will need better logs.

Future you deserves nice things.

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating

Recent Posts

bottom of page