Getting Started With 4 Easy AdWords Scripts

December 5, 2013
AdWords scripts key to AdWords success

If you aren’t already using scripts in your AdWords account, you’re missing out. The possibilities of what you can do with them are only limited by your creativity (and coding skills, obviously). Even for JavaScript rookies like myself, there are still easy scripts that can make your life easier.

What is an AdWords script?

AdWords scripts allow a user to programmatically access their AdWords account using JavaScript. Like I said above, what you can do with this is only limited by your creativity.

For example, let’s say you own a sports apparel store that specializes in NBA swag and you’re advertising jerseys using AdWords. During the NBA season, there are multiple games every day. Using JavaScript, you could write code that dynamically adjust bids higher for the teams that won. Brilliant, right?

Inputting the scripts

This is where we’ll start. You’ll find the “Create and Manage Scripts” menu option concealed under “Automate” on the campaigns tab.

After navigating here, you’ll end up on a page where you can create, schedule, and manage all of your scripts. From here, select the “+ Script” button shown below.

Now you’ll be shown the box where you can easily copy and paste any AdWords scripts you have. Remember to delete what shows up in the box by default when pasting a script.

And finally, when you’re done adding your script and making the necessary changes, go ahead and save it. After you save, you can navigate back to the scripts menu and schedule your script.

 

Four easy-to-use AdWords scripts

My NBA example is complicated; the simpler scripts below are just four of my favorites that I use regularly. All of them require little programming knowledge and are essentially “plug and play.” If you’re hungry for more, my favorite AdWords scripts blog has a plethora of them to choose from. The source code for each script is found underneath setup instructions.

Daily account summary report

The origins of this script can be found directly from Google. This one is my favorite because I can check it every morning, wherever I am. Since it updates to a Google Spreadsheet, even checking it on your smartphone is nice. It gives a nice snapshot of performance from an account level.

Setup

This one does involve some initial setup. But getting it going is simple.

  1. First, make a copy of this spreadsheet in your Drive account. (File > Make a copy…)
  2. Following the instructions above, create a new script then copy and paste the source code below.
  3. Be sure to update “SPREADSHEET_URL” with the URL of your new template.
  4. Lastly, to be sure you have it updating every morning, it has to be scheduled to run every day at a set time. Google recommends scheduling it at 5:00am, because there sometimes can be a delay in the statistics. 
var SPREADSHEET_URL = "[YOUR_URL]";

function main() {
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  spreadsheet.getRangeByName("account_id_report").setValue(AdWordsApp.currentAccount().getCustomerId());  
  
  var yesterday = getYesterday();
  var last_check = spreadsheet.getRangeByName("last_check").getValue();
  // Checks every day from last day checked to yesterday inclusive. If there isn't a last date checked,
  // checks yesterday.
  var date;
  if (last_check.length == 0) {
    date = new Date(yesterday);
  } else {
    date = new Date(last_check); 
    date.setDate(date.getDate() + 1);
  }
  
  var rows = [];
  
  while (date.getTime() <= yesterday.getTime()) {
    var row = getReportRowForDate(date);
    rows.push([new Date(date), row['Cost'], row['AverageCpc'], row['Ctr'], row['AveragePosition'], row['Impressions'], row['Clicks']]);
    date.setDate(date.getDate() + 1);
  }
  
  if (rows.length > 0) {
    var access = new SpreadsheetAccess(SPREADSHEET_URL, "Report");
    var emptyRow = access.findEmptyRow(6, 2);
    if (emptyRow < 0) {
      access.addRows(rows.length);
      emptyRow = access.findEmptyRow(6, 2);
    }
    access.writeRows(rows, emptyRow, 2);
    
    var last_check = spreadsheet.getRangeByName("last_check").setValue(yesterday);
    var email = spreadsheet.getRangeByName("email").getValue();
  
    if (email) {
      sendEmail(email);
    }
  }
}

