Improving Google Campaigns Using Rules, Experiments, and Scripts

Rules, Experiments, and Scripts - How can you automatically improve your Google campaigns using scripts?

At the Update Upgrade 2019 conference, Avishai Freund gave a lecture on how we can automatically improve our Google campaigns. Using rules, experiments, and scripts, we can maximize our results. All details below.

Automated Rules

Also called automated rules / bulk actions, their purpose is to perform certain actions automatically, within a defined time frame, such as once a day / a week / a day of the month.

Examples:

When the campaign cost reaches 6,000 NIS this month, pause advertising

Send an email every morning with search terms that have a quality score below 3.

So how do we do this?

– Go to the Campaigns / Ad Groups / Keywords level

– Select the desired item (campaign, ad group, keywords – you can select one or more)

– Click on “More” (the three dots):

Scheduling hourly automated script execution inside Google Ads tools

– Click on Create an automated rule:

B2B audience targeting options inside LinkedIn advertising platform

First Example

– Every day at 6:00 AM the rule will run – if Campaign X has spent 6,000 NIS to date -> pause the campaign.

Setting daily budget and bidding options for LinkedIn ads

What did we choose?

– We chose to pause the campaign

– You can choose any campaign you want

– Set the condition

– Set the action frequency and the attribution period

LinkedIn desktop text ads visible in right-hand sidebar

Second Example

– Send an email every morning with all keywords that have a quality score lower than 3.

– Go to Keywords.

– Choose send an email.

LinkedIn video ad preview appearing in member news feed
LinkedIn event ad promoting an upcoming virtual business webinar


Experiments

– Today, Google provides us with a wide range of recommendations for account management.

– To switch between strategies, navigate through the menu:

“Campaign”, then “Settings”, then “Bidding”:

Interactive LinkedIn conversation ad setup with automated response paths

 

How to run an experiment?

First, create a draft, then define the experiment, and then make the decision:

Step 1:

– At the bottom of the menu, select Drafts & Experiments

– In the resulting screen, add a new draft

Sponsored InMail message ad delivered to LinkedIn inbox

– Name the draft and save:

Google Ads script editor with custom optimization JavaScript code

– The draft will appear in the menu below.

– Select it (hover/click on it), and choose Drafts & Experiments again.

– Choose New Experiment.

Authorizing and previewing script execution results in Google Ads

 

Step 2 – Creating the Experiment:

– Now we will fill in the details.

– Originally, the campaign strategy was manual CPC; I want to test switching to a Target CPA strategy.

– The experiment will run on half of the traffic.

– The experiment will last about two weeks (5/2 to 22/3).

– Save.

Scheduling hourly automated script execution inside Google Ads tools

– Connect the campaign

Setting automated budget increase rules based on target ROAS

– The experiment has started:

Configuring keyword bid adjustments triggered by conversion rate thresholds
Draft campaign setup panel before launching a split experiment

Notes:

– You can pause the experiment at any time.

– If the experiment was successful and you don’t want to wait, you can apply the experiment.

Monitoring statistical significance confidence levels in Google Ads experiments


Scripts

What is a script, essentially?

– A script is a piece of code that performs an automated action; there are many scripts available for download online.

– The script runs on Google’s servers.

– The script can run on a single account or across your entire MCC

Installing an Account-Level Script

Bulk actions menu displaying list of active Google Ads scripts

 

The First Script:

– The script monitors active ads and checks their status; if the link is broken – sends a report + option for immediate pause.

– Script name: (R) Ads Final URL Monitor (Pause + Report).

– Two parameters: 

1. Email to which the report will be sent

2. Whether to pause ads or not (report only)

– Account-level script.

– Note: At a certain point, you will be asked to grant permissions.

 

You can copy the script from here:


var EMAIL = '[email protected]';
var PAUSE = false;

