Google Ads Scripts
You are helping the user write or modify a Google Ads Script. These run inside
Google's sandboxed JavaScript runtime and are accessed via the Ads UI under
Tools and settings -> Bulk actions -> Scripts.
Read this whole file before writing code. Most "this didn't work" loops come
from skipping a section that warned about it.
What Google Ads Scripts is (and is not)
- A JavaScript (ES2017+ subset) runtime that exposes
AdsApp (account-level)
and AdsManagerApp (MCC-level) globals.
- NOT the Google Ads API. It is a thin, opinionated wrapper around it. Some
resources have first-class wrappers (
AdsApp.campaigns(),
AdsApp.keywords()). Many do not. When the wrapper is missing, you fall
back to AdsApp.search() (read) and AdsApp.mutate() (write) which speak
the raw API.
- Runtime limit: 30 minutes per single-account run, 60 minutes per MCC run.
- Preview mode does not persist changes. Some operations behave weirdly in
preview (see
reference/gotchas.md).
- API version used by
AdsApp.search() / AdsApp.mutate() follows the
current Google Ads API version. Field names in GAQL are snake_case;
field names in mutate() payloads are camelCase.
Before you write code: decide the shape
Ask yourself:
- Single account or MCC? If MCC, use
AdsManagerApp.accounts()...executeInParallel().
See reference/mcc-scripts.md.
- Read-only or mutating? Read-only scripts can rely entirely on
AdsApp.search() (GAQL). Mutating scripts use either the wrappers
(adGroup.pause(), keyword.bidding().setCpc(1.5)) or AdsApp.mutate().
- Is the resource wrapped by the SDK? Campaigns, ad groups, keywords,
ads, labels, audiences-on-ad-group are wrapped. Demographic criteria
(age, gender, parental status, income) are mostly NOT wrapped outside
of video campaigns. Asset groups, asset group signals, listing groups,
experiment arms, recommendations: not wrapped — use mutate.
- Idempotency. If the script will be scheduled, it MUST be safe to run
repeatedly. Use labels (
reference/idempotency.md) or pre-check via
AdsApp.search().
The selector pattern
Every read in the wrapped API uses the selector pattern: build, filter,
order, limit, then call .get() for an iterator.
var iterator = AdsApp.campaigns()
.withCondition("Status = ENABLED")
.withCondition("CampaignName CONTAINS_IGNORE_CASE 'channable'")
.orderBy("Cost DESC")
.forDateRange("LAST_30_DAYS")
.withLimit(50)
.get();
while (iterator.hasNext()) {
var campaign = iterator.next();
// ...
}
Cheatsheet of conditions and field names lives in reference/selectors.md.
Note: selector field names (e.g. CampaignName, Status) are NOT the same
as GAQL field names (campaign.name, campaign.status).
The mutate escape hatch
When the SDK has no wrapper, use AdsApp.mutate() (single) or
AdsApp.mutateAll() (batch). The payload is a JSON MutateOperation with
camelCase field names matching the Google Ads API REST schema.
var result = AdsApp.mutate({
adGroupCriterionOperation: {
create: {
resourceName: 'customers/' + customerId + '/adGroupCriteria/' + adGroupId + '~503006',
negative: true,
ageRange: { type: 'AGE_RANGE_65_UP' },
},
},
});
if (!result.isSuccessful()) {
Logger.log(result.getErrorMessages().join('; '));
}
CRITICAL gotcha: when creating an adGroupCriterion for a demographic, pass
the full resourceName (which encodes the criterion ID) and DO NOT also pass
adGroup. The API computes one from the other and rejects mismatches with:
The field's contents don't match another field that represents the same
data. At adGroupCriterionOperation.create.resourceName
The ~ in resource names is a literal separator: customers/{cid}/adGroupCriteria/{adGroupId}~{criterionId}.
Full reference: reference/mutate-escape-hatch.md.
Reading with GAQL
AdsApp.search() runs raw GAQL against the current account.
var rows = AdsApp.search(
"SELECT ad_group.id, ad_group.name, ad_group_criterion.age_range.type " +
"FROM ad_group_criterion " +
"WHERE ad_group_criterion.type = 'AGE_RANGE' " +
"AND ad_group_criterion.negative = TRUE"
);
while (rows.hasNext()) {
var row = rows.next();
Logger.log(row.adGroup.name + ': ' + row.adGroupCriterion.ageRange.type);
}
Field names in GAQL are snake_case in the query string but the returned
objects use camelCase. Resource is plural for the query (ad_group_criterion)
but field paths nest naturally.
Full reference: reference/gaql-cheatsheet.md.
Well-known criterion IDs
Some criterion IDs are global constants (same across every account). You
NEED these when building resource names for mutate():
| Type |
ID |
| AGE_RANGE_18_24 |
503001 |
| AGE_RANGE_25_34 |
503002 |
| AGE_RANGE_35_44 |
503003 |
| AGE_RANGE_45_54 |
503004 |
| AGE_RANGE_55_64 |
503005 |
| AGE_RANGE_65_UP |
503006 |
| AGE_RANGE_UNDETERMINED |
503999 |
| GENDER_MALE |
10 |
| GENDER_FEMALE |
11 |
| GENDER_UNDETERMINED |
20 |
| PARENTAL_STATUS_PARENT |
300 |
| PARENTAL_STATUS_NOT_A_PARENT |
301 |
| PARENTAL_STATUS_UNDETERMINED |
302 |
Income brackets, device types, and more in reference/criterion-ids.md.
Idempotency: label everything you touch
For any script that will run on a schedule, mark processed entities so the
next run skips them. Labels are the easiest mechanism for campaigns, ad
groups, ads, and keywords. Pattern:
ensureLabel(name) — create if missing.
- Selector excludes labeled entities, or check
entity.labels() per item.
- After mutating an entity,
entity.applyLabel(name).
In preview mode AdsApp.createLabel() does not persist, so a subsequent
applyLabel() in the same preview run throws. Wrap applyLabel() in
try/catch and log a soft warning. On a live run the label is created and
applied normally.
Full pattern: reference/idempotency.md.
Templates
Start from templates/ and modify. They include the boilerplate every
script needs (customer ID handling, structured stats logging, error
handling, summary at the end):
templates/single-account.js — most scripts start here
templates/mcc-parallel.js — run across many accounts under an MCC
templates/mutate-with-search.js — the read-then-mutate pattern with
idempotency via search
Worked examples
examples/ contains scripts the user has actually shipped:
examples/exclude-age-demographic.js — exclude an age range from every
ad group in matching campaigns, with label-based idempotency and the
mutate escape hatch.
Workflow rules when writing for the user
- Pick the right tool. Wrapped operation? Use the wrapper. Not wrapped?
mutate() with a hand-built payload. Reading? Prefer AdsApp.search()
over report() (the latter is deprecated).
- Run a probe before bulk writes. When unsure of the payload shape,
write a one-off "diagnostic" version that picks one entity, dumps its
current state via
search(), and tries 2–3 candidate payloads with
try/catch. Then write the production version from what worked. This is
way faster than guessing.
- Handle preview mode. Don't crash the script when an apply-only
operation (label creation, mutate side effects) fails in preview.
- Log structured stats. Maintain a
stats object with named counters,
print a single summary block at the end. Don't make the user grep
100-line logs.
- Use
withCondition("Status = ENABLED") unless the user explicitly
wants paused/removed entities. By default they almost certainly only
care about live stuff.
- No em dashes in any output. This is a user preference.
- Never claim success without verifying. If you can run the script
yourself, do so. If you can't, say "ready to run, do a preview first".
Reference files (read on demand)
reference/selectors.md — selector conditions, field names, operators,
date ranges, ordering, limits.
reference/gaql-cheatsheet.md — GAQL query language: SELECT, FROM,
WHERE, segments, common resources, field naming.
reference/mutate-escape-hatch.md — every common mutate operation with
verified payload shape. Includes the demographic-exclusion case.
reference/criterion-ids.md — well-known criterion IDs you must hard-code.
reference/mcc-scripts.md — AdsManagerApp, executeInParallel, accounts
selector, return value protocol.
reference/idempotency.md — label patterns, search-before-mutate, GAQL
pre-checks.
reference/gotchas.md — preview mode, runtime limits, immutable fields,
bid modifier ranges, criterion auto-creation, timezone quirks.
reference/error-handling.md — interpreting common API errors:
PERMISSION_DENIED, DUPLICATE_AD_GROUP_CRITERION, RESOURCE_NAME_MALFORMED,
CRITERION_TYPE_TARGETING_NOT_SUPPORTED, the "field contents don't match"
error and its causes.
External canonical references the user can pull up:
1---2name: google-ads-scripts3description: Write, debug, and ship Google Ads Scripts (AdsApp / AdsManagerApp) correctly the first time. Covers selectors, GAQL search, the AdsApp.mutate() escape hatch for resources the SDK does not wrap, demographic exclusions, MCC parallel execution, preview-mode gotchas, idempotency patterns, and common error messages. Use when the user says "Google Ads Script", "AdsApp", "AdsManagerApp", "ads script", "AdWordsApp", "automate Google Ads", "mutate ad group criterion", or pastes JavaScript that imports AdsApp. NOT for auditing Google Ads accounts — for that see ads-google.4---56# Google Ads Scripts78You are helping the user write or modify a Google Ads Script. These run inside9Google's sandboxed JavaScript runtime and are accessed via the Ads UI under10Tools and settings -> Bulk actions -> Scripts.1112Read this whole file before writing code. Most "this didn't work" loops come13from skipping a section that warned about it.1415## What Google Ads Scripts is (and is not)1617- A JavaScript (ES2017+ subset) runtime that exposes `AdsApp` (account-level)18 and `AdsManagerApp` (MCC-level) globals.19- NOT the Google Ads API. It is a thin, opinionated wrapper around it. Some20 resources have first-class wrappers (`AdsApp.campaigns()`,21 `AdsApp.keywords()`). Many do not. When the wrapper is missing, you fall22 back to `AdsApp.search()` (read) and `AdsApp.mutate()` (write) which speak23 the raw API.24- Runtime limit: 30 minutes per single-account run, 60 minutes per MCC run.25- Preview mode does not persist changes. Some operations behave weirdly in26 preview (see `reference/gotchas.md`).27- API version used by `AdsApp.search()` / `AdsApp.mutate()` follows the28 current Google Ads API version. Field names in GAQL are snake_case;29 field names in `mutate()` payloads are camelCase.3031## Before you write code: decide the shape3233Ask yourself:34351. **Single account or MCC?** If MCC, use `AdsManagerApp.accounts()...executeInParallel()`.36 See `reference/mcc-scripts.md`.372. **Read-only or mutating?** Read-only scripts can rely entirely on38 `AdsApp.search()` (GAQL). Mutating scripts use either the wrappers39 (`adGroup.pause()`, `keyword.bidding().setCpc(1.5)`) or `AdsApp.mutate()`.403. **Is the resource wrapped by the SDK?** Campaigns, ad groups, keywords,41 ads, labels, audiences-on-ad-group are wrapped. Demographic criteria42 (age, gender, parental status, income) are mostly NOT wrapped outside43 of video campaigns. Asset groups, asset group signals, listing groups,44 experiment arms, recommendations: not wrapped — use mutate.454. **Idempotency.** If the script will be scheduled, it MUST be safe to run46 repeatedly. Use labels (`reference/idempotency.md`) or pre-check via47 `AdsApp.search()`.4849## The selector pattern5051Every read in the wrapped API uses the selector pattern: build, filter,52order, limit, then call `.get()` for an iterator.5354```javascript55var iterator = AdsApp.campaigns()56 .withCondition("Status = ENABLED")57 .withCondition("CampaignName CONTAINS_IGNORE_CASE 'channable'")58 .orderBy("Cost DESC")59 .forDateRange("LAST_30_DAYS")60 .withLimit(50)61 .get();6263while (iterator.hasNext()) {64 var campaign = iterator.next();65 // ...66}67```6869Cheatsheet of conditions and field names lives in `reference/selectors.md`.70Note: selector field names (e.g. `CampaignName`, `Status`) are NOT the same71as GAQL field names (`campaign.name`, `campaign.status`).7273## The mutate escape hatch7475When the SDK has no wrapper, use `AdsApp.mutate()` (single) or76`AdsApp.mutateAll()` (batch). The payload is a JSON `MutateOperation` with77camelCase field names matching the Google Ads API REST schema.7879```javascript80var result = AdsApp.mutate({81 adGroupCriterionOperation: {82 create: {83 resourceName: 'customers/' + customerId + '/adGroupCriteria/' + adGroupId + '~503006',84 negative: true,85 ageRange: { type: 'AGE_RANGE_65_UP' },86 },87 },88});8990if (!result.isSuccessful()) {91 Logger.log(result.getErrorMessages().join('; '));92}93```9495CRITICAL gotcha: when creating an `adGroupCriterion` for a demographic, pass96the full `resourceName` (which encodes the criterion ID) and DO NOT also pass97`adGroup`. The API computes one from the other and rejects mismatches with:9899> The field's contents don't match another field that represents the same100> data. At adGroupCriterionOperation.create.resourceName101102The `~` in resource names is a literal separator: `customers/{cid}/adGroupCriteria/{adGroupId}~{criterionId}`.103104Full reference: `reference/mutate-escape-hatch.md`.105106## Reading with GAQL107108`AdsApp.search()` runs raw GAQL against the current account.109110```javascript111var rows = AdsApp.search(112 "SELECT ad_group.id, ad_group.name, ad_group_criterion.age_range.type " +113 "FROM ad_group_criterion " +114 "WHERE ad_group_criterion.type = 'AGE_RANGE' " +115 "AND ad_group_criterion.negative = TRUE"116);117while (rows.hasNext()) {118 var row = rows.next();119 Logger.log(row.adGroup.name + ': ' + row.adGroupCriterion.ageRange.type);120}121```122123Field names in GAQL are snake_case in the query string but the returned124objects use camelCase. Resource is plural for the query (`ad_group_criterion`)125but field paths nest naturally.126127Full reference: `reference/gaql-cheatsheet.md`.128129## Well-known criterion IDs130131Some criterion IDs are global constants (same across every account). You132NEED these when building resource names for `mutate()`:133134| Type | ID |135|------|----|136| AGE_RANGE_18_24 | 503001 |137| AGE_RANGE_25_34 | 503002 |138| AGE_RANGE_35_44 | 503003 |139| AGE_RANGE_45_54 | 503004 |140| AGE_RANGE_55_64 | 503005 |141| AGE_RANGE_65_UP | 503006 |142| AGE_RANGE_UNDETERMINED | 503999 |143| GENDER_MALE | 10 |144| GENDER_FEMALE | 11 |145| GENDER_UNDETERMINED | 20 |146| PARENTAL_STATUS_PARENT | 300 |147| PARENTAL_STATUS_NOT_A_PARENT | 301 |148| PARENTAL_STATUS_UNDETERMINED | 302 |149150Income brackets, device types, and more in `reference/criterion-ids.md`.151152## Idempotency: label everything you touch153154For any script that will run on a schedule, mark processed entities so the155next run skips them. Labels are the easiest mechanism for campaigns, ad156groups, ads, and keywords. Pattern:1571581. `ensureLabel(name)` — create if missing.1592. Selector excludes labeled entities, or check `entity.labels()` per item.1603. After mutating an entity, `entity.applyLabel(name)`.161162In preview mode `AdsApp.createLabel()` does not persist, so a subsequent163`applyLabel()` in the same preview run throws. Wrap `applyLabel()` in164try/catch and log a soft warning. On a live run the label is created and165applied normally.166167Full pattern: `reference/idempotency.md`.168169## Templates170171Start from `templates/` and modify. They include the boilerplate every172script needs (customer ID handling, structured stats logging, error173handling, summary at the end):174175- `templates/single-account.js` — most scripts start here176- `templates/mcc-parallel.js` — run across many accounts under an MCC177- `templates/mutate-with-search.js` — the read-then-mutate pattern with178 idempotency via search179180## Worked examples181182`examples/` contains scripts the user has actually shipped:183184- `examples/exclude-age-demographic.js` — exclude an age range from every185 ad group in matching campaigns, with label-based idempotency and the186 mutate escape hatch.187188## Workflow rules when writing for the user1891901. **Pick the right tool.** Wrapped operation? Use the wrapper. Not wrapped?191 `mutate()` with a hand-built payload. Reading? Prefer `AdsApp.search()`192 over `report()` (the latter is deprecated).1932. **Run a probe before bulk writes.** When unsure of the payload shape,194 write a one-off "diagnostic" version that picks one entity, dumps its195 current state via `search()`, and tries 2–3 candidate payloads with196 try/catch. Then write the production version from what worked. This is197 way faster than guessing.1983. **Handle preview mode.** Don't crash the script when an apply-only199 operation (label creation, mutate side effects) fails in preview.2004. **Log structured stats.** Maintain a `stats` object with named counters,201 print a single summary block at the end. Don't make the user grep202 100-line logs.2035. **Use `withCondition("Status = ENABLED")`** unless the user explicitly204 wants paused/removed entities. By default they almost certainly only205 care about live stuff.2066. **No em dashes in any output.** This is a user preference.2077. **Never claim success without verifying.** If you can run the script208 yourself, do so. If you can't, say "ready to run, do a preview first".209210## Reference files (read on demand)211212- `reference/selectors.md` — selector conditions, field names, operators,213 date ranges, ordering, limits.214- `reference/gaql-cheatsheet.md` — GAQL query language: SELECT, FROM,215 WHERE, segments, common resources, field naming.216- `reference/mutate-escape-hatch.md` — every common mutate operation with217 verified payload shape. Includes the demographic-exclusion case.218- `reference/criterion-ids.md` — well-known criterion IDs you must hard-code.219- `reference/mcc-scripts.md` — AdsManagerApp, executeInParallel, accounts220 selector, return value protocol.221- `reference/idempotency.md` — label patterns, search-before-mutate, GAQL222 pre-checks.223- `reference/gotchas.md` — preview mode, runtime limits, immutable fields,224 bid modifier ranges, criterion auto-creation, timezone quirks.225- `reference/error-handling.md` — interpreting common API errors:226 PERMISSION_DENIED, DUPLICATE_AD_GROUP_CRITERION, RESOURCE_NAME_MALFORMED,227 CRITERION_TYPE_TARGETING_NOT_SUPPORTED, the "field contents don't match"228 error and its causes.229230External canonical references the user can pull up:231232- Google Ads Scripts overview: https://developers.google.com/google-ads/scripts233- AdsApp reference: https://developers.google.com/google-ads/scripts/docs/reference/adsapp234- Mutate concept: https://developers.google.com/google-ads/scripts/docs/concepts/mutate235- Google Ads API field reference (for GAQL + mutate payloads):236 https://developers.google.com/google-ads/api/fields/v22/overview237- "Try this" interactive query builder:238 https://developers.google.com/google-ads/api/docs/query/overview