function sendEmail(email) {
  var day = getYesterday();
  var yesterdayRow = getReportRowForDate(day);
  day.setDate(day.getDate() - 1);
  var twoDaysAgoRow = getReportRowForDate(day);
  day.setDate(day.getDate() - 5);
  var weekAgoRow = getReportRowForDate(day);
  
  var html = [];
  html.push(
    "<html>",
      "<body>", 
        "<table width=800 cellpadding=0 border=0 cellspacing=0>", 
          "<tr>", 
            "<td colspan=2 align=right>", 
              "<div style='font: italic normal 10pt Times New Roman, serif; margin: 0; color: #666; padding-right: 5px;'>Powered by AdWords Scripts</div>", 
            "</td>", 
          "</tr>", 
          "<tr bgcolor='#3c78d8'>", 
            "<td width=500>", 
              "<div style='font: normal 18pt verdana, sans-serif; padding: 3px 10px; color: white'>Account Summary report</div>", 
            "</td>", 
            "<td align=right>", 
              "<div style='font: normal 18pt verdana, sans-serif; padding: 3px 10px; color: white'>", AdWordsApp.currentAccount().getCustomerId(), "</h1>", 
            "</td>", 
            "</tr>", 
          "</table>", 
          "<table width=800 cellpadding=0 border=0 cellspacing=0>", 
            "<tr bgcolor='#ddd'>", 
              "<td></td>", 
              "<td style='font: 12pt verdana, sans-serif; padding: 5px 0px 5px 5px; background-color: #ddd; text-align: left'>Yesterday</td>", 
              "<td style='font: 12pt verdana, sans-serif; padding: 5px 0px 5px 5px; background-color: #ddd; text-align: left'>Two Days Ago</td>", 
              "<td style='font: 12pt verdana, sans-serif; padding: 5px 0px 5x 5px; background-color: #ddd; text-align: left'>A week ago</td>", 
            "</tr>",
            emailRow('Cost', 'Cost', yesterdayRow, twoDaysAgoRow, weekAgoRow),
            emailRow('Average Cpc', 'AverageCpc', yesterdayRow, twoDaysAgoRow, weekAgoRow),
            emailRow('Ctr', 'Ctr', yesterdayRow, twoDaysAgoRow, weekAgoRow),
            emailRow('Average Position', 'AveragePosition', yesterdayRow, twoDaysAgoRow, weekAgoRow),
            emailRow('Impressions', 'Impressions', yesterdayRow, twoDaysAgoRow, weekAgoRow),
            emailRow('Clicks', 'Clicks', yesterdayRow, twoDaysAgoRow, weekAgoRow),
        "</table>",
      "</body>",
    "</html>");
  MailApp.sendEmail(email, "AdWords Account " + AdWordsApp.currentAccount().getCustomerId() + " Summary Report", "", {htmlBody: html.join("\n")});
}

function emailRow(title, column, yesterdayRow, twoDaysAgoRow, weekAgoRow) {
  return "<tr> \
        <td style='padding: 5px 10px'>" + title + "</td> \
        <td style='padding: 0px 10px'>" + yesterdayRow[column] + "</td> \
        <td style='padding: 0px 10px'>" + twoDaysAgoRow[column] + formatChangeString(yesterdayRow[column], twoDaysAgoRow[column]) + "</td> \
        <td style='padding: 0px 10px'>" + weekAgoRow[column] + formatChangeString(yesterdayRow[column], weekAgoRow[column]) + "</td> \
      </tr>";
}

// returns noon in the timezone of the account
function getYesterday() {
  var now = new Date(Utilities.formatDate(new Date(), 
      AdWordsApp.currentAccount().getTimeZone(), "MMM dd,yyyy HH:mm:ss"));
  var yesterday = new Date(now.getTime() - 24 * 3600 * 1000);
  yesterday.setHours(12);
  return yesterday;
}

function getReportRowForDate(date) {
  var accountDate = new Date(Utilities.formatDate(date,
      AdWordsApp.currentAccount().getTimeZone(), "MMM dd,yyyy HH:mm:ss"));
  var dateString = Utilities.formatDate(accountDate, "PST", "yyyyMMdd");
  return getReportRowForDuring(dateString + "," + dateString);
}

function getReportRowForDuring(during) {
  var report = AdWordsApp.report(
      "SELECT Cost, AverageCpc, Ctr, AveragePosition, Impressions, Clicks " +
      "FROM ACCOUNT_PERFORMANCE_REPORT " +
      "DURING " + during);
  return report.rows().next();
}