function main(){  
  var htmlService = HTMLBuilderService();
  
  var ads = AdWordsApp.ads()
  .withCondition('CampaignStatus = ENABLED')
  .withCondition('AdGroupStatus = ENABLED')
  .withCondition('Status = ENABLED')
  .withCondition('Type NOT_IN [CALL_ONLY_AD]') // IMAGE_AD, MOBILE_AD, MOBILE_IMAGE_AD, PRODUCT_AD, RICH_MEDIA_AD, TEMPLATE_AD, TEXT_AD, CALL_ONLY_AD
  .withLimit(4)
  .get();
  
  Logger.log('%s ads found.', ads.totalNumEntities());
  
  var matches = 0;  
  while(ads.hasNext()){
    var ad = ads.next();      
    var adURL = ad.urls().getFinalUrl();
    var adHeader = ad.getHeadline();
    var adType = ad.getType();
    
    Logger.log('ad: %s (%s)', adHeader, adType);
    
    var statusCode = -1;
    try{
      var response = UrlFetchApp.fetch(adURL, { muteHttpExceptions: true });
      statusCode = response.getResponseCode();      
      Logger.log('[status %s] %s', statusCode, adURL);      
    }
    catch(e){
      Logger.log('[exception %s] %s', e.message, adURL);      
    }
    
    // OK
    if(statusCode == 200 || statusCode == -1) 
      continue;    
    
    var adgroup = ad.getAdGroup();
    var adgroupName = adgroup.getName();
    
    Logger.log('adgroup: %s', adgroupName);
    Logger.log('%s [%s]', adURL, statusCode);    
    
    matches++;
    
    htmlService.add(
      '

statusCode: ' + statusCode +'

' + '
adGroup: ' + adgroupName +'
' + '

adURL: ' + adURL + '

' ); if(PAUSE) { Logger.log('ad paused'); ad.pause(); } } if(matches > 0){ MailApp.sendEmail(EMAIL, 'Ad Monitor', '', { htmlBody: htmlService.get() }); } } var HTMLBuilderService = function(){ var _html = ''; return { add: function(content){ _html += content; }, get: function(){ return _html; } }; }

Name the script, grant permissions, and paste the script:

Automated anomaly detector script logs and performance error reports

Enter your email and whether to pause or not:

Negative keyword automation script running in Google Ads editor

Click on Scripts, select the script execution frequency

Automated bid adjustment rule configuration for high-converting search keywords

You can see in the script history when the script ran and what action it performed:

Google Ads script execution history showing runtime status and changes

Second Script – Monthly Reporting:

– Script name: (R) Account Daily Stats + Charts – Current Month)

– Script purpose: To create a neat Excel sheet with charts.

– The script creates a Google Sheet file inside Google Drive.

– At the top of the script, specify the file name you want.

– If the file does not exist, the script generates it automatically; if the file exists, the script updates it.

– Account-level script.

 

You can copy the script from here:


var SPREADSHEET = 'AccountDailyStats';

function main() {  
  var sheetService = SpreadsheetLoader.loadSheetService(SPREADSHEET);
  if(!sheetService) // create the sheet 
    sheetService = SpreadsheetLoader.createSheetService(SPREADSHEET);
  
  sheetService.clear();
  
  Logger.log('Loading "%s" Account Statistics ...', AdWordsApp.currentAccount().getName());
  var report = AdWordsApp.report(
     'SELECT Date, Impressions, Clicks, ConversionRate, Cost ' +
     'FROM   ACCOUNT_PERFORMANCE_REPORT ' +
     'DURING THIS_MONTH');
  
  report.exportToSheet(sheetService.sheet);
  sheetService.sort(1);
  
  SetTotals(sheetService);    
  
  if(!sheetService.hasCharts)
    DrawChart(sheetService);  
  
  Logger.log('Done!');
}

function SetTotals(sheetService){
  
  // TOTALS (using formulas)
  Logger.log('Setting formulas ...');
  
  var lastRowIndex = sheetService.getLastRowIndex();  
  var totalRowIndex = (parseInt(lastRowIndex) + 1).toString();
  
  sheetService.setCellSumFormula('B2:B'.concat(lastRowIndex), 'B'.concat(totalRowIndex));
  sheetService.setCellSumFormula('C2:C'.concat(lastRowIndex), 'C'.concat(totalRowIndex));
  sheetService.setCellAvgFormula('D2:D'.concat(lastRowIndex), 'D'.concat(totalRowIndex));
  sheetService.setCellSumFormula('E2:E'.concat(lastRowIndex), 'E'.concat(totalRowIndex));
  sheetService.setDataUI('A'.concat(totalRowIndex, ':E', totalRowIndex), '#F5F6F9', '#000000', 20, 'center');   
}

function DrawChart(sheetService){
  Logger.log('Drawing Chart ... ');
     
  var lastRowIndex = sheetService.getLastRowIndex();
  
  // arrRanges, vAxisDataLeft, vAxisDataRight, width, height, rowPosition, columnPosition, chartTitle 
  // vAxisData: { title, ticks, color, legend }
  sheetService.addLineChartDualY(
    ['A2:A'.concat(lastRowIndex), 'B2:B'.concat(lastRowIndex), 'C2:C'.concat(lastRowIndex)],
    { 
      title: 'impressions',
      color:'#4572A7', 
      legend:'impressions'
    },
    { 
      title: 'clicks',       
      color:'#AA4643', 
      legend:'clicks'
    },   
    700, 160, 3, 7, // chart location - row 3, column 7
    'impressions vs clicks'
  ); 
  
  sheetService.addLineChartDualY(
    ['A2:A'.concat(lastRowIndex), 'D2:D'.concat(lastRowIndex), 'C2:C'.concat(lastRowIndex)],
    { 
      title: 'conv.Rate',
      color:'#4572A7', 
      legend:'conv.Rate'
    },
    { 
      title: 'clicks',       
      color:'#AA4643', 
      legend:'clicks'
    },  
    700, 160, 11, 7, // chart location - row 10, column 7
    'conv.Rate vs clicks'
  ); 
  
  sheetService.addLineChartDualY(
    ['A2:A'.concat(lastRowIndex), 'E2:E'.concat(lastRowIndex), 'C2:C'.concat(lastRowIndex)],
    { 
      title: 'cost',
      color:'#4572A7', 
      legend:'cost'
    },
    { 
      title: 'clicks',       
      color:'#AA4643', 
      legend:'clicks'
    },
    700, 160, 19, 7, // chart location - row 17, column 7
    'cost vs clicks'
  ); 
}

/* SPREADSHEET SERVICE */
var SpreadSheetService = function(spreadSheet) {
  var _spreadSheet = spreadSheet;
  var _sheets = [];

  (function(){
    var sheetServices = [];
    var temp_sheets = spreadSheet.getSheets();
    for(var i= 0; i < temp_sheets.length; i++)
      _sheets.push(new SheetService(temp_sheets[i]));   
  })();   
  
  return {      
    sheets: _sheets,
    getActiveSheet: function(){
      return SheetService(_spreadSheet.getActiveSheet());
    },
    getSheetByName: function(sheetName) { 
         for(var i= 0; i < _sheets.length; i++)
            if(_sheets[i].sheetName == sheetName)
              return _sheets[i];
          return '';
    },
    addNewSheet: function(sheetName){
        var new_sheet = SheetService(_spreadSheet.insertSheet(sheetName));
        _sheets.push(new_sheet)
        return new_sheet;         
    },
    isSheetExists: function(sheetName){
        return this.getSheetByName(sheetName) != '';
    }, 
    getURL: function(){
      return _spreadSheet.getUrl();
    },
	getId: function(){
      return _spreadSheet.getId();
    }  
  }; 
}

/* SHEET SERVICE */
var SheetService = function(sheet) { 
  var _sheet = sheet;
  
  var setFormat = function(strRange, format){
    var range = _sheet.getRange(strRange);
    range.setNumberFormat(format);  
  }
  
  var buildLineChart = function(arrRanges, series, vAxes, width, height, rowPosition, columnPosition, chartTitle){        
       var chartBuilder = _sheet.newChart();
              
       for (var i in arrRanges)         
         chartBuilder.addRange(_sheet.getRange(arrRanges[i]));       
       
       chartBuilder
       .setChartType(Charts.ChartType.LINE)  // chart type - line        
       .setPosition(rowPosition, columnPosition, 0, 0)  // chart location row X column Y       
                            
       .setOption('animation.duration', 500) // animation                 
       .setOption('width', width)        
       .setOption('height', height)                                    
       .setOption('legend', { position: 'bottom', alignment: 'center' })                 
       .setOption('hAxis.textPosition', 'none') // hide hAxis labels (options: out, in, none)     
       .setOption('series', series)  // parameter       
       .setOption('vAxes', vAxes) // parameter              
       
       if(chartTitle)
         chartBuilder.setOption('title', chartTitle); 

       _sheet.insertChart(chartBuilder.build());
  }
  
  return {   
    sheet: _sheet,
     hasSheet: _sheet != null,
     sheetName: _sheet ? _sheet.getName() : '',
     getLastRowIndex: function(){ return _sheet.getLastRow();}, 
     getLastColumnIndex: function(){ return _sheet.getLastColumn();}, 
     getValue: function(rowPosition, columnPosition){ 
       return _sheet.getRange(rowPosition, columnPosition).getValue();
     }, 
     getValues: function(){
       return _sheet.getDataRange().getValues();
     }, 
     getRowValues:function(rowPosition){       
       return _sheet.getRange(rowPosition, 1, 1, this.getLastColumnIndex()).getValues();
     },
     getRowIndex: function(columnPosition, key){
       var rows = _sheet.getDataRange().getValues();
 
       // row index is 0 based but range is 1 based
       for(row in rows)
         if(rows[row][columnPosition - 1] == key)
           return parseInt(row) + 1;
       return -1;  
     },
     getRowIndexBy2Columns: function(columnAPosition, keyA, columnBPosition, keyB){ 
       var rows = _sheet.getDataRange().getValues();
 
       // row index is 0 based but range is 1 based
       for(row in rows)
         if(rows[row][columnAPosition - 1] == keyA && rows[row][columnBPosition - 1] == keyB)
           return parseInt(row) + 1;
       return -1;  
     },
     getColumnValues: function(startRowPosition, columnPosition){
        // getRange(row, column, numRows, numColumns)
        return _sheet.getRange(startRowPosition, columnPosition, _sheet.getLastRow() - 1, 1).getValues();
     }, 
     addRow: function(arrValues){ 
       //_sheet.appendRow(arrValues);
       this.addRowAtIndex(arrValues, _sheet.getLastRow() + 1);
     },
     addRowAtIndex: function(arrValues, rowPosition){               
        rowPosition = rowPosition || _sheet.getLastRow() + 1;
        var range = _sheet.getRange(rowPosition, 1, 1, arrValues.length);
        range.setValues([arrValues]);
     },
     addLineChart: function(arrRanges /* range format: 'B15:B24' */, vAxisData /* { title, ticks, lines[]{color, legend}  } */, width, height, rowPosition, columnPosition, chartTitle){  
       var series = [];
       for (var i in vAxisData.lines)         
         series.push({ 
           color: vAxisData.lines[i].color, 
           labelInLegend : vAxisData.lines[i].legend,
           pointShape: 'square',
           pointSize: 4,
           targetAxisIndex: 0 // only one vAxis (index 0)
         });
       
       var minValue = 0;
       if(vAxisData.ticks) // custom vAxis values      
         // set minimum value to be the minimum tick value
         minValue = Math.min.apply(null, vAxisData.ticks).toFixed(0);
       
       var vAxis = { 
         ticks: vAxisData.ticks, // null for default
         title: vAxisData.title,
         minValue: minValue, 
         viewWindow: { min: minValue }
       };

       var vAxes = [];
       vAxes.push(vAxis); // only one vAxis (index 0)
       
       buildLineChart(arrRanges, series, vAxes, width, height, rowPosition, columnPosition, chartTitle);
     },
     addLineChartDualY: function(arrRanges /* range format: 'B15:B24' */, vAxisDataLeft /* { title, ticks, color, legend } */, vAxisDataRight, width, height, rowPosition, columnPosition, chartTitle){  
       var series = [];
       
       series.push({ 
           color: vAxisDataLeft.color, 
           labelInLegend : vAxisDataLeft.legend,
           pointShape: 'square',
           pointSize: 4,
           targetAxisIndex: 0 // vAxis (index 0)
       });
             
       series.push({ 
           color: vAxisDataRight.color, 
           labelInLegend : vAxisDataRight.legend,
           pointShape: 'square',
           pointSize: 4,
           targetAxisIndex: 1 // vAxis (index 1)
       });
       
       var minValueLeft = 0;
       if(vAxisDataLeft.ticks) // custom vAxis values      
         // set minimum value to be the minimum tick value
         minValueLeft = Math.min.apply(null, vAxisDataLeft.ticks).toFixed(0);
       
       var vAxisLeft = { 
         ticks: vAxisDataLeft.ticks, // null for default
         title: vAxisDataLeft.title,
         minValue: minValueLeft, 
         viewWindow: { min: minValueLeft }
       };
       
       var minValueRight = 0;
       if(vAxisDataRight.ticks) // custom vAxis values      
         // set minimum value to be the minimum tick value
         minValueRight = Math.min.apply(null, vAxisDataRight.ticks).toFixed(0);
       
       var vAxisRight = { 
         ticks: vAxisDataRight.ticks, // null for default
         title: vAxisDataRight.title,
         minValue: minValueRight, 
         viewWindow: { min: minValueRight }
       };

       var vAxes = [];
       vAxes.push(vAxisLeft); // vAxis (index 0)
       vAxes.push(vAxisRight); // vAxis (index 1)
       
       buildLineChart(arrRanges, series, vAxes, width, height, rowPosition, columnPosition, chartTitle);
     },
     addImageFromURL: function(url, rowPosition, columnPosition, top, left){ 
       top = top || 0;
       left = left || 0;
       
       _sheet.insertImage(url, rowPosition, columnPosition, left, top);
     },   
     deleteRow: function(rowPosition){
       _sheet.deleteRow(rowPosition);
     },  
     sort: function(columnPosition){
       _sheet.sort(columnPosition);
     },
     hasCharts:  _sheet.getCharts().length > 0,
     findColumnValuesByFilter: function(columnPosition, filterValue, filterColumnPosition){
       /* 
          get column values filtered by other column
          
          e.g: findColumnValuesByFilter(2, '100', 1)          
          all B column values that the value in A column equals to '100'
       */

       var result = [];       
       var rows = _sheet.getDataRange().getValues();
 
       for(row in rows)
         if(rows[row][filterColumnPosition - 1] == filterValue)
           result.push(rows[row][columnPosition]);
       return result;  
     }, 
     clear: function(charts, format, contents){
       charts = charts || false;
       format = format || false;
       contents = contents || true;
       
       if(!charts) return; 
       
       // clear all charts
       _sheet.clear({ formatOnly: format, contentsOnly: contents });        
       var charts = _sheet.getCharts();
       for (var i in charts)
         _sheet.removeChart(charts[i]);
     },          
     setValue: function(rowPosition, columnPosition, value){ 
       _sheet.getRange(rowPosition, columnPosition).setValue(value);
     },     
     setRangeValue: function(strRange, value){ 
       // e.g: setCurrencyFormat('A1'); // set cell
       _sheet.getRange(strRange).setValue(value);
     },
     setDataUI: function(strRange, backgroundColor, foreColor, fontSize, align){
       var range = _sheet.getRange(strRange);
       if(backgroundColor)
           range.setBackground(backgroundColor);
       if(foreColor)
           range.setFontColor(foreColor);
       if(fontSize)
           range.setFontSize(fontSize);
       if(align)
         range.setHorizontalAlignment(align);
     }, 
     setNumberFormat: function(strRange){
       setFormat(strRange, '0');       
     },
     setDecimalFormat: function(strRange){
       setFormat(strRange, '0.00');
     },
     setCurrencyFormat: function(strRange){
       // e.g: setCurrencyFormat('A1'); // set cell
       // e.g: setCurrencyFormat('A1:A10'); // set range
       
       setFormat(strRange, '$0.00');
     },
     setCellSumFormula: function(strRange, strCell){
       // e.g: setCellSumFormula('A1:A10', 'B1'); 
       // set SUM value of cells A1 to A10 to cell B1
       
       var cell = _sheet.getRange(strCell);
       cell.setFormula('=SUM(' + strRange + ')');
     },
     setCellAvgFormula: function(strRange, strCell){
       // e.g: setCellSumFormula('A1:A10', 'B1'); 
       // set AVG value of cells A1 to A10 to cell B1
       
       var cell = _sheet.getRange(strCell);
       cell.setFormula('=AVERAGE(' + strRange + ')');
     }       
   }; 
}

/* SPREADSHEET LOADER */
var SpreadsheetLoader = {
  createSpreadSheet: function(spreadSheetName, folderName){
    Logger.log('CREATING %s ... ', spreadSheetName);
    var spreadsheet = SpreadsheetApp.create(spreadSheetName); // create new file         
    
    if(!folderName || folderName == '') 
      return spreadsheet; // folder not specified  - return spreadsheet
     
    // save in specific folder 
    
    for(var i=0;i<500000; i++); // delay

    var root_folder = DriveApp.getRootFolder();     
    var folder_iterator = root_folder.getFoldersByName(folderName);
    var folderExists = folder_iterator.hasNext();
    
    if(!folderExists) // no such folder - return the spreadsheet
    {
      Logger.log('%s NOT EXISTS!', folderName);
      return spreadsheet;
    }
    
    var folder = root_folder.getFoldersByName(folderName).next();
    var file = root_folder.getFilesByName(spreadSheetName).next();
    folder.addFile(file);
    root_folder.removeFile(file);

    return SpreadsheetApp.openById(file.getId());    
  }, 
  loadSpreadSheet: function(spreadSheetName, folderName){
	if(spreadSheetName == '') {
      Logger.log('EMPTY NAME!');
      return null;
    }

    var root_folder = DriveApp.getRootFolder();
    var folder = root_folder; // default 
    if(folderName && folderName != '')
    {
      var folder_iterator = root_folder.getFoldersByName(folderName);
      var folderExists = folder_iterator.hasNext();
      
      if(folderExists)
      {
        Logger.log('FOLDER %s', folderName);
        folder = root_folder.getFoldersByName(folderName).next();
      }
    }
    
    var file_iterator = folder.getFilesByName(spreadSheetName);
    var fileExists = file_iterator.hasNext();
    
    if(!fileExists){
      Logger.log('%s NOT EXISTS!', spreadSheetName);
      return null;
    }
    
    // file exists - load it
    Logger.log('LOADING %s ... ', spreadSheetName);
    var file = file_iterator.next();
    return SpreadsheetApp.openById(file.getId());    
  }, 
  loadSpreadSheetById: function(spreadSheet_id){
    if(spreadSheet_id == '') {
      Logger.log('EMPTY ID!');
      return null;
    }
    
    var file = DriveApp.getFileById(spreadSheet_id);
    if(!file || file.isTrashed()){
      Logger.log('%s NOT EXISTS!', spreadSheet_id);
      return null;
    }
    
    // file exists - load it
    Logger.log('LOADING %s ... ', spreadSheet_id);
    return SpreadsheetApp.openById(file.getId());    
  },  
  loadSheetService: function(spreadSheetName, folderName){
    var spreadsheet = this.loadSpreadSheet(spreadSheetName, folderName);
    if(!spreadsheet) return null;
    return SheetService(spreadsheet.getActiveSheet());
  }, 
  loadSheetServiceById: function(spreadSheet_id){
    var spreadsheet = this.loadSpreadSheetById(spreadSheet_id);
    if(!spreadsheet) return null;
    return SheetService(spreadsheet.getActiveSheet());
  },  
  loadSpreadSheetService: function(spreadSheetName, folderName){
    var spreadsheet = this.loadSpreadSheet(spreadSheetName, folderName);
    if(!spreadsheet) return null;
    return SpreadSheetService(spreadsheet);
  },
  loadSpreadSheetServiceById: function(spreadSheet_id){
    var spreadsheet = this.loadSpreadSheetById(spreadSheet_id);
    if(!spreadsheet) return null;
    return SpreadSheetService(spreadsheet);
  },
  createSheetService: function(spreadSheetName, folderName){
    var spreadsheet = this.createSpreadSheet(spreadSheetName, folderName);
    if(!spreadsheet) return null;
    return SheetService(spreadsheet.getActiveSheet());
  },
  createSpreadSheetService: function(spreadSheetName, folderName){
    var spreadsheet = this.createSpreadSheet(spreadSheetName, folderName);
    if(!spreadsheet) return null;
    return SpreadSheetService(spreadsheet);
  }
}
    

 

Third Script – Handling Non-Converting Ads:

– Script name: R) Pause Non Converting or High Cost Ads)

