Google Ads Scripts Reference
Complete reference guide to Google Ads Scripts: JavaScript automation within Google Ads, AdsApp object model, selectors and iterators, GAQL queries, common script patterns with working code, Google Sheets integration, MCC scripts, and best practices.
Overview
Google Ads Scripts let you programmatically control your Google Ads account using JavaScript. Scripts run directly inside the Google Ads web interface — no external server, no API keys, no OAuth setup. You write JavaScript, Google executes it on a schedule, and your account changes happen automatically.
Key capabilities:
- Read and modify campaigns, ad groups, ads, keywords, and extensions
- Query performance data using GAQL (Google Ads Query Language)
- Integrate with Google Sheets for dashboards and logging
- Send email alerts via MailApp
- Make HTTP requests to external APIs via UrlFetchApp
- Run on a schedule (hourly, daily, weekly, monthly)
Single-account vs MCC scripts:
| Feature | Single-Account Script | MCC Script |
|---|---|---|
| Scope | One Google Ads account | All accounts under an MCC |
| Entry point | main() | main() with AdsManagerApp |
| Execution limit | 30 minutes | 60 minutes |
| Parallel execution | No | Yes, via executeInParallel() |
| Access object | AdsApp | AdsManagerApp + AdsApp per account |
Language support: Google Ads Scripts use a subset of JavaScript (ES5 with some ES6 features). let, const, arrow functions, and template literals work. async/await, import/export, and most ES6+ features do not.
AdsApp Object Model
The AdsApp object is the root of all single-account operations. It provides access to every entity type in the account.
Entity Hierarchy
AdsApp
+-- campaigns()
| +-- adGroups()
| | +-- ads()
| | +-- keywords()
| | +-- audiences()
| +-- extensions()
+-- adGroups() (account-level shortcut)
+-- ads() (account-level shortcut)
+-- keywords() (account-level shortcut)
+-- negativeKeywords()
+-- shoppingCampaigns()
+-- videoCampaigns()
+-- labels()
+-- budgets()
+-- biddingStrategies()
Common Entity Methods
Every entity supports a core set of methods:
// Status
entity.isEnabled()
entity.isPaused()
entity.isRemoved()
entity.enable()
entity.pause()
entity.remove()
// Naming
entity.getName()
entity.setName('New Name')
// Stats (requires date range)
entity.getStatsFor('LAST_30_DAYS')
entity.getStatsFor('20250101', '20250131')
// Stats object methods
var stats = entity.getStatsFor('LAST_30_DAYS');
stats.getImpressions()
stats.getClicks()
stats.getCtr()
stats.getAverageCpc()
stats.getCost()
stats.getConversions()
stats.getConversionRate()
stats.getAveragePosition() // deprecated
Selectors and Iterators
Every entity collection in Google Ads Scripts uses the selector-iterator pattern. This is the fundamental data access pattern you will use in every script.
Selector Methods
// Basic selector chain
var keywords = AdsApp.keywords()
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.withCondition('Ctr < 0.01')
.forDateRange('LAST_30_DAYS')
.orderBy('Impressions DESC')
.withLimit(100)
.get();
.withCondition(condition) — Filter entities. Supports operators: =, !=, >, <, >=, <=, CONTAINS, DOES_NOT_CONTAIN, STARTS_WITH, CONTAINS_IGNORE_CASE, DOES_NOT_CONTAIN_IGNORE_CASE, REGEXP_MATCH, IN [].
// String matching
.withCondition('Name CONTAINS "brand"')
.withCondition('Name REGEXP_MATCH "^(buy|shop|order).*"')
// Numeric conditions
.withCondition('QualityScore > 5')
.withCondition('MaxCpc > 0.50')
// Status conditions
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus != REMOVED')
// Label conditions
.withCondition('LabelNames CONTAINS_ANY ["Priority", "Monitor"]')
// IN operator
.withCondition('CampaignName IN ["Search - Brand", "Search - Generic"]')
.forDateRange(dateRange) — Required for metrics-based conditions. Predefined ranges: TODAY, YESTERDAY, LAST_7_DAYS, THIS_WEEK_SUN_TODAY, THIS_WEEK_MON_TODAY, LAST_WEEK, LAST_14_DAYS, LAST_30_DAYS, LAST_BUSINESS_WEEK, LAST_WEEK_SUN_SAT, THIS_MONTH, LAST_MONTH, ALL_TIME.
// Custom date range
.forDateRange('20250101', '20250131')
.orderBy(orderSpec) — Sort results. Supports ASC and DESC.
.orderBy('Impressions DESC')
.orderBy('Ctr ASC')
.withLimit(limit) — Cap the number of results.
Iterator Pattern
var iterator = AdsApp.keywords()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS')
.withCondition('Impressions > 100')
.get();
while (iterator.hasNext()) {
var keyword = iterator.next();
var stats = keyword.getStatsFor('LAST_7_DAYS');
Logger.log(keyword.getText() + ': ' + stats.getClicks() + ' clicks');
}
GAQL in Scripts
Google Ads Query Language (GAQL) provides SQL-like access to the full Google Ads API from within scripts. Use it when you need fields not available through the standard selector interface.
AdsApp.search(query)
Returns an iterator of result rows. Each row is a JavaScript object mirroring the GAQL resource structure.
function main() {
var query = "SELECT " +
"campaign.name, " +
"campaign.status, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM campaign " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date DURING LAST_30_DAYS " +
"ORDER BY metrics.cost_micros DESC";
var results = AdsApp.search(query);
while (results.hasNext()) {
var row = results.next();
Logger.log(row.campaign.name +
' | Cost: $' + (row.metrics.costMicros / 1000000).toFixed(2) +
' | Conversions: ' + row.metrics.conversions);
}
}
AdsApp.report(query)
Returns a report object with export capabilities. Useful for bulk data extraction and Google Sheets integration.
function main() {
var report = AdsApp.report(
"SELECT " +
"ad_group_criterion.keyword.text, " +
"ad_group_criterion.quality_info.quality_score, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_criterion.status = 'ENABLED' " +
"AND segments.date DURING LAST_30_DAYS"
);
// Export to Google Sheets
var sheet = SpreadsheetApp.openByUrl('YOUR_SHEET_URL').getActiveSheet();
report.exportToSheet(sheet);
}
Search vs Report
| Feature | AdsApp.search() | AdsApp.report() |
|---|---|---|
| Return type | Row iterator | Report object |
| Field access | row.campaign.name | Row iterator or exportToSheet() |
| Sheet export | Manual | Built-in exportToSheet() |
| Best for | Processing rows individually | Bulk export, dashboards |
Common Script Patterns
1. Pause Low-Performing Keywords
Find keywords below a quality score or CTR threshold and pause them.
function main() {
var CONFIG = {
MIN_QUALITY_SCORE: 4,
MIN_CTR: 0.005, // 0.5%
MIN_IMPRESSIONS: 100, // Minimum data threshold
DATE_RANGE: 'LAST_30_DAYS',
DRY_RUN: true // Set to false to actually pause
};
var query = "SELECT " +
"ad_group_criterion.keyword.text, " +
"ad_group_criterion.keyword.match_type, " +
"ad_group_criterion.quality_info.quality_score, " +
"ad_group.name, " +
"campaign.name, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.ctr, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_criterion.status = 'ENABLED' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var pauseCount = 0;
while (results.hasNext()) {
var row = results.next();
var qs = row.adGroupCriterion.qualityInfo.qualityScore;
var ctr = row.metrics.ctr;
if (qs < CONFIG.MIN_QUALITY_SCORE || ctr < CONFIG.MIN_CTR) {
Logger.log('PAUSE: "' + row.adGroupCriterion.keyword.text + '"' +
' | QS: ' + qs +
' | CTR: ' + (ctr * 100).toFixed(2) + '%' +
' | Campaign: ' + row.campaign.name +
' | AdGroup: ' + row.adGroup.name);
if (!CONFIG.DRY_RUN) {
var keywords = AdsApp.keywords()
.withCondition('Text = "' + row.adGroupCriterion.keyword.text + '"')
.withCondition('AdGroupName = "' + row.adGroup.name + '"')
.withCondition('CampaignName = "' + row.campaign.name + '"')
.get();
if (keywords.hasNext()) {
keywords.next().pause();
}
}
pauseCount++;
}
}
Logger.log('Total keywords to pause: ' + pauseCount);
if (CONFIG.DRY_RUN) {
Logger.log('DRY RUN — no changes applied. Set DRY_RUN = false to execute.');
}
}
2. Budget Pacing Monitor
Check daily spend against a monthly target and alert if overspending or underspending.
function main() {
var CONFIG = {
MONTHLY_BUDGET: 10000, // Target monthly spend
ALERT_THRESHOLD_OVER: 0.15, // Alert if 15% over pace
ALERT_THRESHOLD_UNDER: 0.20, // Alert if 20% under pace
ALERT_EMAIL: 'team@example.com',
CAMPAIGN_LABEL: 'Budget-Monitor' // Only monitor labeled campaigns
};
var today = new Date();
var daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
var dayOfMonth = today.getDate();
var expectedSpend = (CONFIG.MONTHLY_BUDGET / daysInMonth) * dayOfMonth;
// Get month-to-date spend
var firstOfMonth = Utilities.formatDate(
new Date(today.getFullYear(), today.getMonth(), 1), 'UTC', 'yyyyMMdd');
var todayStr = Utilities.formatDate(today, 'UTC', 'yyyyMMdd');
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
.forDateRange(firstOfMonth, todayStr)
.get();
var totalSpend = 0;
var campaignDetails = [];
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor(firstOfMonth, todayStr);
var spend = stats.getCost();
totalSpend += spend;
campaignDetails.push({
name: campaign.getName(),
spend: spend,
conversions: stats.getConversions()
});
}
var paceRatio = totalSpend / expectedSpend;
var projectedSpend = (totalSpend / dayOfMonth) * daysInMonth;
var status = 'ON PACE';
if (paceRatio > (1 + CONFIG.ALERT_THRESHOLD_OVER)) {
status = 'OVERSPENDING';
} else if (paceRatio < (1 - CONFIG.ALERT_THRESHOLD_UNDER)) {
status = 'UNDERSPENDING';
}
Logger.log('Budget Pacing Report');
Logger.log('Day ' + dayOfMonth + ' of ' + daysInMonth);
Logger.log('Expected spend: $' + expectedSpend.toFixed(2));
Logger.log('Actual spend: $' + totalSpend.toFixed(2));
Logger.log('Projected month-end: $' + projectedSpend.toFixed(2));
Logger.log('Status: ' + status);
if (status !== 'ON PACE') {
var subject = '[Google Ads] Budget Alert: ' + status;
var body = 'Budget Pacing Alert\n\n' +
'Status: ' + status + '\n' +
'Day ' + dayOfMonth + ' of ' + daysInMonth + '\n' +
'Monthly budget: $' + CONFIG.MONTHLY_BUDGET.toFixed(2) + '\n' +
'Expected spend to date: $' + expectedSpend.toFixed(2) + '\n' +
'Actual spend to date: $' + totalSpend.toFixed(2) + '\n' +
'Projected month-end spend: $' + projectedSpend.toFixed(2) + '\n\n' +
'Campaign Breakdown:\n';
for (var i = 0; i < campaignDetails.length; i++) {
body += ' ' + campaignDetails[i].name +
': $' + campaignDetails[i].spend.toFixed(2) +
' (' + campaignDetails[i].conversions + ' conv)\n';
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL, subject, body);
Logger.log('Alert email sent to ' + CONFIG.ALERT_EMAIL);
}
}
3. Search Terms Negative Keyword Miner
Find search terms with high spend and low conversions, then auto-add them as negative keywords.
function main() {
var CONFIG = {
MIN_COST: 50, // Min spend before evaluating
MAX_CONVERSIONS: 0, // Max conversions to qualify as negative
MIN_CLICKS: 10, // Min clicks for statistical significance
DATE_RANGE: 'LAST_30_DAYS',
NEGATIVE_MATCH_TYPE: 'EXACT', // EXACT, PHRASE, or BROAD
DRY_RUN: true,
LOG_SHEET_URL: '' // Optional: Google Sheet URL for logging
};
var query = "SELECT " +
"search_term_view.search_term, " +
"campaign.name, " +
"campaign.resource_name, " +
"ad_group.name, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"metrics.ctr " +
"FROM search_term_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND metrics.cost_micros > " + (CONFIG.MIN_COST * 1000000) + " " +
"AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
"AND metrics.conversions <= " + CONFIG.MAX_CONVERSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var negatives = [];
while (results.hasNext()) {
var row = results.next();
var term = row.searchTermView.searchTerm;
var cost = row.metrics.costMicros / 1000000;
negatives.push({
term: term,
campaign: row.campaign.name,
adGroup: row.adGroup.name,
clicks: row.metrics.clicks,
cost: cost,
conversions: row.metrics.conversions
});
Logger.log('NEGATIVE: "' + term + '"' +
' | Cost: $' + cost.toFixed(2) +
' | Clicks: ' + row.metrics.clicks +
' | Conv: ' + row.metrics.conversions +
' | Campaign: ' + row.campaign.name);
if (!CONFIG.DRY_RUN) {
var campaigns = AdsApp.campaigns()
.withCondition('Name = "' + row.campaign.name + '"')
.get();
if (campaigns.hasNext()) {
var campaign = campaigns.next();
if (CONFIG.NEGATIVE_MATCH_TYPE === 'EXACT') {
campaign.createNegativeKeyword('[' + term + ']');
} else if (CONFIG.NEGATIVE_MATCH_TYPE === 'PHRASE') {
campaign.createNegativeKeyword('"' + term + '"');
} else {
campaign.createNegativeKeyword(term);
}
}
}
}
Logger.log('Total negative keyword candidates: ' + negatives.length);
// Log to Google Sheet if configured
if (CONFIG.LOG_SHEET_URL && negatives.length > 0) {
var sheet = SpreadsheetApp.openByUrl(CONFIG.LOG_SHEET_URL).getActiveSheet();
var timestamp = new Date().toISOString();
for (var i = 0; i < negatives.length; i++) {
sheet.appendRow([
timestamp,
negatives[i].term,
negatives[i].campaign,
negatives[i].adGroup,
negatives[i].clicks,
negatives[i].cost,
negatives[i].conversions,
CONFIG.DRY_RUN ? 'DRY RUN' : 'ADDED'
]);
}
}
}
4. Broken URL Checker
Iterate through ads and sitelinks, check for HTTP errors (404, 500), and pause or alert.
function main() {
var CONFIG = {
CHECK_ADS: true,
CHECK_SITELINKS: true,
ALERT_EMAIL: 'team@example.com',
PAUSE_BROKEN: false, // Pause ads with broken URLs
DATE_RANGE: 'LAST_7_DAYS',
MIN_IMPRESSIONS: 10 // Only check active ads
};
var brokenUrls = [];
// Check ad final URLs
if (CONFIG.CHECK_ADS) {
var ads = AdsApp.ads()
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.forDateRange(CONFIG.DATE_RANGE)
.withCondition('Impressions > ' + CONFIG.MIN_IMPRESSIONS)
.get();
while (ads.hasNext()) {
var ad = ads.next();
var urls = ad.urls().getFinalUrl();
if (urls) {
var status = checkUrl(urls);
if (status !== 200) {
brokenUrls.push({
type: 'Ad',
campaign: ad.getCampaign().getName(),
adGroup: ad.getAdGroup().getName(),
url: urls,
httpStatus: status,
entity: ad
});
Logger.log('BROKEN [' + status + ']: ' + urls +
' | Campaign: ' + ad.getCampaign().getName());
if (CONFIG.PAUSE_BROKEN) {
ad.pause();
Logger.log(' -> Ad paused');
}
}
}
}
}
// Check sitelink URLs
if (CONFIG.CHECK_SITELINKS) {
var query = "SELECT " +
"asset.final_urls, " +
"asset.name, " +
"asset.type, " +
"campaign.name " +
"FROM asset " +
"WHERE asset.type = 'SITELINK' ";
var results = AdsApp.search(query);
while (results.hasNext()) {
var row = results.next();
var assetUrls = row.asset.finalUrls;
if (assetUrls && assetUrls.length > 0) {
for (var i = 0; i < assetUrls.length; i++) {
var status = checkUrl(assetUrls[i]);
if (status !== 200) {
brokenUrls.push({
type: 'Sitelink',
campaign: row.campaign ? row.campaign.name : 'Account-level',
url: assetUrls[i],
httpStatus: status
});
Logger.log('BROKEN SITELINK [' + status + ']: ' + assetUrls[i]);
}
}
}
}
}
// Send alert
if (brokenUrls.length > 0 && CONFIG.ALERT_EMAIL) {
var subject = '[Google Ads] ' + brokenUrls.length + ' Broken URL(s) Found';
var body = 'Broken URL Report\n\n';
for (var j = 0; j < brokenUrls.length; j++) {
body += brokenUrls[j].type + ' | ' +
brokenUrls[j].campaign + ' | ' +
'HTTP ' + brokenUrls[j].httpStatus + ' | ' +
brokenUrls[j].url + '\n';
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL, subject, body);
}
Logger.log('Check complete. Broken URLs found: ' + brokenUrls.length);
}
function checkUrl(url) {
try {
var response = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
followRedirects: true,
validateHttpsCertificates: false
});
return response.getResponseCode();
} catch (e) {
Logger.log('Error checking ' + url + ': ' + e.message);
return 0; // Connection error
}
}
5. Bid Adjustments by Time of Day (Dayparting)
Adjust bids based on hourly performance data — increase bids during high-converting hours, decrease during low-performing hours.
function main() {
var CONFIG = {
DATE_RANGE: 'LAST_30_DAYS',
TARGET_CAMPAIGNS: [], // Empty = all enabled campaigns
MAX_BID_MODIFIER: 0.30, // +30% max
MIN_BID_MODIFIER: -0.50, // -50% max
MIN_CONVERSIONS: 5, // Min conversions in an hour slot to adjust
SHEET_URL: '' // Optional: log modifiers to sheet
};
// Get hourly performance
var query = "SELECT " +
"campaign.name, " +
"campaign.resource_name, " +
"segments.hour, " +
"segments.day_of_week, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.conversions, " +
"metrics.cost_micros, " +
"metrics.conversions_value " +
"FROM campaign " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var hourlyData = {}; // { campaignName: { hour: { clicks, conv, cost } } }
while (results.hasNext()) {
var row = results.next();
var name = row.campaign.name;
var hour = row.segments.hour;
if (!hourlyData[name]) hourlyData[name] = {};
if (!hourlyData[name][hour]) {
hourlyData[name][hour] = { clicks: 0, conversions: 0, cost: 0, convValue: 0 };
}
hourlyData[name][hour].clicks += row.metrics.clicks;
hourlyData[name][hour].conversions += row.metrics.conversions;
hourlyData[name][hour].cost += row.metrics.costMicros / 1000000;
hourlyData[name][hour].convValue += row.metrics.conversionsValue || 0;
}
// Calculate and apply modifiers
for (var campaignName in hourlyData) {
var hours = hourlyData[campaignName];
// Calculate average conversion rate across all hours
var totalConv = 0;
var totalClicks = 0;
for (var h in hours) {
totalConv += hours[h].conversions;
totalClicks += hours[h].clicks;
}
var avgCvr = totalClicks > 0 ? totalConv / totalClicks : 0;
Logger.log('\nCampaign: ' + campaignName + ' | Avg CVR: ' + (avgCvr * 100).toFixed(2) + '%');
for (var hour = 0; hour < 24; hour++) {
var data = hours[hour] || { clicks: 0, conversions: 0, cost: 0 };
var hourCvr = data.clicks > 0 ? data.conversions / data.clicks : 0;
var modifier = 0;
if (avgCvr > 0 && data.conversions >= CONFIG.MIN_CONVERSIONS) {
modifier = (hourCvr / avgCvr) - 1;
modifier = Math.max(CONFIG.MIN_BID_MODIFIER, Math.min(CONFIG.MAX_BID_MODIFIER, modifier));
}
Logger.log(' Hour ' + hour + ':00 | Clicks: ' + data.clicks +
' | Conv: ' + data.conversions +
' | CVR: ' + (hourCvr * 100).toFixed(2) + '%' +
' | Modifier: ' + (modifier > 0 ? '+' : '') + (modifier * 100).toFixed(0) + '%');
}
}
}
6. Quality Score Tracker
Log quality score data to Google Sheets for historical tracking — Google Ads does not retain quality score history natively.
function main() {
var CONFIG = {
SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
SHEET_NAME: 'QS History',
MIN_IMPRESSIONS: 50,
DATE_RANGE: 'LAST_7_DAYS'
};
var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
.getSheetByName(CONFIG.SHEET_NAME);
// Add headers if sheet is empty
if (sheet.getLastRow() === 0) {
sheet.appendRow([
'Date', 'Campaign', 'Ad Group', 'Keyword', 'Match Type',
'Quality Score', 'Expected CTR', 'Ad Relevance', 'Landing Page Exp',
'Impressions', 'Clicks', 'Cost', 'Conversions'
]);
}
var today = Utilities.formatDate(new Date(), 'UTC', 'yyyy-MM-dd');
var query = "SELECT " +
"campaign.name, " +
"ad_group.name, " +
"ad_group_criterion.keyword.text, " +
"ad_group_criterion.keyword.match_type, " +
"ad_group_criterion.quality_info.quality_score, " +
"ad_group_criterion.quality_info.creative_quality_score, " +
"ad_group_criterion.quality_info.search_predicted_ctr, " +
"ad_group_criterion.quality_info.post_click_quality_score, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_criterion.status = 'ENABLED' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var rows = [];
while (results.hasNext()) {
var row = results.next();
var qi = row.adGroupCriterion.qualityInfo;
rows.push([
today,
row.campaign.name,
row.adGroup.name,
row.adGroupCriterion.keyword.text,
row.adGroupCriterion.keyword.matchType,
qi.qualityScore || '',
qi.searchPredictedCtr || '',
qi.creativeQualityScore || '',
qi.postClickQualityScore || '',
row.metrics.impressions,
row.metrics.clicks,
(row.metrics.costMicros / 1000000).toFixed(2),
row.metrics.conversions
]);
}
if (rows.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
.setValues(rows);
}
Logger.log('Logged ' + rows.length + ' keywords to sheet');
}
7. Ad Copy Testing (RSA Performance)
Compare responsive search ad performance across ad groups and flag winners and losers.
function main() {
var CONFIG = {
MIN_IMPRESSIONS: 500,
DATE_RANGE: 'LAST_30_DAYS',
WINNER_CTR_LIFT: 0.20, // 20% CTR lift = winner
LOSER_CTR_DROP: -0.20, // 20% CTR drop = loser
ALERT_EMAIL: 'team@example.com'
};
var query = "SELECT " +
"campaign.name, " +
"ad_group.name, " +
"ad_group_ad.ad.id, " +
"ad_group_ad.ad.responsive_search_ad.headlines, " +
"ad_group_ad.ad.responsive_search_ad.descriptions, " +
"ad_group_ad.ad_strength, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.ctr, " +
"metrics.conversions, " +
"metrics.cost_micros " +
"FROM ad_group_ad " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_ad.status = 'ENABLED' " +
"AND ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
// Group by ad group
var adGroups = {};
while (results.hasNext()) {
var row = results.next();
var agKey = row.campaign.name + ' > ' + row.adGroup.name;
if (!adGroups[agKey]) adGroups[agKey] = [];
adGroups[agKey].push({
adId: row.adGroupAd.ad.id,
strength: row.adGroupAd.adStrength,
impressions: row.metrics.impressions,
clicks: row.metrics.clicks,
ctr: row.metrics.ctr,
conversions: row.metrics.conversions,
cost: row.metrics.costMicros / 1000000
});
}
var winners = [];
var losers = [];
for (var agKey in adGroups) {
var ads = adGroups[agKey];
if (ads.length < 2) continue; // Need at least 2 ads to compare
// Calculate ad group average CTR
var totalClicks = 0;
var totalImpr = 0;
for (var i = 0; i < ads.length; i++) {
totalClicks += ads[i].clicks;
totalImpr += ads[i].impressions;
}
var avgCtr = totalImpr > 0 ? totalClicks / totalImpr : 0;
for (var j = 0; j < ads.length; j++) {
var ad = ads[j];
var ctrLift = avgCtr > 0 ? (ad.ctr - avgCtr) / avgCtr : 0;
if (ctrLift >= CONFIG.WINNER_CTR_LIFT) {
winners.push({ adGroup: agKey, ad: ad, lift: ctrLift });
} else if (ctrLift <= CONFIG.LOSER_CTR_DROP) {
losers.push({ adGroup: agKey, ad: ad, lift: ctrLift });
}
}
}
Logger.log('Winners: ' + winners.length + ' | Losers: ' + losers.length);
if ((winners.length > 0 || losers.length > 0) && CONFIG.ALERT_EMAIL) {
var body = 'Ad Copy Test Results (' + CONFIG.DATE_RANGE + ')\n\n';
if (winners.length > 0) {
body += 'WINNERS (CTR lift > ' + (CONFIG.WINNER_CTR_LIFT * 100) + '%):\n';
for (var w = 0; w < winners.length; w++) {
body += ' ' + winners[w].adGroup +
' | Ad #' + winners[w].ad.adId +
' | CTR: ' + (winners[w].ad.ctr * 100).toFixed(2) + '%' +
' | Lift: +' + (winners[w].lift * 100).toFixed(0) + '%\n';
}
}
if (losers.length > 0) {
body += '\nLOSERS (CTR drop > ' + (Math.abs(CONFIG.LOSER_CTR_DROP) * 100) + '%):\n';
for (var l = 0; l < losers.length; l++) {
body += ' ' + losers[l].adGroup +
' | Ad #' + losers[l].ad.adId +
' | CTR: ' + (losers[l].ad.ctr * 100).toFixed(2) + '%' +
' | Drop: ' + (losers[l].lift * 100).toFixed(0) + '%\n';
}
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] Ad Copy Test Results', body);
}
}
8. Budget Allocation by ROAS
Shift budget between campaigns based on return on ad spend — feed high performers, starve low performers.
function main() {
var CONFIG = {
TOTAL_DAILY_BUDGET: 500, // Total daily budget across campaigns
MIN_BUDGET: 20, // Minimum per-campaign daily budget
MAX_BUDGET: 200, // Maximum per-campaign daily budget
DATE_RANGE: 'LAST_14_DAYS',
CAMPAIGN_LABEL: 'Auto-Budget',
MIN_CONVERSIONS: 3, // Min conversions to be eligible for increase
DRY_RUN: true
};
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
.forDateRange(CONFIG.DATE_RANGE)
.get();
var campaignData = [];
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
var cost = stats.getCost();
var convValue = stats.getConversionValue();
var roas = cost > 0 ? convValue / cost : 0;
campaignData.push({
campaign: campaign,
name: campaign.getName(),
cost: cost,
convValue: convValue,
conversions: stats.getConversions(),
roas: roas,
currentBudget: campaign.getBudget().getAmount()
});
}
// Sort by ROAS descending
campaignData.sort(function(a, b) { return b.roas - a.roas; });
// Calculate budget allocation based on ROAS weighting
var totalRoas = 0;
for (var i = 0; i < campaignData.length; i++) {
if (campaignData[i].conversions >= CONFIG.MIN_CONVERSIONS) {
totalRoas += campaignData[i].roas;
}
}
var remaining = CONFIG.TOTAL_DAILY_BUDGET;
for (var j = 0; j < campaignData.length; j++) {
var c = campaignData[j];
var newBudget;
if (c.conversions >= CONFIG.MIN_CONVERSIONS && totalRoas > 0) {
newBudget = (c.roas / totalRoas) * CONFIG.TOTAL_DAILY_BUDGET;
} else {
newBudget = CONFIG.MIN_BUDGET;
}
newBudget = Math.max(CONFIG.MIN_BUDGET, Math.min(CONFIG.MAX_BUDGET, newBudget));
newBudget = Math.round(newBudget * 100) / 100;
Logger.log(c.name +
' | ROAS: ' + c.roas.toFixed(2) +
' | Current: $' + c.currentBudget.toFixed(2) +
' | New: $' + newBudget.toFixed(2) +
' | Change: ' + (newBudget > c.currentBudget ? '+' : '') +
'$' + (newBudget - c.currentBudget).toFixed(2));
if (!CONFIG.DRY_RUN) {
c.campaign.getBudget().setAmount(newBudget);
}
}
if (CONFIG.DRY_RUN) {
Logger.log('\nDRY RUN — no budgets changed.');
}
}
9. Anomaly Detection
Alert on sudden drops or spikes in impressions, clicks, cost, or conversions compared to historical averages.
function main() {
var CONFIG = {
LOOKBACK_DAYS: 14, // Compare against this many days
SPIKE_THRESHOLD: 2.0, // Alert if 2x the average
DROP_THRESHOLD: 0.5, // Alert if below 50% of average
ALERT_EMAIL: 'team@example.com',
METRICS: ['impressions', 'clicks', 'cost_micros', 'conversions']
};
var today = new Date();
var yesterday = new Date(today.getTime() - 86400000);
var lookbackStart = new Date(today.getTime() - (CONFIG.LOOKBACK_DAYS + 1) * 86400000);
var yesterdayStr = Utilities.formatDate(yesterday, 'UTC', 'yyyyMMdd');
var lookbackStr = Utilities.formatDate(lookbackStart, 'UTC', 'yyyyMMdd');
// Get yesterday's data
var query = "SELECT " +
"campaign.name, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"segments.date " +
"FROM campaign " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date BETWEEN '" +
lookbackStr.substring(0, 4) + '-' + lookbackStr.substring(4, 6) + '-' + lookbackStr.substring(6, 8) +
"' AND '" +
yesterdayStr.substring(0, 4) + '-' + yesterdayStr.substring(4, 6) + '-' + yesterdayStr.substring(6, 8) + "'";
var results = AdsApp.search(query);
// Aggregate by campaign and date
var campaignDays = {}; // { name: { date: { impressions, clicks, ... } } }
while (results.hasNext()) {
var row = results.next();
var name = row.campaign.name;
var date = row.segments.date;
if (!campaignDays[name]) campaignDays[name] = {};
campaignDays[name][date] = {
impressions: row.metrics.impressions,
clicks: row.metrics.clicks,
cost_micros: row.metrics.costMicros,
conversions: row.metrics.conversions
};
}
var alerts = [];
var yesterdayDate = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');
for (var campaignName in campaignDays) {
var days = campaignDays[campaignName];
var yesterdayData = days[yesterdayDate];
if (!yesterdayData) continue;
for (var m = 0; m < CONFIG.METRICS.length; m++) {
var metric = CONFIG.METRICS[m];
// Calculate historical average (excluding yesterday)
var sum = 0;
var count = 0;
for (var d in days) {
if (d !== yesterdayDate) {
sum += days[d][metric] || 0;
count++;
}
}
if (count === 0) continue;
var avg = sum / count;
var current = yesterdayData[metric] || 0;
if (avg > 0) {
var ratio = current / avg;
if (ratio >= CONFIG.SPIKE_THRESHOLD) {
alerts.push({
campaign: campaignName,
metric: metric,
type: 'SPIKE',
current: current,
average: avg,
ratio: ratio
});
} else if (ratio <= CONFIG.DROP_THRESHOLD) {
alerts.push({
campaign: campaignName,
metric: metric,
type: 'DROP',
current: current,
average: avg,
ratio: ratio
});
}
}
}
}
if (alerts.length > 0) {
Logger.log('ANOMALIES DETECTED: ' + alerts.length);
var body = 'Google Ads Anomaly Report — ' + yesterdayDate + '\n\n';
for (var a = 0; a < alerts.length; a++) {
var alert = alerts[a];
var metricLabel = alert.metric.replace('_micros', '').replace('_', ' ');
var currentVal = alert.metric === 'cost_micros'
? '$' + (alert.current / 1000000).toFixed(2)
: alert.current.toFixed(0);
var avgVal = alert.metric === 'cost_micros'
? '$' + (alert.average / 1000000).toFixed(2)
: alert.average.toFixed(0);
body += alert.type + ': ' + alert.campaign +
' | ' + metricLabel +
' | Yesterday: ' + currentVal +
' | Avg: ' + avgVal +
' | Ratio: ' + alert.ratio.toFixed(2) + 'x\n';
Logger.log(body);
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] ' + alerts.length + ' Anomalies Detected', body);
} else {
Logger.log('No anomalies detected.');
}
}
10. Landing Page Performance
Cross-reference landing page URLs with conversion data to find underperforming pages.
function main() {
var CONFIG = {
DATE_RANGE: 'LAST_30_DAYS',
MIN_CLICKS: 50,
SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
SHEET_NAME: 'Landing Pages'
};
var query = "SELECT " +
"landing_page_view.unexpanded_final_url, " +
"metrics.clicks, " +
"metrics.impressions, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"metrics.conversions_value, " +
"metrics.bounce_rate " +
"FROM landing_page_view " +
"WHERE segments.date DURING " + CONFIG.DATE_RANGE + " " +
"AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
"ORDER BY metrics.clicks DESC";
var results = AdsApp.search(query);
var pages = [];
while (results.hasNext()) {
var row = results.next();
var cost = row.metrics.costMicros / 1000000;
var cvr = row.metrics.clicks > 0
? row.metrics.conversions / row.metrics.clicks
: 0;
var roas = cost > 0
? row.metrics.conversionsValue / cost
: 0;
pages.push({
url: row.landingPageView.unexpandedFinalUrl,
clicks: row.metrics.clicks,
impressions: row.metrics.impressions,
cost: cost,
conversions: row.metrics.conversions,
convValue: row.metrics.conversionsValue,
cvr: cvr,
roas: roas,
bounceRate: row.metrics.bounceRate || 0
});
Logger.log(row.landingPageView.unexpandedFinalUrl +
' | Clicks: ' + row.metrics.clicks +
' | CVR: ' + (cvr * 100).toFixed(2) + '%' +
' | ROAS: ' + roas.toFixed(2));
}
// Export to sheet
if (CONFIG.SHEET_URL && pages.length > 0) {
var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
.getSheetByName(CONFIG.SHEET_NAME);
sheet.clear();
sheet.appendRow([
'URL', 'Clicks', 'Impressions', 'Cost', 'Conversions',
'Conv Value', 'CVR', 'ROAS', 'Bounce Rate'
]);
for (var i = 0; i < pages.length; i++) {
var p = pages[i];
sheet.appendRow([
p.url, p.clicks, p.impressions,
p.cost.toFixed(2), p.conversions,
p.convValue.toFixed(2),
(p.cvr * 100).toFixed(2) + '%',
p.roas.toFixed(2),
(p.bounceRate * 100).toFixed(2) + '%'
]);
}
Logger.log('Exported ' + pages.length + ' landing pages to sheet');
}
}
Google Sheets Integration
Google Ads Scripts can read from and write to Google Sheets using SpreadsheetApp. This is the most common way to build dashboards, log data, and configure scripts dynamically.
Opening a Spreadsheet
// By URL (recommended — works across accounts)
var ss = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/SHEET_ID/edit');
// By ID
var ss = SpreadsheetApp.openById('SHEET_ID');
// Access a specific sheet tab
var sheet = ss.getSheetByName('Data');
var activeSheet = ss.getActiveSheet();
Reading Data
// Read a specific range
var range = sheet.getRange('A1:D10');
var values = range.getValues(); // Returns 2D array
// Read all data
var allData = sheet.getDataRange().getValues();
// Iterate rows
for (var i = 1; i < allData.length; i++) { // Skip header row
var campaign = allData[i][0];
var target = allData[i][1];
Logger.log(campaign + ': ' + target);
}
// Read a single cell
var value = sheet.getRange('B2').getValue();
Writing Data
// Write a single value
sheet.getRange('A1').setValue('Campaign Name');
// Write a row
sheet.appendRow(['2025-02-11', 'Search - Brand', 150, 12, 3]);
// Write a 2D array (batch — much faster)
var data = [
['Campaign', 'Clicks', 'Cost', 'Conv'],
['Brand', 150, 45.00, 12],
['Generic', 300, 120.00, 8]
];
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
// Clear a sheet
sheet.clear();
Building an Automated Dashboard
function main() {
var ss = SpreadsheetApp.openByUrl('YOUR_SHEET_URL');
var sheet = ss.getSheetByName('Dashboard');
sheet.clear();
// Headers
sheet.appendRow([
'Campaign', 'Status', 'Impressions', 'Clicks', 'CTR',
'Cost', 'Conversions', 'CPA', 'ROAS', 'Updated'
]);
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.forDateRange('LAST_30_DAYS')
.orderBy('Cost DESC')
.get();
var rows = [];
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor('LAST_30_DAYS');
var cost = stats.getCost();
rows.push([
campaign.getName(),
campaign.isEnabled() ? 'Enabled' : 'Paused',
stats.getImpressions(),
stats.getClicks(),
(stats.getCtr() * 100).toFixed(2) + '%',
'$' + cost.toFixed(2),
stats.getConversions(),
stats.getConversions() > 0 ? '$' + (cost / stats.getConversions()).toFixed(2) : 'N/A',
cost > 0 ? (stats.getConversionValue() / cost).toFixed(2) : 'N/A',
new Date().toISOString()
]);
}
if (rows.length > 0) {
sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
}
Email and Notifications
MailApp.sendEmail()
// Simple text email
MailApp.sendEmail('team@example.com', 'Subject Line', 'Body text here');
// Email with options
MailApp.sendEmail({
to: 'team@example.com',
cc: 'manager@example.com',
subject: '[Google Ads] Weekly Report',
body: 'Plain text fallback',
htmlBody: '<h1>Weekly Report</h1><p>See details below.</p>'
});
HTML Email Template
function buildAlertEmail(alerts) {
var html = '<html><body style="font-family: Arial, sans-serif;">';
html += '<h2 style="color: #d93025;">Google Ads Alert</h2>';
html += '<table style="border-collapse: collapse; width: 100%;">';
html += '<tr style="background: #f1f3f4;">' +
'<th style="padding: 8px; text-align: left;">Campaign</th>' +
'<th style="padding: 8px; text-align: left;">Metric</th>' +
'<th style="padding: 8px; text-align: right;">Value</th>' +
'<th style="padding: 8px; text-align: right;">Threshold</th>' +
'</tr>';
for (var i = 0; i < alerts.length; i++) {
var bgColor = i % 2 === 0 ? '#ffffff' : '#f9f9f9';
html += '<tr style="background: ' + bgColor + ';">' +
'<td style="padding: 8px;">' + alerts[i].campaign + '</td>' +
'<td style="padding: 8px;">' + alerts[i].metric + '</td>' +
'<td style="padding: 8px; text-align: right;">' + alerts[i].value + '</td>' +
'<td style="padding: 8px; text-align: right;">' + alerts[i].threshold + '</td>' +
'</tr>';
}
html += '</table>';
html += '<p style="color: #666; font-size: 12px;">Generated by Google Ads Scripts</p>';
html += '</body></html>';
return html;
}
// Usage
MailApp.sendEmail({
to: 'team@example.com',
subject: '[Google Ads] ' + alerts.length + ' Alerts',
htmlBody: buildAlertEmail(alerts)
});
Alert Threshold Pattern
function checkAndAlert(metricName, currentValue, threshold, direction) {
var triggered = false;
if (direction === 'above' && currentValue > threshold) {
triggered = true;
} else if (direction === 'below' && currentValue < threshold) {
triggered = true;
}
if (triggered) {
Logger.log('ALERT: ' + metricName + ' = ' + currentValue + ' (threshold: ' + threshold + ')');
}
return triggered;
}
Scheduling
Google Ads Scripts can be scheduled to run automatically from the Google Ads interface.
Available Frequencies
| Frequency | When it runs |
|---|---|
| Hourly | Every hour on the hour |
| Daily | Once per day at your chosen time |
| Weekly | Once per week on your chosen day and time |
| Monthly | Once per month on your chosen day and time |
Setting Up a Schedule
- Navigate to Tools & Settings > Bulk Actions > Scripts
- Select or create your script
- Click the pencil icon next to "Frequency"
- Choose the frequency and time
- Click Save
Scheduling Best Practices
- Budget pacing: Run daily or hourly
- Negative keyword mining: Run weekly
- Quality score tracking: Run daily
- Anomaly detection: Run daily (morning)
- Broken URL checks: Run weekly
- Performance dashboards: Run daily
- Bid adjustments: Run daily after enough data accumulates
Error Handling
try/catch Pattern
function main() {
try {
processKeywords();
} catch (e) {
Logger.log('ERROR: ' + e.message);
Logger.log('Stack: ' + e.stack);
// Send error notification
MailApp.sendEmail(
'team@example.com',
'[Google Ads Script] Error in Keyword Processor',
'Error: ' + e.message + '\n\nStack trace:\n' + e.stack
);
}
}
function processKeywords() {
var keywords = AdsApp.keywords()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS')
.get();
var processed = 0;
var errors = 0;
while (keywords.hasNext()) {
try {
var keyword = keywords.next();
// Process keyword...
processed++;
} catch (e) {
errors++;
Logger.log('Error processing keyword: ' + e.message);
}
}
Logger.log('Processed: ' + processed + ' | Errors: ' + errors);
}
Logging
// Basic logging
Logger.log('Message here');
// Structured logging pattern
function log(level, message, data) {
var entry = '[' + level + '] ' + message;
if (data) {
entry += ' | ' + JSON.stringify(data);
}
Logger.log(entry);
}
log('INFO', 'Script started');
log('WARN', 'Low impressions', { campaign: 'Brand', impressions: 5 });
log('ERROR', 'Failed to update bid', { keyword: 'buy shoes', error: 'Budget exceeded' });
Execution Time Limits
| Script Type | Time Limit |
|---|---|
| Single-account | 30 minutes |
| MCC script | 60 minutes |
Handling long-running scripts:
function main() {
var startTime = new Date().getTime();
var MAX_RUNTIME = 25 * 60 * 1000; // 25 minutes (5 min buffer)
var keywords = AdsApp.keywords()
.withCondition('Status = ENABLED')
.get();
while (keywords.hasNext()) {
// Check remaining time
if (new Date().getTime() - startTime > MAX_RUNTIME) {
Logger.log('Approaching time limit. Stopping gracefully.');
break;
}
var keyword = keywords.next();
// Process...
}
}
MCC Scripts
MCC (My Client Center) scripts operate across multiple accounts using AdsManagerApp.
Basic MCC Script
function main() {
var accounts = AdsManagerApp.accounts()
.withCondition('Impressions > 0')
.forDateRange('LAST_7_DAYS')
.withLimit(50)
.get();
while (accounts.hasNext()) {
var account = accounts.next();
AdsManagerApp.select(account);
Logger.log('Processing: ' + account.getName() + ' (' + account.getCustomerId() + ')');
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS')
.get();
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor('LAST_7_DAYS');
Logger.log(' ' + campaign.getName() + ': ' + stats.getClicks() + ' clicks');
}
}
}
Parallel Processing with executeInParallel()
Process multiple accounts simultaneously for much faster execution.
function main() {
var accounts = AdsManagerApp.accounts()
.withCondition('Impressions > 0')
.forDateRange('LAST_7_DAYS')
.get();
// Execute processAccount() on each account in parallel
// Results are collected by aggregateResults()
accounts.executeInParallel('processAccount', 'aggregateResults');
}
// Runs on each account (has access to AdsApp for that account)
function processAccount() {
var account = AdsApp.currentAccount();
var result = {
accountName: account.getName(),
accountId: account.getCustomerId(),
campaigns: []
};
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS')
.get();
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor('LAST_7_DAYS');
result.campaigns.push({
name: campaign.getName(),
clicks: stats.getClicks(),
cost: stats.getCost(),
conversions: stats.getConversions()
});
}
// Must return a string (JSON)
return JSON.stringify(result);
}
// Called once after all parallel executions complete
function aggregateResults(results) {
var allData = [];
for (var i = 0; i < results.length; i++) {
if (results[i].getStatus() === 'OK') {
var data = JSON.parse(results[i].getReturnValue());
allData.push(data);
Logger.log('Account: ' + data.accountName + ' — ' + data.campaigns.length + ' campaigns');
} else {
Logger.log('Error for account: ' + results[i].getError());
}
}
// Export aggregated data to a sheet, send report, etc.
Logger.log('Total accounts processed: ' + allData.length);
}
executeInParallel() Rules
- The per-account function receives an optional string argument — pass config as JSON if needed
- The per-account function must return a string (use
JSON.stringify()) - The callback function receives an array of
ExecutionResultobjects - Each
ExecutionResulthas.getStatus()('OK'or'ERROR'),.getReturnValue(),.getError(),.getCustomerId() - Maximum 50 accounts per
executeInParallel()call - Each account execution has a 30-minute limit
Best Practices
1. Always Use Preview Mode First
Before running any script that modifies your account, use the Preview button. This executes the script in read-only mode and shows all Logger.log output without making any changes.
2. Log Before Modifying
// Log the change BEFORE making it
Logger.log('Pausing keyword: "' + keyword.getText() + '"' +
' in campaign: ' + keyword.getCampaign().getName() +
' | Reason: QS = ' + qs + ', CTR = ' + (ctr * 100).toFixed(2) + '%');
keyword.pause();
3. Use a DRY_RUN Config Flag
var CONFIG = {
DRY_RUN: true // Set to false only after verifying Preview output
};
// In your modification code:
if (!CONFIG.DRY_RUN) {
keyword.pause();
}
4. Incremental Rollout
Start with narrow targeting (one campaign, one ad group) and expand:
var campaigns = AdsApp.campaigns()
.withCondition('Name = "Search - Brand"') // Start narrow
// .withCondition('Status = ENABLED') // Expand later
.get();
5. Always Send Error Notifications
Do not rely on checking script logs manually. Send email alerts when scripts fail or detect anomalies.
6. Document Your Scripts
Add a comment block at the top of every script:
/**
* Script: Budget Pacing Monitor
* Purpose: Alerts when monthly spend is off pace
* Schedule: Daily at 9:00 AM
* Author: team@example.com
* Last modified: 2025-02-11
*
* Config:
* MONTHLY_BUDGET - Target monthly spend
* ALERT_THRESHOLD_OVER - % over pace before alert
* ALERT_EMAIL - Alert recipient
*/
7. Use Labels for Script Targeting
Instead of hardcoding campaign names, use labels:
// Add label "Auto-Bid" to campaigns in the UI
var campaigns = AdsApp.campaigns()
.withCondition('LabelNames CONTAINS_ANY ["Auto-Bid"]')
.get();
8. Batch Writes with Arrays
Writing row-by-row to Google Sheets is slow. Batch your writes:
// Slow: one API call per row
for (var i = 0; i < data.length; i++) {
sheet.appendRow(data[i]);
}
// Fast: one API call for all rows
sheet.getRange(2, 1, data.length, data[0].length).setValues(data);
Limitations
| Limitation | Details |
|---|---|
| Execution time | 30 minutes (single account), 60 minutes (MCC) |
| No async/await | Scripts run synchronously |
| Limited ES6 | No import/export, no class, limited destructuring |
| No Display/Video APIs | Limited support for Display and Video campaigns via scripts |
| No Performance Max | PMax campaigns have read-only access (limited reporting) |
| URL fetch | 50 MB response limit, 60-second timeout per request |
| MailApp | 100 recipients per day, email body size limits |
| SpreadsheetApp | Read/write limits apply (cells per operation) |
| No npm/modules | Cannot import external libraries |
| Account limits | executeInParallel() caps at 50 accounts per call |
Claude Code Skill
This reference is also available as a free Claude Code skill — use it directly in your terminal:
# Install
curl -sSL https://raw.githubusercontent.com/cognyai/claude-code-marketing-skills/main/install.sh | bash
# Use
/google-ads-scripts # Full overview
/google-ads-scripts budget pacing # Budget pacing monitor script
/google-ads-scripts negative keywords # Search term negative keyword miner
/google-ads-scripts MCC parallel # MCC parallel processing pattern
Resources
- Google Ads Scripts Documentation: developers.google.com/google-ads/scripts/docs/overview
- AdsApp Reference: developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp
- GAQL Reference: developers.google.com/google-ads/api/docs/query/overview
- Google Ads Scripts Examples: developers.google.com/google-ads/scripts/docs/solutions
- Claude Code Marketing Skills: github.com/cognyai/claude-code-marketing-skills