function formatChangeString(newValue,  oldValue) {
  var x = newValue.indexOf('%');
  if (x != -1) {
    newValue = newValue.substring(0, x);
    var y = oldValue.indexOf('%');
    oldValue = oldValue.substring(0, y);
  }
  
  var change = parseFloat(newValue - oldValue).toFixed(2);
  var changeString = change;
  if (x != -1) {
    changeString = change + '%';
  }
  
  if (change >= 0) {
    return "<span style='color: #38761d; font-size: 8pt'> (+" + changeString + ")</span>"; 
  } else {
    return "<span style='color: #cc0000; font-size: 8pt'> (" + changeString +  ")</span>"; 
  }
}

function SpreadsheetAccess(spreadsheetUrl, sheetName) {
  this.spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
  this.sheet = this.spreadsheet.getSheetByName(sheetName);
  
  // what column should we be looking at to check whether the row is empty?
  this.findEmptyRow = function(minRow, column) {
    var values = this.sheet.getRange(minRow, column, this.sheet.getMaxRows(), 1).getValues();
    for (var i = 0; i < values.length; i ++) {
      if (!values[i][0]) {
        return i + minRow;
      }
    }
    return -1;
  }
  this.addRows = function(howMany) {
    this.sheet.insertRowsAfter(this.sheet.getMaxRows(), howMany);
  }
  this.writeRows = function(rows, startRow, startColumn) {
    this.sheet.getRange(startRow, startColumn, rows.length, rows[0].length).setValues(rows);
  }
}

 

Search query performance report

Looking at your search queries at least once a week is something that you should be doing to look for more potential negative and positive keywords. This script pulls search query performance over the last seven days, outputs it in a Google Doc, and emails a user-defined list of email addresses when it’s finished. The author of this script is Russ Savage and you can find all of his awesome scripts on his site.

Setup

The setup here is fairly straightforward. Create a new Google Doc, input your Google Doc URL and email(s), then you’re good to go.

//-----------------------------------
// Store Search Query Perf Report in Google Doc
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  var date_range = 'LAST_7_DAYS';
  var ignore_exact = true;
  var to = ["email_1@my_company.com","email_2@my_company.com"];
  var spreadsheet_url = "your spreadsheet url goes here";
   
  var columns = ['AccountDescriptiveName',
                 'CampaignName',
                 'AdGroupName',
                 'KeywordTextMatchingQuery',
                 'MatchType',
                 'Query',
                 'Device',
                 'Impressions',
                 'Clicks',
                 'Cost',
                 'Conversions',
                 'AverageCpc',
                 'CostPerConversion',
                 'ConversionRate',
                 'Ctr'];
  var columns_str = columns.join(',') + " ";
   
  var sheet = getSpreadsheet(spreadsheet_url).getActiveSheet();
  sheet.clear();
  sheet.appendRow(columns);
   
  var report_iter = AdWordsApp.report(
    'SELECT ' + columns_str +
    'FROM SEARCH_QUERY_PERFORMANCE_REPORT ' +
    'DURING ' + date_range, {
      includeZeroImpressions: false,
      apiVersion: 'v201302'
    }).rows();
   
  while(report_iter.hasNext()) {
    var row = report_iter.next();
    if(ignore_exact && row['MatchType'].indexOf('exact') >= 0) { continue; }
    var row_array = [];
    for(var i in columns) {
       row_array.push(row[columns[i]]);
    }
    sheet.appendRow(row_array); 
  }
   
  for(var i in to) {
    MailApp.sendEmail(to[i], "Search Query Report Ready", spreadsheet_url);
  }
} 
 
function getSpreadsheet(spreadsheetUrl) {
  var matches = new RegExp('key=([^&#]*)').exec(spreadsheetUrl);
  if (!matches || !matches[1]) {
    throw 'Invalid spreadsheet URL: ' + spreadsheetUrl;
  }
  var spreadsheetId = matches[1];
  return SpreadsheetApp.openById(spreadsheetId);
}

 

Track account, campaign, and ad group level Quality Scores

If you’re unsure about what exactly Quality Score is, I would recommend reading about how Quality Score is calculated and everything that goes into Quality Score. This script is also from Russ Savage, and it does something that Google doesn’t allow you to do: view Quality Scores over time.

