Java, Node.js, Python

A reference for writing Zoho CRM Functions in Java, Node.js, and Python.

 

Java, Node.js, and Python are supported languages for writing Zoho CRM Functions. Unlike Deluge, these languages do not have direct access to zoho.crm.* built-in integration tasks. Instead, they interact with CRM and external APIs through ZRC (Zoho Request Client) and receive trigger context automatically via basicIO.

This guide focuses on how these languages integrate with the CRM platform. It explains how input is received, what context is available, how to handle arguments, and how to make HTTP calls using ZRC. For general language syntax, refer to each language’s own documentation.

Creating Functions

Java, Node.js, and Python Functions can be created in two ways: writing code directly in the in-product editor, or uploading a zip file containing your code and configuration.

Draft State and Deployment

Java, Node.js, and Python Functions have a draft state. When you save changes in the editor, the code is saved as a draft and does not affect the live Function. Changes only take effect after you explicitly deploy the Function. This allows you to iterate, test, and refine your code without impacting any workflows, buttons, or schedules that depend on the Function.

Even after a Function has been deployed, you can continue making edits. Each save creates a new draft while the currently deployed version continues to run. The live Function remains unchanged until you deploy again.

Key difference from Deluge:

Deluge Functions have no draft state. Every save immediately updates the live Function, with no deployment step required. If a Deluge Function is not ready for use yet, the way to control its availability is through associations: simply do not link it to any button, workflow, or schedule until it is ready. Java, Node.js, and Python Functions can be safely edited and saved as drafts without this concern.

Zip Upload

You can script the Function in any external editor (Eclipse, VS Code, etc.) and upload the folder as a zip file to CRM. Once uploaded, the in-product editor opens where you can make further modifications.

Every zip must include a config.json in the root path that specifies the entry point.

Java

The zip must contain the Java source files and a config.json that specifies the main class name in the action parameter.

config.json:

 

{"router":[{"path":"/","action":"CreateLead"}],"connector":[],"name":"CreateLead","type":0}

Requirements:

  • The Function name, the zip file name, and the main Java file name must all be the same (case-sensitive).
  • Specify the main class name in the action param of config.json.

Node.js

The zip must contain the JS file, config.json, and optionally a node_modules folder for external libraries.

To bundle external libraries:

  1. Create a folder containing config.json, node_modules, and the JS file.
  2. Install the external libraries in the node_modules folder.
  3. Zip the folder and create a function with the zip file.

Requirements:

  • The Function name, the zip file name, and the main JS file name must all be the same (case-sensitive).
  • The Node.js editor uses Node.js 8.11, so use libraries compatible with this version.
  • External libraries can only be added via zip when creating a new function, not to existing functions.

Python

The zip must contain the Python file (FunctionName.py), config.json, and optionally a lib folder for external libraries.

Requirements:

  • The Function name, the zip file name, and the main Python file name must all be the same (case-sensitive).

General Zip Constraints

  • Only one zip file can be uploaded per function.
  • Maximum zip file size is 10MB.
  • The Function name, zip file name, and main source file name must match exactly (case-sensitive), or the system will throw an error.

Caution:

  • When compressing the folder on macOS, exclude hidden system files to prevent them from being included in the package. Use the following command:

     

    zip -r archive_name.zip folder_to_zip/ -x "*.DS_Store" -x "__MACOSX*" -x "*/._*"

  • When bundling external libraries, ensure that their versions are compatible with the language runtime version used by the CRM execution environment. Version mismatches between your development environment and the CRM runtime may cause errors during execution.

basicIO

basicIO is the standardized input/output mechanism for Java, Node.js, and Python Functions. When a Function is triggered, CRM automatically packages the relevant context and delivers it to the Function as a structured payload via basicIO. You do not need to declare arguments or configure merge variable mappings because the data is passed automatically.

Default Parameters

The following parameters are always provided through basicIO:

ParameterTypeDescription
recordsArrayEntity information, the CRM record(s) that triggered the Function
userObjectDetails of the user who triggered the action
organizationObjectOrganization details (org ID, domain, etc.)
variablesArrayCRM variable information
requestObjectRequest-specific information (see keys below)

The request object contains the remaining HTTP request details:

KeyTypeDescription
methodStringHTTP method of the request (e.g. "GET", "POST")
headersObjectHTTP headers sent with the request
paramsObjectQuery parameters from the URL
bodyStringThe raw request body
auth_typeStringAuthentication type used for the request
file_contentFile content, when the request includes a file upload

How this relates to crmAPIRequest:

In Deluge, all request data is available as a single crmAPIRequest object. In Java, Node.js, and Python, CRM precesses the crmAPIRequest and splits it into multiple parameters. The record object is extracted into the records parameter, user_info (which contains org_info) is extracted into the user and organization parameters, and the remaining data, such as (method, headers, params, body, auth_type, file_content) is available through the request parameter. See Exposing Functions as REST APIs for the complete crmAPIRequest structure.

Additional parameters based on context:

ParameterTypeAvailable When
moduleStringModule-specific information, when applicable
related_moduleStringWhen the trigger involves related records
related_recordsArrayWhen the trigger involves related records

Reading Parameters

Use basicIO.getParameter("paramName") to access each parameter. Cast to the appropriate type (JSONArray for arrays, JSONObject for objects).

Java:

 

// Entity information
JSONArray records = new JSONArray(basicIO.getParameter("records").toString());

// Organization details
JSONObject organization = new JSONObject(basicIO.getParameter("organization").toString());

// User details
JSONObject user = new JSONObject(basicIO.getParameter("user").toString());

// CRM Variables
JSONArray variables = new JSONArray(basicIO.getParameter("variables").toString());

// Request information
JSONObject request = new JSONObject(basicIO.getParameter("request").toString());

Node.js:

 

let records = basicIO.getParameter("records");
let organization = basicIO.getParameter("organization");
let user = basicIO.getParameter("user");
let variables = basicIO.getParameter("variables");
let request = basicIO.getParameter("request");

Python:

 

records = basicIO.getParameter("records")
organization = basicIO.getParameter("organization")
user = basicIO.getParameter("user")
variables = basicIO.getParameter("variables")
request = basicIO.getParameter("request")

Writing Return Values

Use basicIO.write() to set the Function's return value.

 

// Return a JSON object
basicIO.write(new JSONObject().put("records", records).put("organization", organization));

// Return a simple string
basicIO.write("Function Executed Successfully");

Key difference from Deluge:

In Deluge, you must explicitly declare each argument and map it to a CRM field. With basicIO, the entire trigger context is delivered automatically, and you can access the parameters you need using basicIO.getParameter().

The basicIO parameters records, user, organization, and variables carry the full trigger context into your Function. This section documents the structure inside each of these objects so you know exactly what fields are available.

records

An array of record objects that triggered the Function. Each record contains the module's field API names and their values. The exact fields depend on the module and layout.

Example structure (Lead record):

 

[
  {
    "id": "2303XXXXXXXXXX",
    "First_Name": "James",
    "Last_Name": "Williams",
    "Email": "james@zylker.com",
    "Phone": "+1 678 XXX XXXX",
    "Company": "Zylker",
    "Lead_Source": "Web Form",
    "Owner": {
      "id": "2303XXXXXXXXXX",
      "name": "John Smith"
    },
    "Created_Time": "2025-03-10T14:30:00+05:30",
    "Modified_Time": "2025-03-10T14:30:00+05:30"
  }
]

Accessing in Java:

 

JSONArray records = new JSONArray(basicIO.getParameter("records").toString());
JSONObject firstRecord = records.getJSONObject(0);
String leadId = firstRecord.getString("id");
String email = firstRecord.optString("Email", "");
String ownerName = firstRecord.getJSONObject("Owner").getString("name");

Accessing in Node.js:

 

const records = JSON.parse(basicIO.getParameter("records"));
const firstRecord = records[0];
const leadId = firstRecord.id;
const email = firstRecord.Email || "";
const ownerName = firstRecord.Owner?.name;

Accessing in Python:

 

import json
records = json.loads(basic_io.getParameter("records"))
first_record = records[0]
lead_id = first_record["id"]
email = first_record.get("Email", "")
owner_name = first_record["Owner"]["name"]

Note:

