Watch Tutorial on Youtube
Copy Paste below code in Google Apps Script and change configuration variables as shown in video.
// Google Apps Script Code
// Configuration Variables
const API_KEY = 'YOUR_BOLNA_API_KEY_HERE'; // From Bolna dashboard > Developers
const AGENT_ID = 'YOUR_AGENT_ID_HERE'; // From Bolna agent creation
const FROM_PHONE_NUMBER = '+YOUR_FROM_NUMBER_HERE'; // Bolna-purchased or connected number
const SHEET_NAME = 'Sheet1'; // Your sheet tab name
const API_BASE = 'https://api.bolna.ai';
const BATCH_SIZE = 5; // Max rows to process per run (adjust to stay under 6-min timeout)
const TRIGGER_INTERVAL_MINUTES = 1; // Time between batches (minutes)
// Handles incoming webhook POST requests from Bolna
function doPost(e) {
try {
// Log raw payload for debugging
const rawPayload = e.postData ? e.postData.contents : '{}';
Logger.log('Raw webhook payload: ' + rawPayload);
// Parse the incoming JSON payload
const payload = JSON.parse(rawPayload);
// Extract id and status
const id = payload.id;
const status = payload.status;
if (!id || !status) {
throw new Error(`Invalid payload: Missing id (${id}) or status (${status}).`);
}
// Prepare call results (include extracted_data, summary, error_message, recipient_data)
const errorMessage = payload.error_message || '';
const recipientName = payload.context_details?.recipient_data?.name || '';
let callResults = {};
if (payload.extracted_data) callResults.extracted_data = payload.extracted_data;
if (payload.summary) callResults.summary = payload.summary;
if (errorMessage) callResults.error_message = errorMessage;
if (recipientName) callResults.recipient_name = recipientName;
callResults = JSON.stringify(callResults || 'No results');
// Access the spreadsheet
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
// Find the row with matching id (Column C)
let rowUpdated = false;
for (let i = 1; i < data.length; i++) {
if (data[i][2] === id) { // Column C (Execution ID)
// Update Status (Column D) – append error if present
const updatedStatus = errorMessage ? `${status} (${errorMessage})` : status;
sheet.getRange(i + 1, 4).setValue(updatedStatus);
// Update Call Results (Column E) for final statuses
const finalStatuses = ['completed', 'failed', 'error', 'canceled', 'stopped', 'balance-low', 'busy', 'no-answer'];
if (finalStatuses.includes(status)) {
sheet.getRange(i + 1, 5).setValue(callResults);
}
rowUpdated = true;
break;
}
}
if (!rowUpdated) {
Logger.log(`No matching row found for id: ${id}`);
}
// Return success response to Bolna
return ContentService.createTextOutput(JSON.stringify({ status: 'success' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
// Log error and return failure response
Logger.log('Webhook error: ' + error);
return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: error.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// Trigger calls for a batch of unprocessed rows
function triggerBolnaCalls() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
let processedCount = 0;
// Loop through rows, process up to BATCH_SIZE
for (let i = 1; i < data.length && processedCount < BATCH_SIZE; i++) {
const name = data[i][0]; // Column A (Name)
const phoneNumber = '+91'+data[i][1]; // Column B (Phone Number)
const processed = data[i][5]; // Column F (Processed)
if (!phoneNumber || processed === true) continue; // Skip empty or processed rows
// Trigger call with user_data
const payload = {
agent_id: AGENT_ID,
recipient_phone_number: phoneNumber,
from_phone_number: FROM_PHONE_NUMBER,
user_data: { name: name || 'Unknown' } // Include name in user_data
};
const options = {
method: 'post',
contentType: 'application/json',
headers: { Authorization: `Bearer ${API_KEY}` },
payload: JSON.stringify(payload),
};
try {
const response = UrlFetchApp.fetch(`${API_BASE}/call`, options);
const result = JSON.parse(response.getContentText());
const executionId = result.execution_id;
// Update sheet
sheet.getRange(i + 1, 3).setValue(executionId); // Execution ID (Column C)
sheet.getRange(i + 1, 6).setValue(true); // Mark as Processed (Column F)
processedCount++;
} catch (error) {
Logger.log(`Error triggering call for ${phoneNumber}: ${error}`);
sheet.getRange(i + 1, 4).setValue('error'); // Status (Column D)
sheet.getRange(i + 1, 6).setValue(true); // Mark as Processed to skip retries
processedCount++;
}
}
// If more unprocessed rows exist, set up a trigger for the next batch
if (hasUnprocessedRows(sheet, data)) {
ScriptApp.newTrigger('triggerBolnaCalls')
.timeBased()
.after(TRIGGER_INTERVAL_MINUTES * 60 * 1000) // Convert minutes to milliseconds
.create();
}
}
// Check if there are unprocessed rows
function hasUnprocessedRows(sheet, data) {
for (let i = 1; i < data.length; i++) {
if (data[i][1] && data[i][5] !== true) { // Has phone number and not processed
return true;
}
}
return false;
}
// Optional: Polling fallback for rows missing webhook updates
function checkPendingCalls() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
const executionId = data[i][2]; // Column C
const status = data[i][3]; // Column D
if (!executionId || status === 'completed' || status === 'failed' || status === 'error') continue;
// Poll for status
const options = {
method: 'get',
headers: { Authorization: `Bearer ${API_KEY}` },
};
try {
const response = UrlFetchApp.fetch(`${API_BASE}/executions/${executionId}`, options);
const result = JSON.parse(response.getContentText());
const newStatus = result.status;
// Update sheet
sheet.getRange(i + 1, 4).setValue(newStatus); // Status (Column D)
if (newStatus === 'completed') {
const callResults = JSON.stringify(result.extracted_data || result.summary || 'Call completed');
sheet.getRange(i + 1, 5).setValue(callResults); // Call Results (Column E)
}
} catch (error) {
Logger.log(`Error polling ${executionId}: ${error}`);
sheet.getRange(i + 1, 4).setValue('error');
}
}
// Re-schedule polling if pending calls remain
if (hasPendingCalls(sheet, data)) {
ScriptApp.newTrigger('checkPendingCalls')
.timeBased()
.after(5 * 60 * 1000) // Run every 5 minutes
.create();
}
}
// Check if there are pending calls (not completed/failed/error)
function hasPendingCalls(sheet, data) {
for (let i = 1; i < data.length; i++) {
const executionId = data[i][2];
const status = data[i][3];
if (executionId && status !== 'completed' && status !== 'failed' && status !== 'error') {
return true;
}
}
return false;
}
// Add a custom menu to trigger manually
function onOpen() {
SpreadsheetApp.getUi().createMenu('Bolna Helper by Yohita')
.addItem('Trigger Calls', 'triggerBolnaCalls')
.addItem('Check Pending Calls', 'checkPendingCalls')
.addToUi();
}
//end of code