It’s arguable whether there’s actionable insight available in doing this, but it’s always nice to have the data available. This script looks at the top 50,000 keywords in your account. (As a sidenote, re-running the script does not overwrite the previous entries.) 

Setup

Create a new Google Spreadsheet and copy the URL in the script. Once you’ve done that, copy the script into AdWords and schedule it to run every 30 days.

/************************************
* Store Account, Campaign, and AdGroup Level Quality Score
* Version 1.2
* ChangeLog v1.2
*  - Changed status to ENABLED
* ChangeLog v1.1
*  - Added APPEND option
*  - Added ability to create spreadsheet sheets
*  - Updated logic for faster spreadsheet insertion
* Created By: Russ Savage
* FreeAdWordsScripts.com
**************************************/
var SIG_FIGS = 10000; //this will give you 4 decimal places of accuracy
var APPEND = true; //set this to false to overwrite your data daily
  
function main() {
  var SPREADSHEET_URL = "PUT YOUR SPREADSHEET HERE";
  var date_str = Utilities.formatDate(new Date(),AdWordsApp.currentAccount().getTimeZone(),'yyyy-MM-dd');
  var account_id = AdWordsApp.currentAccount().getCustomerId();
    
  var kw_iter = AdWordsApp.keywords()
    .withCondition("Status = ENABLED")
    .forDateRange("LAST_30_DAYS")
    .withCondition("Impressions > 0")
    .orderBy("Impressions DESC")
    .withLimit(50000)
    .get();
   
  //This is where i am going to store all my data
  var account_score_map = {};
  var camp_score_map = {};
  var ag_score_map = {};
     
  while(kw_iter.hasNext()) {
    var kw = kw_iter.next();
    var kw_stats = kw.getStatsFor("LAST_30_DAYS");
    var imps = kw_stats.getImpressions();
    var qs = kw.getQualityScore();
    var camp_name = kw.getCampaign().getName();
    var ag_name = kw.getAdGroup().getName();
    var imps_weighted_qs = (qs*imps);
    _loadEntityMap(account_score_map,account_id,imps_weighted_qs,imps);
    _loadEntityMap(camp_score_map,camp_name,imps_weighted_qs,imps);
    _loadEntityMap(ag_score_map,camp_name + "~~!~~" + ag_name,imps_weighted_qs,imps);
  }
  
  //Make sure the spreadsheet has all the sheets
  _addSpreadsheetSheets(SPREADSHEET_URL,['Account','Campaign','AdGroup']);
   
  //Load Account level QS
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName('Account');
  _addHeadingsIfNeeded(sheet,['Date','Account','QS']);
  var e = account_score_map[account_id];
  sheet.appendRow([date_str,account_id,Math.round(e.imps_weighted_qs / e.tot_imps * SIG_FIGS)/SIG_FIGS]);
    
  //Load Campaign level QS
  sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName('Campaign');
  _addHeadingsIfNeeded(sheet,['Date','Account','Campaign','QS']);
  var to_write = [];
  for(var i in camp_score_map) {
    var e = camp_score_map[i];
    to_write.push([date_str,
                   account_id,
                   i,
                   Math.round(e.imps_weighted_qs / e.tot_imps * SIG_FIGS)/SIG_FIGS]);
  }
  _writeDataToSheet(sheet,to_write);
    
  //Load Campaign + AdGroup level QS
  sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName('AdGroup');
  _addHeadingsIfNeeded(sheet,['Date','Account','Campaign','AdGroup','QS']);
  to_write = [];
  for(var i in ag_score_map) {
    var e = ag_score_map[i];
    to_write.push([date_str,
                   account_id,
                   i.split('~~!~~')[0],
                   i.split('~~!~~')[1],
                   Math.round(e.imps_weighted_qs / e.tot_imps * SIG_FIGS)/SIG_FIGS]);
  }
  _writeDataToSheet(sheet,to_write);
}
 