– Script purpose: To pause ads that do not convert or have a high cost per conversion.

– Account-level script.

 

You can copy the script from here:


    var PERIOD = 'ALL_TIME'; // ALL_TIME, LAST_30_DAYS

function main() {
  HandleNonConvertingAds();
  HandleConvertingAds();
}

function HandleNonConvertingAds(){ 
  Logger.log('## Non Converting Ads ##')
  var ads = AdWordsApp.ads()
     .withCondition('ConvertedClicks = 0')
     .withCondition('Status = ENABLED')
     .withCondition('Cost > 0')  
     .forDateRange(PERIOD)
     .orderBy('Cost ASC')
     .get();
  
  while (ads.hasNext()) {
    var ad = ads.next();
    var adGroup = ad.getAdGroup();
    var adGroupCPA = GetAdGroupCPA(adGroup);
    var adCost = ad.getStatsFor(PERIOD).getCost();
    
    Logger.log('ad: %s -> cost %s', ad.getHeadline(), adCost);
    Logger.log('adGroup %s -> CPA %s', adGroup.getName(), adGroupCPA);
        
    if(adGroupCPA == null || adCost < adGroupCPA * 2) // no changes require
      continue;
    
    // adCost is too high - pause the ad 
    if(!AdWordsApp.getExecutionInfo().isPreview())
      ad.pause();
    Logger.log('PAUSE THE AD!');
  }
}