For Workflow Rule triggers, records typically contains one record. For bulk operations or related record triggers, it may contain multiple records.

user

An object containing details about the user who triggered the action.

Example structure:

 

{
  "id": "2303XXXXXXXXXX",
  "name": "John Smith",
  "email": "john@zylker.com",
  "language": "en_US",
  "locale": "en_US",
  "time_zone": "Asia/Kolkata",
  "country": "IN",
  "profile": {
    "id": "2303XXXXXXXXXX",
    "name": "Administrator"
  },
  "role": {
    "id": "2303XXXXXXXXXX",
    "name": "CEO"
  }
}

Accessing in Java:

 

JSONObject user = new JSONObject(basicIO.getParameter("user").toString());
String userId = user.getString("id");
String userName = user.getString("name");
String profileName = user.getJSONObject("profile").getString("name");
String timezone = user.getString("time_zone");

organization

An object containing the CRM organization details.

Example structure:

 

{
  "id": "2303XXXXXXXXXX",
  "company_name": "Zylker Corp",
  "domain": "zylker",
  "zgid": "XXXXXXXXXX",
  "primary_email": "admin@zylker.com",
  "country_code": "IN",
  "time_zone": "Asia/Kolkata",
  "currency_symbol": "$",
  "iso_code": "USD"
}

Accessing in Node.js:

 

const org = JSON.parse(basicIO.getParameter("organization"));
const orgId = org.id;
const domain = org.domain;
const timezone = org.time_zone;

variables

An array of CRM Variables (org-level and module-level) configured in the CRM org. These are the variables set up under Setup > Developer Space > CRM Variables.

Example structure:

 

[
  {
    "id": "2303XXXXXXXXXX",
    "api_name": "ERP_API_Base_URL",
    "value": "https://erp.example.com/api/v2",
    "group": "General",
    "type": "text"
  },
  {
    "id": "2303XXXXXXXXXX",
    "api_name": "Cliq_Webhook_URL",
    "value": "https://hooks.zoho.com/services/XXX",
    "group": "General",
    "type": "text"
  }
]

Accessing in Python:

 

import json
variables = json.loads(basic_io.getParameter("variables"))

# Find a specific variable by api_name
erp_url = next(
    (v["value"] for v in variables if v["api_name"] == "ERP_API_Base_URL"),
    None
)

Comparison with Deluge:

In Deluge, you access CRM variables using zoho.crm.getOrgVariable("Variable_Name"), which makes a separate API call. In Java, Node.js, and Python, the variables are included in the basicIO payload, so no additional API call is required. However, if you need to update a variable at runtime, you must use ZRC to call the CRM variables API.

Zoho Request Client (ZRC)

ZRC (Zoho Request Client) is the HTTP client library available in Java, Node.js, and Python CRM Functions. It replaces the need for external HTTP libraries or SDKs. You use ZRC to call CRM APIs, other Zoho service APIs, and external third-party APIs.

ZRC handles CRM authentication automatically. When calling CRM APIs from a CRM Function, you do not need to specify a connection or base URL because ZRC resolves them internally. For calls to other Zoho services or external APIs, you configure a RequestConfig with the base URL and if required, a connection.

Key Components

ComponentDescription
ZRCThe main class providing static HTTP methods: get, post, put, patch, delete, head, options
RequestConfigConfiguration object for base URL, headers, query params, connection, response type, and scope.
ZrcResponseResponse object containing status (HTTP code), headers, and data/body

RequestConfig

Use RequestConfig to set base URL, headers, query parameters, connection, response type, and scope for non-CRM calls or when you need custom configuration.

PropertyDescriptionDefault
baseUrlTarget service base URL. Not needed for CRM calls.(auto-resolved for CRM)
headersCustom HTTP headers as key-value pairs{}
paramsQuery parameters as key-value pairs{}
connectionConnection link name for authenticated calls to other servicesnull
responseTypeResponse format: JSON, TEXT, STREAM, ARRAY_BUFFER, BUFFERJSON
scopeCRM API scope: system or usersystem

Java:

 

RequestConfig config = new RequestConfig();
config.setBaseUrl("https://webhook.site");
config.addHeader("Custom-Header", "value");
config.setResponseType(ResponseType.JSON);
ZrcResponse resp = ZRC.get("/path", config);

