Common causes of Google Sheets CRM failures and how we resolve them:
Failure: 6-Minute Script Timeout
Caused by reading/writing cells in loops (getValue() inside for). We convert loops to single memory batch arrays (getValues()/setValues()), reducing runtimes from minutes to milliseconds.
Failure: Simultaneous Lead Overwrite
When two leads arrive at the exact same second, both read the same getLastRow(). We add LockService.getScriptLock() so requests queue cleanly with zero dropped records.
Live Apps Script Execution Log
[15:48:10.220] INFO: Webhook doPost() received from WhatsApp API
[15:48:10.235] LOCK: Script lock acquired (ID: lk_448102)
[15:48:10.248] ROUTING: Assigned Rohit Verma to Agent Sarah
[15:48:10.260] BATCH_WRITE: Committed to row 49 in 12ms
[15:48:10.262] LOCK: Script lock released cleanly
Clean, modular Google Apps Script architecture for lead intake and agent routing:
/**
* Production-Grade Google Sheets CRM Lead Ingress Handler
* Safe against concurrent submissions & timeout limits
*/
function doPost(e) {
const lock = LockService.getScriptLock();
// Wait up to 30 seconds for concurrent writes to clear
if (!lock.tryLock(30000)) {
return ContentService.createTextOutput(JSON.stringify({
status: "busy",
message: "Sheet currently updating. Please retry."
})).setMimeType(ContentService.MimeType.JSON);
}
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Master_Leads");
const payload = JSON.parse(e.postData.contents);
const nextRow = sheet.getLastRow() + 1;
// Assign sales agent using round-robin logic
const agent = assignNextAvailableAgent();
// High-speed atomic write (Single call instead of multiple setValues)
sheet.getRange(nextRow, 1, 1, 5).setValues([[
new Date(),
payload.name,
payload.channel || "Web Ingress",
agent,
"New Inquiry"
]]);
return ContentService.createTextOutput(JSON.stringify({
status: "success",
row: nextRow,
assignedAgent: agent
})).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
Logger.log("Ingestion error: " + error.toString());
return ContentService.createTextOutput(JSON.stringify({
status: "error",
message: error.toString()
})).setMimeType(ContentService.MimeType.JSON);
} finally {
// Always release lock so next lead processes immediately
lock.releaseLock();
}
}