Hide

Best Practices

This page covers various best practices that should be considered when developing applications with AdWords scripts.

Selectors

Filter with selectors

When possible, use filters to request only the entities you need. Applying proper filters has the following benefits:

  • The code is simpler and easier to understand.
  • The script will execute much faster.
  • Your script is less likely to run into a fetching limit.

Compare the following code snippets:

Coding approach Code snippet
Filter using selectors (recommended)
var keywords = AdWordsApp.keywords()
    .withCondition('Clicks > 10')
    .forDateRange('LAST_MONTH')
    .get();
while (keywords.hasNext()) {
  var keyword = keywords.next();
  // Do work here.
}
Filter in code (not recommended)
var keywords = AdWordsApp.keywords().get();

while (keywords.hasNext()) {
  var keyword = keywords.next();
  var stats = keyword.getStatsFor(
      'LAST_MONTH');
  if (stats.getClicks() > 10) {
    // Do work here.
  }
}

The second approach is not recommended because it attempts to retrieve the list of all the keywords in your account only to apply a filter to the list.

Avoid traversing the campaign hierarchy

When you want to retrieve entities at a particular level, use a collection method at that level instead of traversing the entire campaign hierarchy. In addition to being simpler, this will also perform much better: the system will not have to unnecessarily read in all the campaigns and ad groups.

Compare the following code snippets that retrieve all ads in your account:

Coding approach Code snippet
Use appropriate collection method (Recommended)

var ads = AdWordsApp.ads();

Traverse the hierarchy (Not recommended)
var campaigns = AdWordsApp.campaigns().get();
while (campaigns.hasNext()) {
  var adGroups = campaigns.next().
      adGroups().get();
  while (adGroups.hasNext()) {
    var ads = adGroups.next().ads().get();
    // Do your work here.
  }
}

The second approach is not recommended since it attempts to fetch entire hierarchies of objects (campaigns, ad groups) whereas only ads are required.

Use specific parent accessor methods

Sometimes you need to obtain a retrieved object's parent entity. In this case, you should use a provided accessor method instead of fetching entire hierarchies.

Compare the following code snippets that retrieve the ad groups that have text ads with more than 50 clicks last month:

Coding approach Code snippet
Use appropriate parent accessor method (recommended)
var ads = AdWordsApp.ads()
    .withCondition('Clicks > 50')
    .forDateRange('LAST_MONTH')
    .get();

while (ads.hasNext()) {
  var ad = ads.next();
  var adGroup = ad.getAdGroup();
  var campaign = ad.getCampaign();
  // Store (campaign, adGroup) to an array.
}
Traverse the hierarchy (not recommended)
var campaigns = AdWordsApp.campaigns().get();
while (campaigns.hasNext()) {
  var adGroups = campaigns.next()
      .adGroups()
      .get();
  while (adGroups.hasNext()) {
    var ads = adGroups.ads()
       .withCondition('Clicks > 50')
       .forDateRange('LAST_MONTH')
       .get();
    if (ads.totalNumEntities() > 0) {
      // Store (campaign, adGroup) to an array.
    }
  }
}

The second approach is not recommended since it fetches the entire campaign and ad group hierarchies in your account, whereas you need only a subset of campaigns and ad groups that is associated with your set of ads. The first approach restricts itself to fetch only the relevant ads collection, and uses an appropriate method to access its parent objects.

Use specific parent filters

For accessing entities within a specific campaign or ad group, use a specific filter in the selector instead of fetching then traversing through a hierarchy.

Compare the following code snippets that retrieve the list of text ads within a specified campaign and ad group having more than 50 clicks last month.

Coding approach Code snippet
Use appropriate parent level filters (recommended)
var ads = AdWordsApp.ads()
    .withCondition('CampaignName = "Campaign 1"')
    .withCondition('AdGroupName = "AdGroup 1"')
    .withCondition('Clicks > 50')
    .forDateRange('LAST_MONTH')
    .get();

while (ads.hasNext()) {
  var ad = ads.next();
  var adGroup = ad.getAdGroup();
  var campaign = ad.getCampaign();
  // Store (campaign, adGroup, ad) to
  // an array.
}
Traverse the hierarchy (not recommended)
var campaigns = AdWordsApp.campaigns()
    .withCondition('Name = "Campaign 1"')
    .get();

