/** * Campaign Optimizer Template * * Template for optimizing Google Ads campaigns based on performance metrics. * Customize thresholds and logic to fit your business needs. * * Features: * - Pause underperforming campaigns * - Adjust budgets based on ROAS * - Email notifications for actions taken * - Detailed logging to Google Sheets */ // ============================================================================ // CONFIGURATION // ============================================================================ const CONFIG = { // Performance thresholds MIN_CONVERSIONS: 5, // Minimum conversions to evaluate TARGET_ROAS: 3.0, // Target Return on Ad Spend (300%) LOW_ROAS_THRESHOLD: 2.0, // ROAS below this triggers budget reduction MIN_QUALITY_SCORE: 5, // Minimum acceptable quality score // Budget adjustments BUDGET_INCREASE_FACTOR: 1.10, // +10% for high performers BUDGET_DECREASE_FACTOR: 0.90, // -10% for low performers // Date range for analysis DATE_RANGE: 'LAST_30_DAYS', // Reporting SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID', // Replace with your Sheet ID LOG_SHEET_NAME: 'Campaign Optimizer Log', NOTIFICATION_EMAIL: Session.getEffectiveUser().getEmail(), // Safety limits DRY_RUN: false, // Set to true to preview changes without applying MAX_CAMPAIGNS_TO_PROCESS: 1000 // Prevent accidental large-scale changes }; // ============================================================================ // MAIN FUNCTION // ============================================================================ function main() { try { Logger.log('Starting Campaign Optimizer...'); Logger.log('Date: ' + new Date()); Logger.log('Mode: ' + (CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE')); // Initialize reporting const sheet = initializeReportingSheet(); // Get campaigns to optimize const campaigns = getCampaignsToOptimize(); Logger.log(`Found ${campaigns.totalNumEntities()} campaigns to evaluate`); if (campaigns.totalNumEntities() > CONFIG.MAX_CAMPAIGNS_TO_PROCESS) { throw new Error(`Too many campaigns (${campaigns.totalNumEntities()}). Increase MAX_CAMPAIGNS_TO_PROCESS if intended.`); } // Process campaigns const results = processCampaigns(campaigns); // Generate summary logResults(sheet, results); sendEmailNotification(results); Logger.log('Campaign Optimizer completed successfully'); } catch (error) { handleError(error); } } // ============================================================================ // CAMPAIGN PROCESSING // ============================================================================ function getCampaignsToOptimize() { return AdsApp.campaigns() .withCondition('campaign.status = ENABLED') .orderBy('campaign.metrics.cost DESC') .get(); } function processCampaigns(campaigns) { const results = { evaluated: 0, paused: [], budgetIncreased: [], budgetDecreased: [], noAction: [], errors: [] }; while (campaigns.hasNext()) { const campaign = campaigns.next(); results.evaluated++; try { const decision = evaluateCampaign(campaign); if (decision.action === 'PAUSE') { if (!CONFIG.DRY_RUN) { campaign.pause(); } results.paused.push({ name: campaign.getName(), reason: decision.reason }); } else if (decision.action === 'INCREASE_BUDGET') { if (!CONFIG.DRY_RUN) { increaseBudget(campaign, CONFIG.BUDGET_INCREASE_FACTOR); } results.budgetIncreased.push({ name: campaign.getName(), reason: decision.reason }); } else if (decision.action === 'DECREASE_BUDGET') { if (!CONFIG.DRY_RUN) { decreaseBudget(campaign, CONFIG.BUDGET_DECREASE_FACTOR); } results.budgetDecreased.push({ name: campaign.getName(), reason: decision.reason }); } else { results.noAction.push(campaign.getName()); } } catch (error) { results.errors.push({ campaign: campaign.getName(), error: error.message }); Logger.log(`Error processing campaign ${campaign.getName()}: ${error.message}`); } } return results; } function evaluateCampaign(campaign) { const stats = campaign.getStatsFor(CONFIG.DATE_RANGE); const conversions = stats.getConversions(); const roas = stats.getReturnOnAdSpend(); const cost = stats.getCost() / 1000000; // Insufficient data if (conversions < CONFIG.MIN_CONVERSIONS) { return { action: 'NONE', reason: `Insufficient conversions (${conversions})` }; } // High performer - increase budget if (roas >= CONFIG.TARGET_ROAS) { return { action: 'INCREASE_BUDGET', reason: `High ROAS (${roas.toFixed(2)}), Cost: $${cost.toFixed(2)}` }; } // Low performer - pause if (roas < CONFIG.LOW_ROAS_THRESHOLD) { return { action: 'PAUSE', reason: `Low ROAS (${roas.toFixed(2)}) below threshold (${CONFIG.LOW_ROAS_THRESHOLD})` }; } // Medium performer - decrease budget if (roas < CONFIG.TARGET_ROAS) { return { action: 'DECREASE_BUDGET', reason: `ROAS (${roas.toFixed(2)}) below target (${CONFIG.TARGET_ROAS})` }; } return { action: 'NONE', reason: 'Performance within acceptable range' }; } // ============================================================================ // BUDGET MANAGEMENT // ============================================================================ function increaseBudget(campaign, factor) { const currentBudget = campaign.getBudget().getAmount(); const newBudget = Math.floor(currentBudget * factor); campaign.getBudget().setAmount(newBudget); Logger.log(`Increased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`); } function decreaseBudget(campaign, factor) { const currentBudget = campaign.getBudget().getAmount(); const newBudget = Math.floor(currentBudget * factor); campaign.getBudget().setAmount(newBudget); Logger.log(`Decreased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`); } // ============================================================================ // REPORTING // ============================================================================ function initializeReportingSheet() { const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID); let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME); if (!sheet) { sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME); sheet.appendRow(['Timestamp', 'Mode', 'Action', 'Campaign', 'Reason']); sheet.getRange('A1:E1').setFontWeight('bold'); } return sheet; } function logResults(sheet, results) { const timestamp = new Date(); const mode = CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'; // Log paused campaigns results.paused.forEach(item => { sheet.appendRow([timestamp, mode, 'PAUSED', item.name, item.reason]); }); // Log budget increases results.budgetIncreased.forEach(item => { sheet.appendRow([timestamp, mode, 'BUDGET_INCREASED', item.name, item.reason]); }); // Log budget decreases results.budgetDecreased.forEach(item => { sheet.appendRow([timestamp, mode, 'BUDGET_DECREASED', item.name, item.reason]); }); // Log errors results.errors.forEach(item => { sheet.appendRow([timestamp, mode, 'ERROR', item.campaign, item.error]); }); Logger.log(`Logged ${results.paused.length + results.budgetIncreased.length + results.budgetDecreased.length + results.errors.length} actions to sheet`); } function sendEmailNotification(results) { if (results.paused.length === 0 && results.budgetIncreased.length === 0 && results.budgetDecreased.length === 0) { Logger.log('No actions taken, skipping email notification'); return; } const subject = `Campaign Optimizer Report - ${CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'} - ${new Date().toDateString()}`; const body = ` Campaign Optimizer Results ========================== Mode: ${CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'} Date: ${new Date()} Campaigns Evaluated: ${results.evaluated} Actions Taken: -------------- Paused: ${results.paused.length} Budget Increased: ${results.budgetIncreased.length} Budget Decreased: ${results.budgetDecreased.length} No Action: ${results.noAction.length} Errors: ${results.errors.length} Paused Campaigns: ${results.paused.map(item => `- ${item.name}: ${item.reason}`).join('\n')} Budget Increased: ${results.budgetIncreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')} Budget Decreased: ${results.budgetDecreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')} ${results.errors.length > 0 ? `\nErrors:\n${results.errors.map(item => `- ${item.campaign}: ${item.error}`).join('\n')}` : ''} --- Generated by Google Ads Script `; MailApp.sendEmail(CONFIG.NOTIFICATION_EMAIL, subject, body); Logger.log('Email notification sent to ' + CONFIG.NOTIFICATION_EMAIL); } // ============================================================================ // ERROR HANDLING // ============================================================================ function handleError(error) { Logger.log('FATAL ERROR: ' + error.message); Logger.log('Stack trace: ' + error.stack); MailApp.sendEmail( CONFIG.NOTIFICATION_EMAIL, 'Campaign Optimizer Error', `An error occurred in the Campaign Optimizer script:\n\n${error.message}\n\nStack trace:\n${error.stack}` ); throw error; }