Best Practices

This page covers various best practices for developing with Google Ads 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.

Compare the following code snippets:

Coding approach Code snippet
Filter using selectors (recommended)
var keywords = AdsApp.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 = AdsApp.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 = AdsApp.ads();

Traverse the hierarchy (Not recommended)
var campaigns = AdsApp.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 = AdsApp.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 = AdsApp.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 = AdsApp.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 = AdsApp.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 = AdsApp.campaigns()
    .withIds([12345])
    .get()
    .next();
Filter by Name (less optimal)
var campaign = AdsApp.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 = AdsApp.adGroups()
    .withIds([12345])
    .withCondition('CampaignId="54678"')
    .get()
    .next();
Filter by ad group ID alone (less optimal)
var adGroup = AdsApp.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.

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 = AdsApp.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 = AdsApp.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 = AdsApp.labels()
    .withCondition('LabelName contains "Report Entities"')
    .get()
    .next();

var report = AdsApp.report('SELECT AdGroupId, Id, Clicks, ' +
    'Impressions, Cost FROM KEYWORDS_PERFORMANCE_REPORT ' +
    'WHERE LabelId = "' + label.getId() + '"');
Build a long query using IN clause (not recommended)
var report = AdsApp.report('SELECT AdGroupId, Id, Clicks, ' +
    'Impressions, Cost FROM KEYWORDS_PERFORMANCE_REPORT WHERE ' +
    'AdGroupId IN (123, 456) and Id in (123,345, 456…)');

Account updates

Batch changes

When you make changes to an Google Ads entity, Google Ads 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 Google Ads servers. However, there are some code patterns that force Google Ads 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 = AdsApp.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 = AdsApp.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 Google Ads 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.

Use builders when possible

Google Ads 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 Google Ads 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 Google Ads 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 (for example, 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 TopImpressionPercentage > 0.4 for the last month:

Coding approach Code snippet
Use bulk upload (recommended)

var report = AdsApp.report(
  'SELECT AdGroupId, Id, CpcBid FROM KEYWORDS_PERFORMANCE_REPORT ' +
  'WHERE TopImpressionPercentage > 0.4 DURING LAST_MONTH');

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

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

upload.apply();
Select and update keywords by ID (less optimal)
var reportRows = AdsApp.report('SELECT AdGroupId, Id, CpcBid FROM ' +
    'KEYWORDS_PERFORMANCE_REPORT WHERE TopImpressionPercentage > 0.4 ' +
    ' DURING LAST_MONTH')
    .rows();

var map = {
};

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

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

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

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

While the second approach gives you pretty good performance, the first approach is preferred in this case since

  • Google Ads 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.

Google Ads 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 AdsApp 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)
  report = AdsApp.search(
      'SELECT ' +
      '   ad_group_criterion.keyword.text, ' +
      '   metrics.clicks, ' +
      '   metrics.cost_micros, ' +
      '   metrics.impressions ' +
      'FROM ' +
      '   keyword_view ' +
      'WHERE ' +
      '   segments.date DURING LAST_MONTH ' +
      '   AND metrics.clicks > 50');
  while (report.hasNext()) {
    var row = report.next();
    Logger.log('Keyword: %s Impressions: %s ' +
        'Clicks: %s Cost: %s',
        row.adGroupCriterion.keyword.text,
        row.metrics.impressions,
        row.metrics.clicks,
        row.metrics.cost);
  }
Use AdsApp iterators (not recommended)
var keywords = AdsApp.keywords()
    .withCondition('metrics.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 Cost: %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.

Use search instead of report

The report method was built for the old infrastructure, and will output results in a flat format even if you are using GAQL. This means that it has to transform the results of the query to match the old style, which isn't supported for all fields and adds overhead to each call.

We suggest you use search instead to take advantage of all the features of the new Google Ads API reporting.

Prefer GAQL to AWQL

Although AWQL is still supported in report queries and withCondition calls, it is run through a translation layer which doesn't have full compatibility with true AWQL. To have complete control over your queries, ensure that you are using GAQL.

If you have existing AWQL queries you'd like to translate, we have a Query Migration Tool to help.

Don't select more rows than you need

The speed of execution of reports (and selectors) is based on the total number of rows that would be returned by the report, regardless of whether you iterate through them. This means that you should always use specific filters to minimize the result set as much as possible to match your use case.

For example, let's say you want to find ad groups with bids outside some specific range. It would be faster to make two separate queries, one for bids below the bottom threshold and another for bids above the top threshold, than it would be to fetch all ad groups and ignore the ones that you aren't interested in.

Coding approach Code snippet
Use two queries (recommended)
var adGroups = []
var report = AdsApp.search(
    'SELECT ad_group.name, ad_group.cpc_bid_micros' +
    ' FROM ad_group WHERE ad_group.cpc_bid_micros < 1000000');

while (report.hasNext()) {
  var row = report.next();
  adGroups.push(row.adGroup);
}
var report = AdsApp.search(
    'SELECT ad_group.name, ad_group.cpc_bid_micros' +
    ' FROM ad_group WHERE ad_group.cpc_bid_micros > 2000000');

while (report.hasNext()) {
  var row = report.next();
  adGroups.push(row.adGroup);
}
Filter down from a generic query (not recommended)
var adGroups = []
var report = AdsApp.search(
    'SELECT ad_group.name, ad_group.cpc_bid_micros' +
    ' FROM ad_group');

while (report.hasNext()) {
  var row = report.next();
  var cpcBidMicros = row.adGroup.cpcBidMicros;
  if (cpcBidMicros < 1000000 || cpcBidMicros > 2000000) {
    adGroups.push(row.adGroup);
  }
}

Ads Manager (MCC) scripts

Prefer executeInParallel over serial execution

When writing scripts for manager accounts, use executeInParallel() instead of serial execution when possible. 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 limits page for more details.

Spreadsheets

Use batch operations when updating spreadsheets

When updating spreadsheets, try to use the bulk operation methods (for example, 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 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.