while (campaigns.hasNext()) {
  var adGroups = campaigns.next()
      .adGroups()
      .withCondition('Name = "AdGroup 1"')
      .get();
  while (adGroups.hasNext()) {
    var ads = adGroups.ads()
       .withCondition('Clicks > 50')
       .forDateRange('LAST_MONTH')
       .get();
    while (ads.hasNext()) {
      var ad = ads.next();
      // Store (campaign, adGroup, ad) to
      // an array.
    }
  }
}

The second approach is not recommended since it iterates on campaign and ad group hierarchy in your account, whereas you need only a selected set of ads, and their parent campaigns and ad groups. The first approach limits the iteration to the list of ads by applying a specific filter for parent entities on the selector.

Use IDs for filtering when possible

When filtering for entities, it is preferable to filter for entities by their IDs instead of other fields.

Consider the following code snippets that select a campaign.

Coding approach Code snippet
Filter by ID (recommended)
var campaign = AdWordsApp.campaigns()
    .withIds([12345])
    .get()
    .next();
Filter by Name (less optimal)
var campaign = AdWordsApp.campaigns()
    .withCondition('Name="foo"')
    .get()
    .next();

The second approach is less optimal since we are filtering by a non-ID field.

Filter by parental IDs whenever possible

When selecting an entity, filter by parent IDs whenever possible. This will make your queries faster by limiting the list of entities being retrieved by the servers when filtering results.

Consider the following code snippet that retrieves an AdGroup by its ID. Assume that the parent campaign ID is known.

Coding approach Code snippet
Filter by campaign and ad group IDs (recommended)
var adGroup = AdWordsApp.adGroups()
    .withIds([12345])
    .withCondition('CampaignId="54678"')
    .get()
    .next();
Filter by ad group ID alone (less optimal)
var adGroup = AdWordsApp.adGroups()
    .withIds([12345])
    .get()
    .next();

Even though both code snippets give identical results, the additional filtering in code snippet 1 using a parent ID (CampaignId="54678") makes the code more efficient by restricting the list of entities that the server has to iterate when filtering the results.

Don't run selectors in a tight loop

While AdWords scripts are efficient at fetching entities when a proper filter is applied, the benefit can get offset if you trigger too many selection operations using a very narrow selector.

Consider the following scripts that applies a "Moderate performance" label to keywords whose AveragePosition > 4 last month:

Coding approach Code snippet
Select and update keywords grouped by ad groups (recommended)
var labelText = 'Moderate performance';

var report = AdWordsApp.report('Select AdGroupId, Id, CpcBid from ' +
    'KEYWORD_PERFORMANCE_REPORT where AveragePosition > 4 ' +
    'DURING LAST_MONTH')
    .rows();

var map = {
};

while (reports.hasNext()) {
  var row = reports.next();
  var adGroupId = row['AdGroupId'];
  var id = row['Id'];

  if (map[adGroupId] == null) {
    map[adGroupId] = [];
  }
  map[adGroupId].push(id);
}

for (var key in map) {
  var keywords = AdWordsApp.keywords()
      .withCondition('AdGroupId="' + key + '"')
      .withIds(map[key])
      .get();

  while (keywords.hasNext()) {
    var keyword = keywords.next();
    keyword.applyLabel(labelText);
  }
}
Select and update keywords by their ID (less optimal)
var labelText = 'Moderate performance';

var report = AdWordsApp.report('Select AdGroupId, Id, CpcBid from ' +
    'KEYWORD_PERFORMANCE_REPORT where AveragePosition > 4 ' +
    'DURING LAST_MONTH')
    .rows();

var list = [];

while (reports.hasNext()) {
  var row = reports.next();

  var adGroupId = row['AdGroupId'];
  var id = row['Id'];

  list.push([adGroupId, Id]);
}

var keywords = AdWordsApp.keywords()
    .withIds(list)
    .get();

while (keywords.hasNext()) {
  var keyword = keywords.next();
  keyword.applyLabel(labelText);
}
Select and update keywords one at a time (not recommended)
var report = AdWordsApp.report('Select AdGroupId, Id, CpcBid from ' +
    'KEYWORD_PERFORMANCE_REPORT where AveragePosition > 4 ' +
    'DURING LAST_MONTH')
    .rows();

