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
won't 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) when 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, when 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

To access 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, when 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
GAQL 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 a 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 they
give 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 example, 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
make sure 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 won't 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 still technically works for report queries and `withCondition`
calls, it's not recommended. To have complete control over your queries, make
sure that you are using [GAQL](https://developers.google.com/google-ads/api/docs/query/overview) instead.

### 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, 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](https://developers.google.com/google-ads/scripts/docs/limits#ads_manager_scripts) 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.