Node.js:

 

let res = await ZRC.get("/path", {
    baseUrl: "https://webhook.site",
    headers: {"Custom-Header": "value"},
    responseType: "JSON"
});

Python:

 

config = RequestConfig()
config.setBaseUrl("https://webhook.site")
config.setResponseType("text")
response = ZRC.get("/path", config)

Code Samples

CRM API Calls

When calling CRM APIs from within a CRM Function, you only need the API path. You do not need to specify a base URL, connection, or authentication because ZRC handles them automatically.

Java:

 

import com.zoho.zrc.ZRC;
import com.zoho.zrc.ZrcResponse;

public class FetchLeads implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        try {
            ZrcResponse resp = ZRC.get("/crm/v8/Leads");
            basicIO.write(resp.getData());
        } catch (Exception e) {
            basicIO.write(e.getMessage());
        }
    }
}

Node.js:

 

const {ZRC} = require('zrc');

module.exports = async function (context, basicIO) {
    try {
        let res = await ZRC.get("/crm/v8/Leads");
        basicIO.write(res.data);
        context.close();
    } catch (e) {
        basicIO.write(e);
    }
}

Python:

 

from zrc import ZRC

def runner(context, basicIO):
    resp = ZRC.get("/crm/v8/Leads?fields=Last_Name")
    basicIO.write(resp.data)

CRM POST

By default, ZRC uses system scope for CRM calls. Set the scope to user when the API call should execute in the context of the logged-in user (respecting their permissions and field-level access).

Java:

 

import com.zoho.zrc.ZRC;
import com.zoho.zrc.ZrcResponse;

public class ZRC_Java implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        JSONObject body = new JSONObject(
            "{\"data\":[{\"Last_Name\":\"Boyle\"}]}"
        );
        ZrcResponse resp = ZRC.post("/crm/v8/Leads", body);
        basicIO.write(resp.getData());
    }
}

Node.js:

 

const {ZRC} = require('zrc');

module.exports = async function (context, basicIO) {
    let body = { data: [{ Last_Name: "Boyle" }] };
    const res = await ZRC.post("/crm/v8/Leads", body);
    basicIO.write(res.data);
}

Python:

 

from zrc import ZRC

def runner(context, basicIO):
    body = {"data":[{"Last_Name":"Boyle"}]}
    response = ZRC.post("/crm/v8/Leads", body)
    basicIO.write(response.getData())

Using Connections

Connections are optional when calling CRM APIs from within a CRM Function. However, they are required when calling other Zoho services or external APIs that need OAuth or API key authentication. Customers can also use connections for CRM calls if they need fine-grained scope control.

Java:

 

import com.zoho.zrc.ZRC;
import com.zoho.zrc.ZrcResponse;
import com.zoho.zrc.RequestConfig;

public class ZRC_Java implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        RequestConfig config = new RequestConfig();
        config.setConnection("CONNECTION_NAME_HERE");
        JSONObject body = new JSONObject(
            "{\"data\":[{\"Last_Name\":\"Boyle\"}]}"
        );
        ZrcResponse resp = ZRC.post("/crm/v8/Leads", body);
        basicIO.write(resp.getData());
    }
}

Node.js:

 

const {ZRC} = require('zrc');

module.exports = async function (context, basicIO) {
    let config = {connection: "CONNECTION_NAME_HERE"}
    let body = { data: [{ Last_Name: "Boyle" }] };
    const res = await ZRC.post("/crm/v8/Leads", body, config);
    basicIO.write(res.data);
}

Python:

 

from zrc import ZRC, RequestConfig

def runner(context, basicIO):
    config = RequestConfig()
    config.setConnection("CONNECTION_NAME_HERE")
    body = {"data":[{"Last_Name":"Boyle"}]}
    response = ZRC.post("/crm/v8/Leads", body, config)
    basicIO.write(response.getData())

User Scope

The API call is executed using the current user’s permissions. If the user has access to the requested resource, the call succeeds. Otherwise, the API response contains an error.

Java:

 

import com.zoho.zrc.ZRC;
import com.zoho.zrc.ZrcResponse;
import com.zoho.zrc.RequestConfig;