while (reports.hasNext()) {
  var row = reports.next();
  var adGroupId = row['AdGroupId'];
  var id = row['Id'];
  var keyword = AdWordsApp.keywords()
      .withIds([[AdGroupId, Id]])
      .get()
      .next();
  keyword.applyLabel(labelText);
}

The first approach gives you the best performance, because you are grouping your select and update operations by a specific ad group. The second approach is less optimal, but still gives you good performance, since you are restricting your select operation to a single get() call. The third approach gives you the worst performance. The performance gain you get by applying a very specific filter to optimize your get() calls is offset by the large number of get() calls you are making in a tight loop.

Use labels when there are too many filtering conditions

When you have too many filtering conditions, it is a good idea to create a label for the entities you process, and use that label to filter your entities.

Consider the following snippet of code that retrieves a list of campaigns by their name.

Coding approach Code snippet
Use a label (recommended)
var label = AdWordsApp.labels()
    .withCondition('Name = "My Label"')
    .get()
    .next();
var campaigns = label.campaigns.get();
while (campaigns.hasNext()) {
  var campaign = campaigns.next();
  // Do more work
}
Build complex selectors (not recommended)
var campaignNames = [‘foo’, ‘bar’, ‘baz’];

for (var i = 0; i < campaignNames.length; i++) {
  campaignNames[i] = '"' + campaignNames[i] + '"';
}

var campaigns = AdWordsApp.campaigns
    .withCondition('CampaignName in [' + campaignNames.join(',') + ']')
    .get();

while (campaigns.hasNext()) {
  var campaign = campaigns.next();
  // Do more work.
}

While both code snippets give you similar level of performance, the second approach tends to generate more complex code as the number of conditions in your selector increases. It is also easier to apply the label to a new entity than editing the script to include a new entity.

Limit the number of conditions in your IN clause

When running scripts, a common use case is to run a report for a list of entities. Developers usually accomplish this by constructing a very long AWQL query that filters on the entity IDs using an IN clause. This approach works fine when the number of entities are limited. However, as the length of your query increases, your script performance deteriorates due to two reasons:

  • A longer query takes longer to parse.
  • Each ID you add to an IN clause is an additional condition to evaluate, and hence takes longer.

Under such conditions, it is preferable to apply a label to the entities, and then filter by LabelId.

Coding approach Code snippet
Apply a label and filter by labelID (recommended)
// The label applied to the entity is "Report Entities"
var label = AdWordsApp.labels()
    .withCondition('LabelName contains "Report Entities"')
    .get()
    .next();

var report = AdWordsApp.report('Select AdGroupId, Id, Clicks, ' +
    'Impressions, Cost from KEYWORD_PERFORMANCE_REPORT ' +
    'where LabelId = "' + label.getId() + '"')
    .rows();
Build a long query using IN clause (not recommended)
var report = AdWordsApp.report('Select AdGroupId, Id, Clicks, ' +
    'Impressions, Cost from KEYWORD_PERFORMANCE_REPORT where ' +
    'AdGroupId IN (123, 456) and Id in (123,345, 456…)')
    .rows();

Account updates

Batch changes

When you make changes to an AdWords entity, AdWords scripts doesn’t execute the change immediately. Instead, it tries to combine multiple changes into batches, so that it can issue a single request that does multiple changes. This approach makes your scripts faster and reduces the load on AdWords servers. However, there are some code patterns that force AdWords scripts to flush its batch of operations frequently, thus causing your script to run slowly.

Consider the following script that updates the bids of a list of keywords.

Coding approach Code snippet
Keep track of updated elements (recommended)
var keywords = AdWordsApp.keywords()
    .withCondition('Clicks > 50')
    .withCondition('CampaignName = "Campaign 1"')
    .withCondition('AdGroupName = "AdGroup 1"')
    .forDateRange('LAST_MONTH')
    .get();

var list = [];
while (keywords.hasNext()) {
  var keyword = keywords.next();
  keyword.bidding().setCpc(1.5);
  list.push(keyword);
}

for (var i = 0; i < list.length; i++) {
  var keyword = list[i];
  Logger.log('%s, %s', keyword.getText(),
      keyword.bidding().getCpc());
}
Retrieve updated elements in a tight loop (not recommended)
var keywords = AdWordsApp.keywords()
    .withCondition('Clicks > 50')
    .withCondition('CampaignName = "Campaign 1"')
    .withCondition('AdGroupName = "AdGroup 1"')
    .forDateRange('LAST_MONTH')
    .get();

