Le langage de script Zoho : custom functions, intégrations API, automation cross-applis. Le vrai super-pouvoir de Zoho.
Deluge = Data Enriched Language for the Universal Grid Environment. Langage de script propriétaire Zoho, conçu pour étendre les applis sans framework lourd. Comparable à AppScript chez Google ou Apex chez Salesforce.
| Contexte | Trigger | Usage |
|---|---|---|
| Custom Functions | Workflow Rule, Blueprint, Schedule | Le cas le plus fréquent — logique async |
| Buttons | Clic utilisateur dans une page record | Action ponctuelle déclenchée par un user |
| Validation Rules | Sauvegarde d'un record | Vérifications complexes pré-sauvegarde |
| Schedules | Cron (toutes les heures, daily, weekly) | Batch jobs (sync nocturne, reports auto) |
| Connections / Webhooks | Trigger via HTTP externe | Inbound webhook depuis Stripe, GitHub, n8n |
| Zoho Creator | Formulaires custom apps | Logique métier dans des apps no-code |
Une custom function Deluge a un timeout de 5 min. Au-delà, l'exécution est avortée. Pour les jobs longs, splittez en chunks avec Schedule Recursive ou délégez à un n8n externe via invokeurl.
// Typage dynamique, déclaration implicite
nom = "François"; // string
age = 42; // int
salary = 250.50; // float (decimal)
isActive = true; // boolean
today = zoho.currenttime; // datetime
// Collections
fruits = {"pomme", "poire", "kiwi"}; // list
user = Map(); // map (dict)
user.put("nom", "Dupont");
user.put("email", "j@example.com");
// Accès map
emailUser = user.get("email");
// Iteration list
for each fruit in fruits
{
info "Fruit : " + fruit;
}
// Iteration map
for each key in user.keys()
{
info key + " = " + user.get(key);
}
if(age >= 18)
{
statut = "majeur";
}
else if(age >= 13)
{
statut = "ado";
}
else
{
statut = "enfant";
}
// Ternaire-like (via if expression)
status = if(amount > 1000, "high-value", "regular");
// Concaténation
fullName = firstName + " " + lastName;
// Méthodes string
upper = "hello".toUpperCase(); // "HELLO"
parts = "a,b,c".toList(","); // {"a", "b", "c"}
joined = parts.toString(" - "); // "a - b - c"
trimmed = " spaces ".trim(); // "spaces"
substr = "Hello World".subString(0, 5); // "Hello"
hasZoho = "I love Zoho".contains("Zoho"); // true
replaced = "old".replaceAll("old", "new"); // "new"
// Numbers et dates
rounded = round(3.14159, 2); // 3.14
formatted = today.toString("yyyy-MM-dd"); // "2026-05-20"
tomorrow = today.addDay(1);
diffDays = (date2 - date1) / 86400000;
Deluge expose des wrappers natifs autour des APIs Zoho. Pas besoin de gérer OAuth, datacenter, etc. — tout est implicite quand vous appelez depuis votre tenant.
// Lire un record par ID
lead = zoho.crm.getRecordById("Leads", "554023000001235001");
info lead.get("Email");
// Chercher des records (criteria string)
deals = zoho.crm.searchRecords("Deals", "(Stage:equals:Negotiation)");
info "Found " + deals.size() + " deals";
// Créer un record
newLead = Map();
newLead.put("Last_Name", "Martin");
newLead.put("First_Name", "Sophie");
newLead.put("Email", "sophie@example.com");
newLead.put("Company", "Example Corp");
response = zoho.crm.createRecord("Leads", newLead);
newId = response.get("details").get("id");
// Mettre à jour
updateMap = Map();
updateMap.put("Lead_Status", "Qualified");
zoho.crm.updateRecord("Leads", "554023000001235001", updateMap);
// Supprimer
zoho.crm.deleteRecord("Leads", "554023000001235001");
// Convertir Lead → Deal+Contact+Account
convertMap = Map();
convertMap.put("Deals", {"Deal_Name": "Sophie - Software", "Amount": 5000});
zoho.crm.convertLead("554023000001235001", convertMap);
// Créer une facture
invoice = Map();
invoice.put("customer_id", "1234567000000123456");
invoice.put("line_items", {
Map().put("name", "Consulting Day").put("rate", 800).put("quantity", 3)
});
response = zoho.books.createRecord("Invoices", organizationId, invoice);
// Récupérer un client
customer = zoho.books.getRecordsByID("Contacts", organizationId, "1234567000000123456");
// Lister les factures impayées
invoices = zoho.books.getRecords("Invoices", organizationId, {"status": "unpaid"});
// Envoyer un email
sendmail
[
from : "marketing@ezway-technology.com"
to : "client@example.com"
cc : "manager@ezway-technology.com"
subject : "Votre devis #" + dealName
message : "<p>Bonjour " + firstName + ",</p><p>Voici votre devis...</p>"
];
Pour intégrer avec n'importe quel service externe (n8n, Stripe, Mailjet, votre backend custom), utilisez invokeurl.
response = invokeurl
[
url: "https://api.exchangerate.host/latest?base=EUR&symbols=USD,MAD"
type: GET
];
usdRate = response.get("rates").get("USD");
info "EUR/USD = " + usdRate;
payload = Map();
payload.put("event", "deal_closed_won");
payload.put("deal_id", dealId);
payload.put("amount", dealAmount);
payload.put("contact_email", contactEmail);
response = invokeurl
[
url: "https://n8n.ezway.technology/webhook/zoho-deal-closed"
type: POST
parameters: payload.toString()
headers: { "Content-Type": "application/json", "X-Source": "zoho-deluge" }
];
if(response.get("status") == "ok")
{
info "n8n a bien reçu l'event";
}
// Token stocké dans Connection (recommandé), ici en dur pour l'exemple
apiToken = "Bearer " + zoho.encryption.aesDecode("vault_key", encryptedToken);
response = invokeurl
[
url: "https://api.stripe.com/v1/customers"
type: POST
parameters: { "email": clientEmail, "name": clientName }
headers: { "Authorization": apiToken }
];
N'écrivez JAMAIS "Bearer sk_live_xxx" en dur dans le code Deluge. Tout dev avec accès au code source peut voir et exfiltrer. Utilisez les Connections (cf. section suivante) pour stocker tokens et secrets de manière sécurisée.
Les Connections Zoho sont un coffre intégré pour stocker les credentials des APIs externes. Setup : Setup → Developer Space → Connections.
stripe_prodsk_live_xxx (chiffré côté Zoho)response = invokeurl
[
url: "https://api.stripe.com/v1/customers"
type: POST
parameters: { "email": "j@example.com" }
connection: "stripe_prod"
];
// La clé API est injectée par Zoho dans le header Authorization automatiquement
Avantages :
Use case classique : quand un Deal CRM passe en Closed Won, créer automatiquement un client + facture brouillon dans Books.
// Custom function attachée à un Workflow Rule
// Trigger : Deal → on edit → Stage = Closed Won
// Function name : sync_won_deal_to_books
// Argument : dealId (CRM ID du Deal)
void sync_won_deal_to_books(String dealId)
{
// 1. Récupérer le Deal CRM et son contact
deal = zoho.crm.getRecordById("Deals", dealId);
if(deal == null || deal.get("Stage") != "Closed Won")
{
info "Skipping deal " + dealId + " - not Closed Won";
return;
}
accountId = deal.get("Account_Name").get("id");
account = zoho.crm.getRecordById("Accounts", accountId);
contactId = deal.get("Contact_Name").get("id");
contact = zoho.crm.getRecordById("Contacts", contactId);
booksOrgId = "60023456789"; // À mettre en variable d'environnement
// 2. Chercher si client existe déjà dans Books
existingResp = zoho.books.getRecords(
"Contacts",
booksOrgId,
{ "email": contact.get("Email") }
);
contacts = existingResp.get("contacts");
if(contacts.size() > 0)
{
booksContactId = contacts.get(0).get("contact_id");
info "Client Books existant : " + booksContactId;
}
else
{
// Créer le contact dans Books
newContact = Map();
newContact.put("contact_name", account.get("Account_Name"));
newContact.put("email", contact.get("Email"));
newContact.put("phone", contact.get("Phone"));
newContact.put("billing_address", Map()
.put("address", account.get("Billing_Street"))
.put("city", account.get("Billing_City"))
.put("zip", account.get("Billing_Code"))
.put("country", account.get("Billing_Country"))
);
createResp = zoho.books.createRecord("Contacts", booksOrgId, newContact);
booksContactId = createResp.get("contact").get("contact_id");
info "Client Books créé : " + booksContactId;
}
// 3. Créer la facture brouillon
invoice = Map();
invoice.put("customer_id", booksContactId);
invoice.put("invoice_number", "INV-" + deal.get("Deal_Name"));
invoice.put("date", zoho.currentdate.toString("yyyy-MM-dd"));
invoice.put("line_items", { Map()
.put("name", deal.get("Deal_Name"))
.put("description", "Vente issue du Deal CRM " + dealId)
.put("rate", deal.get("Amount"))
.put("quantity", 1)
.put("tax_id", "FR_VAT_20_TAX_ID") // ID de votre taux 20%
});
invoice.put("status", "draft");
invoiceResp = zoho.books.createRecord("Invoices", booksOrgId, invoice);
if(invoiceResp.get("code") == 0)
{
invoiceId = invoiceResp.get("invoice").get("invoice_id");
info "Facture créée : " + invoiceId;
// 4. Mettre à jour le Deal CRM avec l'ID Books pour traçabilité
updateMap = Map();
updateMap.put("Books_Invoice_ID", invoiceId);
zoho.crm.updateRecord("Deals", dealId, updateMap);
}
else
{
// Erreur : log + alert
errorMsg = invoiceResp.get("message");
info "ERREUR création facture : " + errorMsg;
sendmail
[
from : "alerts@ezway-technology.com"
to : "ops@ezway-technology.com"
subject : "[ALERTE] Sync CRM→Books échouée pour Deal " + dealId
message : errorMsg
];
}
}
Toujours vérifier si l'opération a déjà été faite (champ Books_Invoice_ID sur le Deal) avant de créer un doublon. En cas d'échec, loguer + notifier — ne JAMAIS silencer une erreur dans une custom function de prod.
info "message" + variable; → écrit dans les execution logs de la functionalert "message"; → popup à l'écran (uniquement contexte client-side : Buttons, Validation Rules)thisapp.info() dans Zoho CreatorSetup → Functions → [votre function] → Execution History — voir les logs de chaque run avec inputs/outputs/erreurs.
Sur la page d'édition d'une function, bouton Edit Arguments + Save and Execute → exécution en sandbox avec arguments mockés. Les info apparaissent en bas.
Si vous déclenchez Deluge via webhook entrant, regardez aussi Setup → Webhooks → Logs — les erreurs HTTP (auth, payload mal formé) y apparaissent AVANT que la function ne s'exécute.
status ou code de chaque réponse API, logger + alerter en cas d'échec.info dans chaque itération si vous traitez 1000 records — ça pollue les logs.