public class ZRC_Java implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        RequestConfig config = new RequestConfig();
        config.setScope("user");
        JSONObject body = new JSONObject(
            "{\"data\":[{\"Last_Name\":\"Boyle\"}]}"
        );
        ZrcResponse resp = ZRC.post("/crm/v8/Leads", body);
        basicIO.write(resp.getData());
    }
}

Node.js:

 

const {ZRC} = require('zrc');

module.exports = async function (context, basicIO) {
    let config = {scope: "user"}
    let body = { data: [{ Last_Name: "Boyle" }] };
    const res = await ZRC.post("/crm/v8/Leads", body, config);
    basicIO.write(res.data);
}

Python:

 

from zrc import ZRC, RequestConfig

def runner(context, basicIO):
    config = RequestConfig()
    config.setScope("user")
    body = {"data":[{"Last_Name":"Boyle"}]}
    response = ZRC.post("/crm/v8/Leads", body, config)
    basicIO.write(response.getData())

ZRC Instance

When making multiple requests with the same configuration (same base URL, headers, etc.), create a ZRC instance using ZRC.create() to avoid repeating the config.

Java:

 

import com.zoho.zrc.ZRC;
import com.zoho.zrc.ZrcResponse;
import com.zoho.zrc.RequestConfig;

public class ZRC_Java implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        RequestConfig config = new RequestConfig();
        config.setBaseUrl("DOMAIN");
        ZrcClient zrc = ZRC.create(config);
        ZrcResponse res1 = zrc.post("/PATH", "BODY");
        ZrcResponse res2 = zrc.get("/PATH");
        basicIO.write(res1.getData());
        basicIO.write(res2.getData());
    }
}

Node.js:

 

const {ZRC} = require('zrc');

module.exports = async function (context, basicIO) {
    let zrc = ZRC.create({ baseUrl: "DOMAIN", headers: {"KEY": "VALUE"} });
    let res1 = await zrc.post("/PATH", {key: "value"});
    let res2 = await zrc.get("/PATH");
    basicIO.write(res1.data);
    basicIO.write(res2.data);
}

Python:

 

from zrc import ZRC, RequestConfig

def runner(context, basicIO):
    config = RequestConfig()
    zrc = ZRC.create(config)
    res1 = zrc.get("/PATH")
    res2 = zrc.post("/PATH", "BODY")
    basicIO.write(res1.data)

FormData

Use FormData for multipart form submissions including file uploads.

Node.js:

 

const {ZRC, FormData} = require('zrc');

let form_data = new FormData();
form_data.append("name", "ANANDHAN");
form_data.append("file", "sample test", "test.txt", "text/plain");
let res = await ZRC.post("https://webhook.site/path", form_data, {responseType: "JSON"});

Python:

 

from zrc import ZRC, RequestConfig, FormData
from io import BytesIO

config = RequestConfig()
config.setBaseUrl("https://webhook.site")
form_data = FormData()
form_data.append('field1', 'value1')
form_data.append('file', BytesIO(b"Sample file content"), filename='test.txt', content_type='text/plain')
response = ZRC.post("/path", form_data, config)

Java:

 

FormData data = new FormData();
data.append("title", "foo");
data.append("body", "bar");
data.append("file", byteArray, "file.txt", MimeTypes.TEXT_PLAIN.getMimeType());
ZrcResponse<Object> response = ZRC.post("/path", data, requestConfig);

Exception Handling

ZRC throws specific exception types that you should catch for proper error handling:

ExceptionWhen Thrown
ApiErrorHTTP status code >= 300 (API returned an error)
ZrcValidationErrorInvalid request body values or invalid URL
ZrcErrorError setting up the request or parsing the response
ConnectionErrorError with Connection-based calls (auth failures, etc.)

Java:

 

try {
    ZrcResponse resp = ZRC.get("/crm/v8/Leads");
    basicIO.write(resp.getData());
} catch (ApiError | ConnectionError | ZrcError | ZrcValidationError e) {
    basicIO.write(e);
}

Node.js:

 

try {
    let res = await ZRC.get("/crm/v8/Leads");
    basicIO.write(res.data);
} catch (e) {
    basicIO.write(e);
}