while (keywords.hasNext()) {
  var keyword = keywords.next();
  keyword.bidding().setCpc(1.5);
  Logger.log('%s, %s', keyword.getText(),
      keyword.bidding().getCpc());
}

The second approach is not recommended since the call to keyword.bidding().getCpc() forces AdWords scripts to flush the setCpc() operation and execute only one operation at a time. The first approach, while similar to the second approach, has the added benefit of supporting batching since the getCpc() call is done in a separate loop from the one where setCpc() is called.

Don't leave your selectors in an indeterminate state

When you update a list of entities, make sure your code doesn’t break the iterator’s selector condition as a side effect. This leaves the selector in an indeterminate state and may cause unexpected behavior.

Consider the following code snippet that pauses a list of keywords.

Coding approach Code snippet
Keep a separate list for entities (recommended)
var list = [];

var keywords = AdWordsApp.keywords()
    .withCondition('Status="ENABLED"')
    .get();

while (keywords.hasNext()) {
  var keyword = keywords.next();
  list.push(keyword);
}

for (var i = 0; i < list.length; i++) {
  list[i].pause();
}
Update the entities within the iterator loop (not recommended)
var keywords = AdWordsApp.keywords()
    .withCondition('Status="ENABLED"')
    .get();

while (keywords.hasNext()) {
  var keyword = keywords.next();
  keyword.pause();
}

The second code snippet may skip some keywords. This happens because you are pausing the keywords within the iterator (Status="PAUSED") and this breaks the original selector condition associated with the iterator (Status="ENABLED"). The first approach works since we are keeping track of the entities we want to modify, and updating them once we have iterated through all the entities.

Use builders when possible

AdWords scripts support two ways to create new objects - builders and creation methods. Builders are more flexible than creation methods, since it gives you access to the object that is created from the API call.

Consider the following code snippets:

Coding approach Code snippet
Use builders (recommended)
var operation = adGroup.newKeywordBuilder()
    .withText('shoes')
    .build();
var keyword = operation.getResult();
Use creation methods (not recommended)
adGroup.createKeyword('shoes');
var keyword = adGroup.keywords()
    .withCondition('KeywordText="shoes"')
    .get()
    .next();

The second approach is not preferred due to the extra selection operation involved in retrieving the keyword. In addition, creation methods are also deprecated.

However, keep in mind that builders, when used incorrectly, can prevent AdWords scripts from batching its operations.

Consider the following code snippets that create a list of keywords, and prints the ID of the newly created keywords:

Coding approach Code snippet
Keep track of updated elements (recommended)
var keywords = [‘foo’, ‘bar’, ‘baz’];

var list = [];
for (var i = 0; i < keywords.length; i++) {
  var operation = adGroup.newKeywordBuilder()
      .withText(keywords[i])
      .build();
  list.push(operation);
}

for (var i = 0; i < list.length; i++) {
  var operation = list[i];
  var result = operation.getResult();
  Logger.log('%s %s', result.getId(),
      result.getText());
}
Retrieve updated elements in a tight loop (not recommended)
var keywords = [‘foo’, ‘bar’, ‘baz’];

for (var i = 0; i < keywords.length; i++) {
  var operation = adGroup.newKeywordBuilder()
      .withText(keywords[i])
      .build();
  var result = operation.getResult();
  Logger.log('%s %s', result.getId(),
      result.getText());
}

The second approach is not preferred because it calls operation.getResult() within the same loop that creates the operation, thus forcing AdWords scripts to execute one operation at a time. The first approach, while similar, allows batching since we call operation.getResult() in a different loop than where it was created.

Consider using bulk uploads for large updates

A common task that developers perform is to run reports and update entity properties (e.g. keyword bids) based on current performance values. When you have to update a large number of entities, bulk uploads tend to give you better performance. For instance, consider the following scripts that increase the MaxCpc of keywords whose AveragePosition > 4 for the last month:

Coding approach Code snippet
Use bulk upload (recommended)
var report = AdWordsApp.report(
    'Select AdGroupId, Id, CpcBid from KEYWORD_PERFORMANCE_REPORT ' +
    'where AveragePosition > 4 DURING LAST_MONTH')
    .rows();