function GetAdGroupCPA(adGroup){
  var stats = adGroup.getStatsFor(PERIOD);
  var conversions = stats.getConvertedClicks();
  return conversions == 0 ? 0 : (stats.getCost() / conversions); // calculate the ad "CPA" - cost per converted
}

function HandleConvertingAds(){ 
  Logger.log('## Converting Ads ##')
  var ads = AdWordsApp.ads()
     .withCondition('ConvertedClicks > 0')
     .withCondition('Status = ENABLED')
     .withCondition('Cost > 0')  
     .forDateRange(PERIOD)
     .orderBy('ConvertedClicks DESC')
     .get();
  
  while (ads.hasNext()) {
    var ad = ads.next();
    var adGroup = ad.getAdGroup();
    var adGroupCPA = GetAdGroupCPA(adGroup);
    var stats = ad.getStatsFor(PERIOD);
    var adCPA = stats.getCost() / stats.getConvertedClicks(); // calculate the ad "CPA" - cost per converted
    
    Logger.log('ad: %s -> Conversions %s, CPA %s', ad.getHeadline(), stats.getConvertedClicks(), adCPA)
    Logger.log('adGroup %s -> CPA %s', adGroup.getName(), adGroupCPA);
        
    if(adGroupCPA == null || adCPA < adGroupCPA * 4) // no changes require
      continue;
    
    // adCost is too high - pause the ad 
    if(!AdWordsApp.getExecutionInfo().isPreview())
      ad.pause();
    Logger.log('PAUSE THE AD!');
  }
}
    