// Super fast spreadsheet insertion
function _writeDataToSheet(sheet,to_write) {
  var last_row = sheet.getLastRow();
  var numRows = sheet.getMaxRows();
  if((numRows-last_row) < to_write.length) {
    sheet.insertRows(last_row+1,to_write.length-numRows+last_row); 
  }
  var range = sheet.getRange(last_row+1,1,to_write.length,to_write[0].length);
  range.setValues(to_write);
}
 
// Helper function to add the sheets  to the spreadsheet if needed
function _addSpreadsheetSheets(url,sheet_names) {
  var spreadsheet = SpreadsheetApp.openByUrl(url);
  var all_sheets = spreadsheet.getSheets();
  var all_sheet_names = [];
  for(var i in all_sheets) {
    all_sheet_names.push(all_sheets[i].getName());
  }
   
  for(var i in sheet_names) {
    var name = sheet_names[i];
    if(all_sheet_names.indexOf(name) == -1) {
      spreadsheet.insertSheet(name);
    } else {
      if(!APPEND) {
        spreadsheet.getSheetByName(name).clear();
      }
    }
  }
}
 
// Helper function to load the map for storing data
function _loadEntityMap(map,key,imps_weighted_qs,imps) {
  if(!map[key]) {
    map[key] = { imps_weighted_qs : imps_weighted_qs, tot_imps : imps };
  } else {
    map[key].imps_weighted_qs += imps_weighted_qs;
    map[key].tot_imps += imps;
  }
}
  
//Helper function to add headers to sheet if needed
function _addHeadingsIfNeeded(sheet,headings) {
  if(sheet.getRange('A1:A1').getValues()[0][0] == "") {
    sheet.clear();
    sheet.appendRow(headings);
  }
}

 

Ad, ad group, keyword, or campaign creation date

Yet another script from the Russ Savage gold mine. Because Google doesn’t actually store when these entities were created, the script uses the day it first started receiving impressions, which generally is the day it was created.

This is most useful for A/B testing and seeing how long specific ads have been running. When A/B testing, it’s always important to leave enough time to collect data that can become actionable insight. Having a label showing the date created helps to make sure you don’t make changes to your account too soon (or too late). 

Setup

As far as setting up goes, this one is by far the easiest. Copy and paste the code in a new script and define which entity you want to track in the “ENTITY” line (if you want to track all of them, make a new script for each one). And you’re done.

Depending on your account size, this script can take some time to complete so be patient!

/**************************************
* Track Entity Creation Date
* Version 1.4
* Changelog v1.4
*  - Removed apiVersion from reporting call
* Changelog v1.3
*  - Updated script to handle all entities
* Changelog v1.2
*  - Fixed an issue with comparing dates
* ChangeLog v1.1
*  - Updated logic to work with larger accounts
* Created By: Russ Savage
* http://www.FreeAdWordsScripts.com
**************************************/
//All my labels will start with this. For example: Created:2013-05-01
var LABEL_PREFIX = 'Created:';
var DAYS_IN_REPORT = 30;
var ENTITY = 'ad'; //or adgroup or keyword or campaign
  
function main() {
  //First we get the impression history of our entity
  var ret_map = getImpressionHistory();
  //Then we apply our labels
  applyLabels(ret_map);
}
  
//Function to apply labels to the ads in an account
function applyLabels(ret_map) {
  var iter;
  if(ENTITY === 'campaign') { iter = AdWordsApp.campaigns().get(); }
  if(ENTITY === 'adgroup') { iter = AdWordsApp.adGroups().get(); }
  if(ENTITY === 'ad') { iter = AdWordsApp.ads().get(); }
  if(ENTITY === 'keyword') { iter = AdWordsApp.keywords().get(); }
   
  while(iter.hasNext()) {
    var entity = iter.next();
    var id = entity.getId();
    if(ret_map[id]) {
      var label_name = LABEL_PREFIX+Utilities.formatDate(ret_map[id], AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
      createLabelIfNeeded(label_name);
      entity.applyLabel(label_name);
    }
  }
}
  
//This is a helper function to create the label if it does not already exist
function createLabelIfNeeded(name) {
  if(!AdWordsApp.labels().withCondition("Name = '"+name+"'").get().hasNext()) {
    AdWordsApp.createLabel(name);
  }
}
  
//A helper function to find the date days ago
function getDateDaysAgo(days) {
  var the_past = new Date();
  the_past.setDate(the_past.getDate() - days);
  return Utilities.formatDate(the_past,AdWordsApp.currentAccount().getTimeZone(),"yyyyMMdd");
}
  
//A helper function to compare dates.
//Copied from: http://goo.gl/uW48a
function diffDays(firstDate,secondDate) {
  var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
  return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime())/(oneDay))); 
}
  