var upload = AdWordsApp.bulkUploads().newCsvUpload([
    report.getColumnHeader('AdGroupId').getBulkUploadColumnName(),
    report.getColumnHeader('Id').getBulkUploadColumnName(),
    report.getColumnHeader('CpcBid').getBulkUploadColumnName()]);
upload.forCampaignManagement();

while (reports.hasNext()) {
  var row = reports.next();
  row['CpcBid'] = row['CpcBid'] + 0.02;
  upload.append(row.formatForUpload());
}

upload.apply();
Select and update keywords by ID (less optimal)
var report = AdWordsApp.report('Select AdGroupId, Id, CpcBid from ' +
    'KEYWORD_PERFORMANCE_REPORT where AveragePosition > 4 ' +
    ' DURING LAST_MONTH')
    .rows();

var map = {
};

while (reports.hasNext()) {
  var row = reports.next();
  var adGroupId = row['AdGroupId'];
  var id = row['Id'];

  if (map[adGroupId] == null) {
    map[adGroupId] = [];
  }
  map[adGroupId].push(id);
}

for (var key in map) {
  var keywords = AdWordsApp.keywords()
      .withCondition('AdGroupId="' + key + '"')
      .withIds(map[key])
      .get();

  while (keywords.hasNext()) {
    var keyword = keywords.next();
    keyword.bidding().setCpc(keyword.bidding().getCpc() + 0.02);
  }
}

While approach 2 gives you pretty good performance, approach 1 is preferred in this case because

  • AdWords scripts has a limit on the number of objects that can be retrieved or updated in a single run, and the select and update operations in the second approach counts towards that limit.
  • Bulk uploads have higher limits both in terms of number of entities it can update, and the overall execution time.

Group your bulk uploads by campaigns

When you create your bulk uploads, try to group your operations by the parent campaign. This increases efficiency and decreases the chance of conflicting changes / concurrency errors.

Consider two bulk upload tasks running in parallel. One pauses ads in an ad group; the other adjusts keyword bids. Even though the operations are unrelated, the operations may apply to entities under the same ad group (or two different ad groups under the same campaign). When this happens, the system will lock the parent entity (the shared ad group or campaign), thus causing the bulk upload tasks to block on each other.

AdWords scripts can optimize execution within a single bulk upload task, so the simplest thing to do is to run only one bulk upload task per account at a time. If you decide to run more than one bulk upload per account, then ensure that the bulk uploads operate on mutually exclusive list of campaigns (and their child entities) for optimal performance.

Reporting

Use reports for fetching stats

When you want to retrieve large amounts of entities and their stats, it is often better to use reports rather than standard AdWordsApp methods. The use of reports is preferred due to the following reasons:

  • Reports give you better performance for large queries.
  • Reports will not hit normal fetching quotas.

Compare the following code snippets that fetch the Clicks, Impressions, Cost and Text of all keywords that received more than 50 clicks last month:

Coding approach Code snippet
Use reports (recommended)
var rows = AdWordsApp.report(
    'SELECT KeywordText, Impressions, Clicks, Cost' +
    ' from KEYWORDS_PERFORMANCE_REPORT WHERE Clicks > 50 DURING' +
    ' LAST_MONTH')
    .rows();
while (rows.hasNext()) {
  var row = rows.next();
  Logger.log('Keyword: %s Impressions: %s ' +
      'Clicks: %s Ctr: %s',
      row['KeywordText'],
      row['Impressions'],
      row['Clicks'],
      row['Cost']);
}
Use AdWordsApp iterators (not recommended)
var keywords = AdWordsApp.keywords()
    .withCondition('Clicks > 50')
    .forDateRange('LAST_MONTH')
    .get();
while (keywords.hasNext()) {
  var keyword = keywords.next();
  var stats = keyword.getStatsFor('LAST_MONTH');
  Logger.log('Keyword: %s Impressions: %s ' +
      'Clicks: %s Ctr: %s',
      keyword.getText(),
      stats.getImpressions(),
      stats.getClicks(),
      stats.getCost());
}

The second approach is not preferred because it iterates over the keywords and retrieves the stats one entity at a time. Reports perform faster in this case since it fetches all the data in a single call and streams it as required. In addition, the keywords retrieved in the second approach is counted towards your script's quota for number of entities retrieved using a get() call.