Fourth Script – Hidden Keywords:

– Script name: (R) queries 2 keywords – mcc (set)

– Script purpose: To take search queries that actually converted but do not appear as keywords.

– Explanation: The original keyword match type was phrase match or BMM.

– MCC-level script.

 

You can copy the script from here:


function main() {
  var accounts = MccApp.accounts().get();

  while (accounts.hasNext()) {
    var account = accounts.next();
    var accountName = account.getName() ? account.getName() : '--';
    Logger.log('account #%s %s', account.getCustomerId(), accountName);

    MccApp.select(account); 
    execute();    
  }
}

function execute() {
  var report = AdWordsApp.report(
   'SELECT Query,Clicks,ConvertedClicks,AverageCpc ' +
   'FROM SEARCH_QUERY_PERFORMANCE_REPORT ' +
   'WHERE ConvertedClicks > 0 ' + 
   'DURING 20000101,' + dateFormat(new Date()));
 
  var rows = report.rows();  
  while(rows.hasNext()) {
    var row = rows.next();
    
    var clicks = row['Clicks'];
    var conversions = row['ConvertedClicks'];

    var query = row['Query'];
    //Logger.log(query + ' ' + clicks);
    var found_in_my_keywords = AdWordsApp.keywords().withCondition('Text CONTAINS "' + query + '"').get().hasNext();
    if(found_in_my_keywords)
    {
      Logger.log('"' + query + '" found in my keywords');
      continue;
    }
    
    var adGroup = AdWordsApp.adGroups().get().next();
    var maxcpc = row['AverageCpc'];
    
    if(!AdWordsApp.getExecutionInfo().isPreview())
      adGroup.createKeyword(query, maxcpc);
    Logger.log('"' + query + '" added as keyword with maxcpc ' + maxcpc)
  }
}

