Blog Posts About


AdWords JavaScript

AdWords Broken URL Checker That Won’t Timeout Ever

A common problem with broken URL checker scripts is that they tend to timeout when running on large AdWords accounts. This is because Google would rather you use their API.

I’d love to start using the Google API and I even got my test MCC account up and running. The problem is that I’m not really at that level of coding comfort yet, so I’ve started playing with scripts. The following Broken URL Checker was my first time using JavaScript!

Don’t get me wrong, there’s a few great link checker AdWords scripts out there and some not so great ones.

Here’s one script that works perfectly fine on smaller AdWords account.

Google also has it’s own Link Checker script that checks 800 URLs every execution. It’s actually pretty neat but I haven’t tested it out.

In theory, Google’s Link Checker should also work if the Free AdWords Scripts keeps timing out. I decided to just write my own little version of his script that allows me to segment my account. In other words, instead of running 1 script to check all my destination pages for errors, I run multiple scripts that check a unique set of campaigns each.

It’s heavily inspired (partially stolen) from Russel Savage’s script but there are some key differences.

The most notable difference is the fact that I no longer take a look at keyword destination URLs. I don’t have that many in my account so I have a separate script that doesn’t use my custom campaign filter.

In fact, the filter in the AdGroup selector is the only other difference other than some comments that I just added.

My Broken URL Checker Code

* Broken URL Checker /w Segments
* Version 1.0
* Created by: Philip Tomlinson (@philtomm)
* Modified Version of Russ Savage's Broken URL Finder
function main() {
  // You can add more if you want:
  var BAD_CODES = [404,500];
  var TO = [''/* insert email in the quotes like this ''*/];
  var SUBJECT = 'Broken AdWords for Campaign URL Report - ' + _getDateString(); 
  //You may want to add what letters you're targeting
  var HTTP_OPTIONS = {
  // This is the start of my changes
  // Establishing variables for the alphabet filter
  var alphaIndex;
  var alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
  var iters = [];
  // This filter selects all campaigns starting with the letters A to G.
  for (alphaIndex = 0 /*starting letter number*/; alphaIndex < 7 /*number - 1 equals last letter */; ++alphaIndex){  
  var iterPush = [
      .withCondition("Status = 'ENABLED'")
      .withCondition("AdGroupStatus = 'ENABLED'")
      .withCondition("CampaignStatus = 'ENABLED'")
      .withCondition("Type = 'TEXT_AD'")
      .withCondition("CampaignName STARTS_WITH '" + alphabet[alphaIndex] + "'")
    var iters = iters.concat(iterPush);
  /* Everything else is from Russel Savage's post here: */
  var already_checked = {}; 
  var bad_entities = [];
  for(var x in iters) {
    var iter = iters[x];
    while(iter.hasNext()) {
      var entity =;
      if(entity.getDestinationUrl() == null) { continue; }
      var url = entity.getDestinationUrl();
      if(url.indexOf('{') >= 0) {
        //Let's remove the value track parameters
        url = url.replace(/{[0-9a-zA-Z]+}/g,'');
      if(already_checked[url]) { continue; }
      var response_code;
      try {
        Logger.log("Testing url: "+url);
        response_code = UrlFetchApp.fetch(url, HTTP_OPTIONS).getResponseCode();
      } catch(e) {
        //Something is wrong here, we should know about it.
        bad_entities.push({e : entity, code : -1});
      if(BAD_CODES.indexOf(response_code) >= 0) {
        //This entity has an issue.  Save it for later. 
        bad_entities.push({e : entity, code : response_code});
      already_checked[url] = true;
  var column_names = ['Type','CampaignName','AdGroupName','Id','Headline/KeywordText','ResponseCode','DestUrl'];
  var attachment = column_names.join(",")+"n";
  for(var i in bad_entities) {
    attachment += _formatResults(bad_entities[i],",");
  if(bad_entities.length > 0) {
    var options = { attachments: [Utilities.newBlob(attachment, 'text/csv', 'bad_urls_'+_getDateString()+'.csv')] };
    var email_body = "There are " + bad_entities.length + " urls that are broken. See attachment for details.";
    for(var i in TO) {
      MailApp.sendEmail(TO[i], SUBJECT, email_body, options);
//Formats a row of results separated by SEP
function _formatResults(entity,SEP) {
  var e = entity.e;
  if(typeof(e['getHeadline']) != "undefined") {
    //this is an ad entity
    return ["Ad",
  } else {
    // and this is a keyword
    return ["Keyword",
//Helper function to format todays date
function _getDateString() {
  return Utilities.formatDate((new Date()), AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");

N.B. I did not test my code after adding some comments. If this doesn’t work, please tell me in the comments! I’m a known typo machine.