88% of PPC professionals manage Google Ads entirely by hand. They check budget pacing in spreadsheets. They scan search terms manually. They discover broken tracking weeks after it breaks.
The 12% who use custom scripts don't work harder. They work on different problems - strategy, creative, and the thinking that actually moves accounts forward - because the routine work runs itself.
These three scripts are where you start. They cover the two fastest money leaks in any e-commerce Google Ads account: budget overspend and wasted search terms. Plus a conversion tracking health check that should be running in every account, always.
Adapted from production scripts managing $10M+/mo in Google Ads spend across 20+ e-commerce brands.
Every script follows the same process. Takes about 5 minutes per script.
Google Sheets setup: Create a Google Sheet for script output. Share it with the email address shown in your Scripts editor (usually something like google-ads-scripts@your-account.iam.gserviceaccount.com). Copy the Sheet URL into each script's CONFIG.
What it does: Calculates daily spend versus linear pacing target for every campaign. Logs results to Google Sheets with color-coded status. Catches overspend and underspend the same day it happens.
Why this matters: One supplements brand was overspending by 130-140% on three campaigns daily. Nobody noticed for months because the total monthly spend looked normal. The overspend on some campaigns was masked by underspend on others. This script caught it in 72 hours. Monthly savings: $8,400.
Schedule: Daily at 6:00 PM.
function main() {
var CONFIG = {
SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL_HERE',
SHEET_NAME: 'Budget Pacing',
OVERSPEND_THRESHOLD: 1.20,
UNDERSPEND_LOW: 0.50,
UNDERSPEND_HIGH: 0.80
};
try {
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) { sheet = ss.insertSheet(CONFIG.SHEET_NAME); }
var today = new Date();
var dayOfMonth = today.getDate();
var daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
var pacingRatio = dayOfMonth / daysInMonth;
sheet.appendRow([
'Date', 'Campaign', 'Monthly Budget', 'Month Spend',
'Expected Spend', 'Pacing %', 'Status'
]);
var campaignIterator = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.get();
while (campaignIterator.hasNext()) {
var campaign = campaignIterator.next();
var stats = campaign.getStatsFor('THIS_MONTH');
var budget = campaign.getBudget().getAmount() * daysInMonth;
var spent = stats.getCost();
var expectedSpend = budget * pacingRatio;
var pacingPct = expectedSpend > 0 ? (spent / expectedSpend) : 0;
var status = 'GREEN';
if (pacingPct > CONFIG.OVERSPEND_THRESHOLD) {
status = 'RED - OVERSPEND';
} else if (pacingPct < CONFIG.UNDERSPEND_LOW) {
status = 'YELLOW - SEVERE UNDERSPEND';
} else if (pacingPct < CONFIG.UNDERSPEND_HIGH) {
status = 'YELLOW - UNDERSPEND';
}
sheet.appendRow([
Utilities.formatDate(today, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd'),
campaign.getName(),
budget.toFixed(2),
spent.toFixed(2),
expectedSpend.toFixed(2),
(pacingPct * 100).toFixed(1) + '%',
status
]);
}
Logger.log('Budget pacing report complete. ' + dayOfMonth + '/' + daysInMonth + ' days elapsed.');
} catch (e) {
Logger.log('ERROR: ' + e.message);
}
}
What to customize:
OVERSPEND_THRESHOLD: Lower to 1.10 for earlier warnings. 1.20 is the default because small daily fluctuations are normal.UNDERSPEND_LOW / UNDERSPEND_HIGH: Tighten for high-budget campaigns.What to watch for:
What it does: Finds search terms with 3+ clicks and zero conversions, plus terms with CTR below 0.5% on 100+ impressions. Outputs negative keyword suggestions with match type recommendations. Sends a weekly email digest.
Why this matters: A fashion retailer ran this script for 60 days and found 720 new negative keywords. That translated to $3,200/month in recovered spend. The terms were individually small - 2-3 clicks each - which is exactly why nobody caught them manually.
Schedule: Weekly, Monday at 7:00 AM.
function main() {
var CONFIG = {
SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL_HERE',
SHEET_NAME: 'Negative Candidates',
EMAIL_RECIPIENTS: 'you@company.com',
MIN_CLICKS_ZERO_CONV: 3,
LOW_CTR_THRESHOLD: 0.005,
MIN_IMPRESSIONS_CTR: 100,
DATE_RANGE: 'LAST_30_DAYS'
};
try {
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) { sheet = ss.insertSheet(CONFIG.SHEET_NAME); }
sheet.clear();
sheet.appendRow([
'Search Term', 'Campaign', 'Ad Group', 'Clicks', 'Impressions',
'Cost', 'CTR', 'Conversions', 'Reason', 'Suggested Match Type'
]);
var candidates = [];
var report = AdsApp.report(
'SELECT search_term, campaign.name, ad_group.name, metrics.clicks, ' +
'metrics.impressions, metrics.cost_micros, metrics.ctr, metrics.conversions ' +
'FROM search_term_view ' +
'WHERE segments.date DURING ' + CONFIG.DATE_RANGE
);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var clicks = parseFloat(row['metrics.clicks']);
var impressions = parseFloat(row['metrics.impressions']);
var conversions = parseFloat(row['metrics.conversions']);
var ctr = parseFloat(row['metrics.ctr']);
var cost = parseFloat(row['metrics.cost_micros']) / 1000000;
var reason = '';
var matchType = '';
if (clicks >= CONFIG.MIN_CLICKS_ZERO_CONV && conversions === 0) {
reason = clicks + '+ clicks, 0 conversions';
matchType = clicks >= 10 ? 'Exact' : 'Phrase';
}
else if (impressions >= CONFIG.MIN_IMPRESSIONS_CTR && ctr < CONFIG.LOW_CTR_THRESHOLD && conversions === 0) {
reason = 'CTR ' + (ctr * 100).toFixed(2) + '% on ' + impressions + ' impressions';
matchType = 'Phrase';
}
if (reason) {
var entry = [
row['search_term'], row['campaign.name'], row['ad_group.name'],
clicks, impressions, cost.toFixed(2), (ctr * 100).toFixed(2) + '%',
conversions, reason, matchType
];
candidates.push(entry);
sheet.appendRow(entry);
}
}
if (candidates.length > 0) {
var topCandidates = candidates.sort(function(a, b) { return b[5] - a[5]; }).slice(0, 20);
var emailBody = 'Negative Keyword Candidates Found: ' + candidates.length + '\n\n';
emailBody += 'Top 20 by Cost:\n\n';
for (var i = 0; i < topCandidates.length; i++) {
emailBody += (i + 1) + '. "' + topCandidates[i][0] + '" - $' + topCandidates[i][5] +
' spent, ' + topCandidates[i][3] + ' clicks, ' + topCandidates[i][8] + '\n';
}
MailApp.sendEmail(CONFIG.EMAIL_RECIPIENTS,
'[Scripts] ' + candidates.length + ' Negative Keyword Candidates Found',
emailBody);
}
Logger.log('Negative keyword mining complete. ' + candidates.length + ' candidates found.');
} catch (e) {
Logger.log('ERROR: ' + e.message);
}
}
What to customize:
MIN_CLICKS_ZERO_CONV: Lower to 2 for aggressive mining. Raise to 5 for conservative accounts.LOW_CTR_THRESHOLD: 0.5% is the standard. Fashion and luxury may need lower thresholds.What to watch for:
What it does: Lists all conversion actions with their status. Checks for zero-fire actions in the last 7 days. Alerts when any active conversion action stops firing. Compares 7-day versus 30-day conversion counts to catch recent breaks.
Why this matters: We've seen accounts where the conversion tag stopped firing 3 weeks ago and nobody noticed. The campaigns kept running. The algorithm kept optimizing - on zero data. By the time someone checked, the account had spent thousands bidding blind.
This script catches that within 24 hours.
Schedule: Daily at 8:00 AM.
function main() {
var CONFIG = {
SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL_HERE',
SHEET_NAME: 'Conversion Health',
EMAIL_RECIPIENTS: 'you@company.com'
};
try {
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) { sheet = ss.insertSheet(CONFIG.SHEET_NAME); }
sheet.clear();
var today = Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
sheet.appendRow(['Conversion Health Check - ' + today]);
sheet.appendRow([
'Campaign', '7d Conversions', '7d Conv Value',
'30d Conversions', '30d Conv Value', 'Health'
]);
var alerts = [];
var campaignIterator = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.get();
while (campaignIterator.hasNext()) {
var campaign = campaignIterator.next();
var stats7 = campaign.getStatsFor('LAST_7_DAYS');
var stats30 = campaign.getStatsFor('LAST_30_DAYS');
var conv7 = stats7.getConversions();
var conv30 = stats30.getConversions();
var value7 = stats7.getConversionValue();
var value30 = stats30.getConversionValue();
var health = 'HEALTHY';
if (conv30 > 5 && conv7 === 0) {
health = 'CRITICAL - Was converting, now stopped';
alerts.push(campaign.getName() + ': 0 conversions in 7d (had ' +
conv30 + ' in 30d)');
} else if (conv30 > 10 && conv7 < conv30 * 0.1) {
health = 'WARNING - Conversion volume dropped significantly';
alerts.push(campaign.getName() + ': ' + conv7 +
' conv in 7d vs ' + conv30 + ' in 30d');
}
if (health !== 'HEALTHY') {
sheet.appendRow([
campaign.getName(), conv7, value7.toFixed(2),
conv30, value30.toFixed(2), health
]);
}
}
if (alerts.length > 0) {
var body = 'CONVERSION TRACKING ALERT\n\n';
body += 'The following campaigns show potential tracking issues:\n\n';
body += alerts.join('\n');
body += '\n\nImmediate investigation recommended.';
MailApp.sendEmail(CONFIG.EMAIL_RECIPIENTS,
'[CRITICAL] Conversion Tracking Issue - ' + alerts.length + ' campaign(s)',
body);
}
Logger.log('Conversion health check complete. ' + alerts.length + ' issues found.');
} catch (e) {
Logger.log('ERROR: ' + e.message);
}
}
What to customize:
conv30 > 5 threshold prevents false positives on very low-volume campaigns. Raise for larger accounts.conv7 < conv30 * 0.1) catches severe drops. Set to 0.15 for more sensitive detection.What to watch for:
These three scripts cover the essentials: budget protection, search term waste, and tracking integrity.
The full Google Ads AI Agentic System ($4,997+) includes 35 scripts across 10 categories:
Every script includes a CONFIG object for customization, outputs to Google Sheets, includes error handling, and comes with setup, troubleshooting, and scheduling documentation.
Adapted from production systems managing $10M+/mo in Google Ads spend across 20+ e-commerce brands.
Install these three first. See what they find. Then decide if the other 32 are worth your time.