function dateFormat(date){
  var year = date.getFullYear().toString();
  var month = date.getMonth().toString();
  var day = date.getDate().toString();
  
  if(month.length == 1) month = '0' + month; // return yyyyMMdd
  if(day.length == 1) day = '0' + day;
  
  return year + month + day;
}
    

Additional Examples

– E-commerce website: monitoring active ads and pausing the ad when the sale ends – reading data from HTML

– Full campaign management based on a feed (Excel, XML, or JSON), from creating a new ad when a new product arrives, for example: “Only today Product X is on sale”, stopping the ad when out of stock, updating the price within the ad

– Budget management to reach exactly the defined budget, no more and no less.

– Data injection into ads

– Alert system for campaign issues

Summary

The purpose of rules, experiments, and scripts is to help us manage the account automatically and create workflows that will improve our performance. I recommend that everyone use rules for automated actions, experiments to try new things without harming performance, and scripts for cases where there are no suitable rules
 

Avishai Freund

  • Manager and Owner of Logos Internet Marketing
  • Expert in marketing, advertising, and online promotion
  • Consults and accompanies institutional bodies and private entities in all matters related to the internet medium
  • Google Certified Professional – Google Advertising Professional
  • Senior lecturer at universities and colleges specializing in internet marketing and web technologies
  • Serves as a court-appointed expert in the field of advertising and marketing.
  • Holds an MBA in Business Administration from the School of Management, Faculty of Engineering, Ben-Gurion University of the Negev, Beer Sheva
Google Ads campaign optimization dashboard showing active rules and experiments