Python:

 

from zrc import ZRC, ApiError, ZrcError, ZrcValidationError, ConnectionError

try:
    response = ZRC.get("/crm/v8/Leads")
    basicIO.write(response.getData())
except ApiError as e:
    basicIO.write(e)
except ZrcError as e:
    basicIO.write(e)
except ZrcValidationError as e:
    basicIO.write(e)
except ConnectionError as e:
    basicIO.write(e)

ZRC vs Deluge Integration Tasks

AspectDeluge (zoho.crm.*)ZRC (Java, Node.js, Python)
CRM record opsBuilt-in tasks: zoho.crm.createRecord, getRecordById, etc.HTTP calls via ZRC: ZRC.post("/crm/v8/Leads", body)
Auth for CRM callsAutomatic, no setupAutomatic, no connection or base URL needed
Auth for other Zoho servicesBuilt-in namespaces (zoho.books, zoho.desk, etc.)Use ZRC with a Connection and base URL
External API callsinvokeurl with ConnectionZRC with base URL and optional Connection
Data center routinginvokeAPI handles automaticallyZRC handles automatically for CRM calls

Logging

Java, Node.js, and Python Functions use context.log to write log entries. Logs are viewable in the Function Editor's Logs panel.

Java

Java provides two overloaded methods:

MethodDescription
context.log(String logData)Logs entry with the level INFO
context.log(String logData, Level level)Logs entry with a specific log level
 

import com.zoho.cloud.function.Context;
import com.zoho.cloud.function.basic.*;
import java.util.logging.Level;

public class MyFunction implements ZCFunction {
    public void runner(Context context, BasicIO basicIO) throws Exception {
        context.log("Log Data");
        context.log("Log Data with level INFO", Level.INFO);
        context.log("Log Data with level WARNING", Level.WARNING);
        context.log("Log Data with level SEVERE", Level.SEVERE);
        context.log("Log Data with level FINE", Level.FINE);
        context.log("Log Data with level FINEST", Level.FINEST);
        context.log("Log Data with level FINER", Level.FINER);
        context.log("Log Data with level ALL", Level.ALL);
        context.log("Log Data with level CONFIG", Level.CONFIG);
    }
}

Node.js

Node.js provides level-specific methods on context.log:

MethodDescription
context.log.INFO(logData)Logs with level INFO
context.log.WARNING(logData)Logs with level WARNING
context.log.SEVERE(logData)Logs with level SEVERE
context.log.FINE(logData)Logs with level FINE
context.log.FINER(logData)Logs with level FINER
context.log.FINEST(logData)Logs with level FINEST
context.log.ALL(logData)Logs with level ALL
context.log.CONFIG(logData)Logs with level CONFIG
 

module.exports = async function(context, basicIO) {
    context.log.INFO("log data INFO");
    context.log.WARNING("log data WARNING");
    context.log.SEVERE("log data SEVERE");
    context.log.FINE("log data FINE");
    context.log.FINER("log data FINER");
    context.log.FINEST("log data FINEST");
    context.log.ALL("log data ALL");
    context.log.CONFIG("log data CONFIG");
}

Python

Python uses the same level-specific methods as Node.js:

MethodDescription
context.log.INFO(logData)Logs with level INFO
context.log.WARNING(logData)Logs with level WARNING
context.log.SEVERE(logData)Logs with level SEVERE
context.log.FINE(logData)Logs with level FINE
context.log.FINER(logData)Logs with level FINER
context.log.FINEST(logData)Logs with level FINEST
context.log.ALL(logData)Logs with level ALL
context.log.CONFIG(logData)Logs with level CONFIG
 

def runner(context, basicIO):
    context.log.INFO("log INFO")
    context.log.WARNING("log WARNING")
    context.log.SEVERE("log SEVERE")
    context.log.FINE("log FINE")
    context.log.FINER("log FINER")
    context.log.FINEST("log FINEST")
    context.log.ALL("log ALL")
    context.log.CONFIG("log CONFIG")

Key difference from Deluge:

Deluge uses info statements for logging. Java, Node.js, and Python use context.log with explicit log levels, giving more granular control over log severity.

What's Next?