Don't run reports in a tight loop

While reports are efficient in returning data, creating a report itself is a costly operation, so one should try to minimize the number of reports that a script creates during its runtime, ideally under 100.

Consider the following code snippets that generate a report with the following columns:

AdGroupId, AdGroupName, EstimatedTotalConversions
Coding approach Code snippet
Use reports only (recommended)
var list = [];

var report = AdWordsApp.report(“Select AdGroupId,
   AdGroupName, EstimatedTotalConversions from
   ADGROUP_PERFORMANCE_REPORT DURING
   LAST_MONTH”).rows();

while (report.hasNext()) {
  var reportRow = report.next();
  list.push({
    “Id”: reportRow[“AdGroupId”],
    “Name”: reportRow[“AdGroupName”],
    “EstimatedTotalConversions”:
         reportRow[“EstimatedTotalConversions”]
  });
}
Run reports and Iterators separately (less optimal)
var list = {};

var adGroupIterator = AdWordsApp.adGroups().get();

while (adGroupIterator.hasNext()) {
  var adGroup = adGroupIterator.next();
  list[adGroup.getId()] = {
    “Id”: adGroup.getId(),
    “Name”: adGroup.getName()
  }
}

var report = AdWordsApp.report(“Select AdGroupId, " +
    "EstimatedTotalConversions " +
    "from ADGROUP_PERFORMANCE_REPORT DURING LAST_MONTH”)
    .rows();

while report.hasNext()) {
  var reportRow = report.next();
  var temp = list[reportRow["AdGroupId"]];
  temp["EstimatedTotalConversions"] =
     reportRow[“EstimatedTotalConversions”]
}
Run reports and iterators in a tight loop (not recommended)
var list = {};

var adGroupIterator = AdWordsApp.adGroups().get();

while (adGroupIterator.hasNext()) {
  var adGroup = adGroupIterator.next();
  var report = AdWordsApp.report(“Select AdGroupId,
      EstimatedTotalConversions from
      ADGROUP_PERFORMANCE_REPORT where AdGroupId =
      “ + adGroup.getId() + “ DURING LAST_MONTH”)
      .rows().next();
  list[adGroup.getId()] = {
    “Id”: adGroup.getId(),
    “Name”: adGroup.getName(),
    report[“EstimatedTotalConversions”]
  }
}

The first approach is most recommended because you get all the necessary values from the AD_PERFORMANCE_REPORT itself. The second approach is less optimal since the code iterates over the list of ad groups and then runs an additional report to get the missing stat value. However, some developers prefer this approach due to ease of iterating over a collection. The third approach is the worst in terms of performance since it runs one report for each ad group.

MCC scripts

Prefer executeInParallel over serial execution

When writing MCC Scripts, prefer using executeInParallel method over serial execution. executeInParallel() gives your script more processing time (up to one hour) and up to 30 minutes per account processed (instead of 30 minutes combined for serial execution). See our guide for more details.

Spreadsheets

Use batch operations when updating spreadsheets

When updating spreadsheets, try to use the bulk operation methods (e.g. getRange) over methods that update one cell at a time.

Consider the following code snippet that generates a fractal pattern on a spreadsheet.

Coding approach Code snippet
Update a range of cells in a single call (recommended)
var cell = sheet.getRange('a1');
var colors = new Array(100);
for (var y = 0; y < 100; y++) {
  xcoord = xmin;
  colors[y] = new Array(100);
  for (var x = 0; x < 100; x++) {
    colors[y][x] = getColor_(xcoord, ycoord);
    xcoord += xincrement;
  }
  ycoord -= yincrement;
}
sheet.getRange(1, 1, 100, 100).setBackgroundColors(colors);
Update one cell at a time (not recommended)
var cell = sheet.getRange('a1');
for (var y = 0; y < 100; y++) {
  xcoord = xmin;
  for (var x = 0; x < 100; x++) {
    var c = getColor_(xcoord, ycoord);
    cell.offset(y, x).setBackgroundColor(c);
    xcoord += xincrement;
  }
  ycoord -= yincrement;
  SpreadsheetApp.flush();
}

While Google Spreadsheets tries to optimize the second code snippet by caching values, it still gives you poor performance compared to the first snippet, due to the number of API calls being made.

Send feedback about...

AdWords Scripts