function getImpressionHistory() {
  var API_VERSION = { includeZeroImpressions : false };
  var first_date = new Date('10/23/2000');
  var max_days_ago = diffDays(first_date,new Date());
  var cols = ['Date','Id','Impressions'];
  var report = { 
    'campaign' : 'CAMPAIGN_PERFORMANCE_REPORT',
    'adgroup' : 'ADGROUP_PERFORMANCE_REPORT',
    'ad' : 'AD_PERFORMANCE_REPORT',
    'keyword' : 'KEYWORDS_PERFORMANCE_REPORT'}[ENTITY];
  var ret_map = {};
  var prev_days_ago = 0;
  for(var i = DAYS_IN_REPORT; i < max_days_ago; i+=DAYS_IN_REPORT) {
    var start_date = getDateDaysAgo(i);
    var end_date = getDateDaysAgo(prev_days_ago);
    var date_range = start_date+','+end_date;
    Logger.log('Getting data for ' + date_range);
    var query = ['select',cols.join(','),'from',report,'during',date_range].join(' ');
    var report_iter = AdWordsApp.report(query, API_VERSION).rows();
    if(!report_iter.hasNext()) { Logger.log('No more impressions found. Breaking.'); break; } // no more entries
    while(report_iter.hasNext()) { 
      var row = report_iter.next();
      if(ret_map[row['Id']]) {
        var [year,month,day] = (row['Date']).split('-');
        var from_row = new Date(year, parseFloat(month)-1, day);
        var from_map = ret_map[row['Id']];
          
        if(from_row < from_map) {
          ret_map[row['Id']] = from_row;
        }
      } else {
        var [year,month,day] = (row['Date']).split('-');
        ret_map[row['Id']] = new Date(year, parseFloat(month)-1, day);
      }
    }
    prev_days_ago = i;
  }
  return ret_map;
}

 

Reaching success with Google AdWords

Staying ahead of the curve with pay-per-click advertising can be difficult. But leveraging the latest techniques in the best way can add to the challenge.

If you’re having trouble getting your account to perform like a superstar, Digett is here for you! We have experience helping clients of all sizes get the most for their business out of AdWords. Contact us today for a free consultation to get started! 

Comments

i tried running the script

i tried running the script "Search query performance report".

But it says there's an error...

Invalid reporting query: INVALID_INPUT: RequestError.INVALID_INPUT. (line 33)

did it run smoothly for you?

Yes, this runs smoothly for

Yes, this runs smoothly for me. The problem might be in the account you created your spreadsheet in. Is it the same Google account you manage the AdWords account with?

Hi, Great article. Is it

Hi, Great article. Is it possible to add any other metrics into the daily account summary, specifically Conversion value per Cost? I've tried adding rows into the code, but it won't show up in the spreadsheet.

Greg, thanks for reading! Can

Greg, thanks for reading!

Can you elaborate on the metric you're referring to? Perhaps you mean cost per conversion value? Sorry if I'm misunderstanding here.

A possible workaround you may want to try is to set up a formatted sheet on a different page, and have the cells in the new page reference where those metrics would be.

If you need more elaboration on that, please feel free to ask.

Hi Wes, I'm referring to

Hi Wes,

I'm referring to Conv. Value/Cost, we use it to calculate ROAS. It's not in the default columns in Adwords which may be why I'm having trouble.

I tried adding a column to the spreadsheet with the data label, and I went through the code and added ConversionValuePerCost wherever I saw the other metrics listed, but can't get it to show up in the report. I'm very new to coding, so it's probably an easy fix somewhere.

Greg, Let me look into this

Greg,

Let me look into this and I'll get back to you. That's an interesting idea you have bringing that in there.

You may also want to shoot Russ Savage an email, too. He's the king at AdWords scripts. He wrote a few of them I have listed above.