13 Free Google Ads Scripts to Automate & Scale Your Account
Copy-and-use scripts that monitor your budget, catch wasted spend, flag broken pages and build your reporting — the automations we run for D2C brands, free for you to install in 2 minutes.
✅ Free & MIT-licensed🔒 Safe — test in Preview⚡ 2-min install🛒 Shopping & PMax ready
Google Ads scripts are small pieces of JavaScript that run inside your Google Ads account and automate the repetitive, easy-to-forget work — checking budgets, pausing money-losing keywords, catching disapproved ads, flagging broken landing pages and building reports. They run on Google's servers on a schedule you set, so the work happens whether you're at your desk or not.
For a growing D2C brand, that's the difference between finding a wasted-spend problem three weeks late and getting an email the morning it starts. Every script below is free, documented, and does one useful job well. You don't need to be a developer — paste, tweak a few settings, and schedule.
Scripts handle the monitoring; strategy is what compounds. If you'd like the automations plus the thinking behind them, see how we run performance marketing and CRM automation for D2C brands — or browse the questions founders ask us most before your next agency call.
How to install a script in 2 minutes
In Google Ads, open Tools → Bulk actions → Scripts and click the blue +.
Paste the script's code (use the Copy button on any script below).
Edit the CONFIG block at the top — your email, thresholds, and a blank Google Sheet URL where needed.
Click Authorise, then Preview to see exactly what it would do — safely, with no changes.
Click Save and set a schedule (daily / weekly). Done.
🔔 Monitoring & alerts
Know the moment something breaks — spend, impression share, disapprovals, anomalies.
Account budget monitor
↻ Daily
Emails you the moment month-to-date spend crosses a hard cap, or when the account is pacing to overshoot your monthly budget.
BudgetAlerts
◂ Show the script▾ Hide the script
/**
* ACCOUNT BUDGET MONITOR
* Emails you when month-to-date (MTD) spend crosses a hard cap, or when the account is
* pacing to overshoot a monthly budget. Run daily (morning).
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com', // where alerts go (comma-separate for multiple)
MONTHLY_BUDGET: 300000, // your monthly budget in account currency (e.g. ₹300000)
HARD_CAP: 320000, // email immediately if MTD spend exceeds this
PACING_ALERT_PCT: 105 // email if projected month-end spend > this % of MONTHLY_BUDGET
};
function main() {
var rows = AdsApp.search(
'SELECT metrics.cost_micros FROM campaign WHERE segments.date DURING THIS_MONTH');
var mtd = 0;
while (rows.hasNext()) { mtd += Number(rows.next().metrics.costMicros) / 1e6; }
var now = new Date();
var dayOfMonth = now.getDate();
var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
var projected = mtd / dayOfMonth * daysInMonth;
var pacePct = (projected / CONFIG.MONTHLY_BUDGET) * 100;
var msgs = [];
if (mtd >= CONFIG.HARD_CAP) {
msgs.push('🚨 MTD spend ' + fmt(mtd) + ' has crossed the HARD CAP of ' + fmt(CONFIG.HARD_CAP) + '.');
}
if (pacePct >= CONFIG.PACING_ALERT_PCT) {
msgs.push('⚠️ Pacing to ' + fmt(projected) + ' by month end (' + pacePct.toFixed(0) +
'% of the ' + fmt(CONFIG.MONTHLY_BUDGET) + ' budget).');
}
Logger.log('MTD: ' + fmt(mtd) + ' | Projected: ' + fmt(projected) + ' | Pace: ' + pacePct.toFixed(0) + '%');
if (!msgs.length) return;
var body = '<h3>Google Ads budget alert — ' + AdsApp.currentAccount().getName() + '</h3><ul><li>' +
msgs.join('</li><li>') + '</li></ul>' +
'<p>Month-to-date: <b>' + fmt(mtd) + '</b><br>Projected month-end: <b>' + fmt(projected) +
'</b><br>Budget: ' + fmt(CONFIG.MONTHLY_BUDGET) + '</p>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] Budget alert — ' + AdsApp.currentAccount().getName(), htmlBody: body });
}
function fmt(n) { return AdsApp.currentAccount().getCurrencyCode() + ' ' + Math.round(n).toLocaleString(); }
Impression share monitor
↻ Daily
Alerts you when campaigns lose impression share to budget (raise budget) or to rank (improve bids/Quality Score) beyond a threshold.
Impression shareAlerts
◂ Show the script▾ Hide the script
/**
* IMPRESSION SHARE MONITOR
* Alerts when active campaigns are losing impression share to BUDGET or RANK above a threshold
* over the last 7 days — the two clearest signals of "raise budget" vs "improve quality/bids".
* Run daily.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com',
LOST_IS_BUDGET_MAX: 0.10, // alert if >10% impressions lost to budget
LOST_IS_RANK_MAX: 0.20, // alert if >20% impressions lost to rank
MIN_IMPRESSIONS: 500 // ignore tiny campaigns
};
function main() {
var rows = AdsApp.search(
'SELECT campaign.name, metrics.impressions, ' +
'metrics.search_budget_lost_impression_share, ' +
'metrics.search_rank_lost_impression_share, ' +
'metrics.search_impression_share ' +
'FROM campaign ' +
'WHERE campaign.status = "ENABLED" AND campaign.advertising_channel_type = "SEARCH" ' +
'AND segments.date DURING LAST_7_DAYS');
var flags = [];
while (rows.hasNext()) {
var r = rows.next();
if (Number(r.metrics.impressions) < CONFIG.MIN_IMPRESSIONS) continue;
var lostBudget = Number(r.metrics.searchBudgetLostImpressionShare || 0);
var lostRank = Number(r.metrics.searchRankLostImpressionShare || 0);
var isShare = Number(r.metrics.searchImpressionShare || 0);
var reasons = [];
if (lostBudget > CONFIG.LOST_IS_BUDGET_MAX) reasons.push('budget ' + pct(lostBudget) + ' → raise budget');
if (lostRank > CONFIG.LOST_IS_RANK_MAX) reasons.push('rank ' + pct(lostRank) + ' → improve QS/bids');
if (reasons.length) {
flags.push('<tr><td>' + r.campaign.name + '</td><td>' + pct(isShare) +
'</td><td>' + reasons.join('<br>') + '</td></tr>');
}
}
Logger.log(flags.length + ' campaign(s) flagged for lost impression share.');
if (!flags.length) return;
var body = '<h3>Impression share alert — ' + AdsApp.currentAccount().getName() + '</h3>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse">' +
'<tr><th>Campaign</th><th>Impr. share</th><th>Lost to (7d)</th></tr>' + flags.join('') + '</table>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] Impression-share alert', htmlBody: body });
}
function pct(n) { return (n * 100).toFixed(0) + '%'; }
Disapproved ads alert
↻ Daily
Emails a list of any disapproved ads the same day, so a policy issue never quietly kills your delivery.
AdsAlerts
◂ Show the script▾ Hide the script
/**
* DISAPPROVED ADS ALERT
* Emails a list of any disapproved ads (and the reason) so you can fix them the same day —
* disapproved ads stop serving and quietly kill delivery. Run daily.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = { EMAIL: 'you@yourbrand.com' };
function main() {
var rows = AdsApp.search(
'SELECT campaign.name, ad_group.name, ad_group_ad.ad.id, ' +
'ad_group_ad.ad.type, ad_group_ad.policy_summary.approval_status, ' +
'ad_group_ad.policy_summary.review_status ' +
'FROM ad_group_ad ' +
'WHERE ad_group_ad.policy_summary.approval_status = "DISAPPROVED" ' +
'AND ad_group_ad.status = "ENABLED" AND campaign.status = "ENABLED"');
var list = [];
while (rows.hasNext()) {
var r = rows.next();
list.push('<tr><td>' + r.campaign.name + '</td><td>' + r.adGroup.name +
'</td><td>' + r.adGroupAd.ad.type + '</td><td>' + r.adGroupAd.ad.id + '</td></tr>');
}
Logger.log(list.length + ' disapproved ad(s).');
if (!list.length) return;
var body = '<h3>' + list.length + ' disapproved ad(s) — ' + AdsApp.currentAccount().getName() + '</h3>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse">' +
'<tr><th>Campaign</th><th>Ad group</th><th>Type</th><th>Ad ID</th></tr>' + list.join('') + '</table>' +
'<p>Open Google Ads → Ads → filter by "Disapproved" to see the exact policy reason and fix.</p>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] ' + list.length + ' disapproved ads', htmlBody: body });
}
Account anomaly detector
↻ Hourly
Compares today's spend, clicks and conversions to the recent norm and flags sharp deviations — catches tracking breaks and runaway spend.
AnomaliesAlerts
◂ Show the script▾ Hide the script
/**
* ACCOUNT ANOMALY DETECTOR
* Compares TODAY's spend / clicks / conversions (so far) against the same point-in-day average
* over the previous N weeks. Emails you if any metric deviates beyond a tolerance — catches
* tracking breaks, runaway spend, or a sudden drop. Run hourly.
*
* Inspired by Google's open-source Account Anomaly Detector (Apache-2.0), rewritten & simplified.
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com',
WEEKS_LOOKBACK: 4,
TOLERANCE_LOW: 0.5, // alert if today < 50% of expected
TOLERANCE_HIGH: 1.8 // alert if today > 180% of expected
};
function main() {
var hour = new Date().getHours();
var today = stats('TODAY');
// average of the same weekday, up to the current hour, over the last N weeks
var hist = { cost: 0, clicks: 0, conv: 0, n: 0 };
for (var w = 1; w <= CONFIG.WEEKS_LOOKBACK; w++) {
var d = dateNDaysAgo(7 * w);
var s = statsForDate(d, hour);
hist.cost += s.cost; hist.clicks += s.clicks; hist.conv += s.conv; hist.n++;
}
if (hist.n === 0) return;
var exp = { cost: hist.cost / hist.n, clicks: hist.clicks / hist.n, conv: hist.conv / hist.n };
var alerts = [];
check('Cost', today.cost, exp.cost, alerts);
check('Clicks', today.clicks, exp.clicks, alerts);
check('Conversions', today.conv, exp.conv, alerts);
Logger.log('Today so far — cost:' + today.cost.toFixed(0) + ' clicks:' + today.clicks + ' conv:' + today.conv);
if (!alerts.length) return;
var body = '<h3>⚠️ Anomaly detected — ' + AdsApp.currentAccount().getName() + '</h3><ul><li>' +
alerts.join('</li><li>') + '</li></ul>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] Account anomaly detected', htmlBody: body });
}
function check(label, actual, expected, out) {
if (expected < 1) return; // skip near-zero baselines
var ratio = actual / expected;
if (ratio < CONFIG.TOLERANCE_LOW || ratio > CONFIG.TOLERANCE_HIGH) {
out.push(label + ': today ' + actual.toFixed(0) + ' vs expected ~' + expected.toFixed(0) +
' (' + (ratio * 100).toFixed(0) + '%)');
}
}
function stats(range) {
var r = AdsApp.search('SELECT metrics.cost_micros, metrics.clicks, metrics.conversions ' +
'FROM customer WHERE segments.date DURING ' + range);
return sum(r);
}
function statsForDate(d, uptoHour) {
var ds = Utilities.formatDate(d, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
var r = AdsApp.search('SELECT metrics.cost_micros, metrics.clicks, metrics.conversions, segments.hour ' +
'FROM customer WHERE segments.date = "' + ds + '" AND segments.hour <= ' + uptoHour);
return sum(r);
}
function sum(iter) {
var o = { cost: 0, clicks: 0, conv: 0 };
while (iter.hasNext()) { var x = iter.next(); o.cost += Number(x.metrics.costMicros) / 1e6; o.clicks += Number(x.metrics.clicks); o.conv += Number(x.metrics.conversions); }
return o;
}
function dateNDaysAgo(n) { var d = new Date(); d.setDate(d.getDate() - n); return d; }
Day-of-week anomaly detector
↻ Daily
Baselines each campaign against the SAME weekday over the last 26 weeks and emails you only the campaigns whose CPC, cost, conversions, CPA or impression share broke 2 standard deviations from normal yesterday. The fastest way to catch fallout from a bidding change without watching dashboards.
AnomaliesBiddingAlerts
◂ Show the script▾ Hide the script
/**
* Day-of-Week Anomaly Detector · Google Ads Script
* ----------------------------------------------------------------------------
* Flags campaigns whose performance YESTERDAY broke from their normal pattern
* for that weekday. The baseline is the SAME weekday over the previous 26 weeks
* (e.g. if yesterday was a Tuesday, it compares against the 26 prior Tuesdays).
*
* Why day-of-week? Weekday and weekend traffic behave nothing alike, so a plain
* trailing-28-day average smears them together and hides real shifts. Baselining
* per weekday is the whole point — and it's the fastest way to catch the fallout
* from a bidding change (like Google's Aug 2026 target-based bidding update),
* a tracking break, or runaway spend, without watching dashboards all day.
*
* Metrics checked : avg CPC, cost, conversions, conversion rate, cost/conv,
* search impression share.
* Flag rule : yesterday's value is >= SD_THRESHOLD standard deviations
* from its 26-week weekday mean, in EITHER direction.
* Output : ONE summary email listing only the flagged campaigns.
* Sends nothing if nothing is flagged.
*
* Safe by design : read-only. It never changes your account — it only reads and
* emails. Click Preview the first time to see the log.
*
* Free to use & modify (MIT). By Digistex4u — https://www.digistex4u.com
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*
* ============================== CONFIG ==================================== */
var CONFIG = {
RECIPIENT_EMAIL : 'you@yourbrand.com', // where the alert email is sent
SD_THRESHOLD : 2.0, // flag when |deviation| >= this many standard deviations
LOOKBACK_WEEKS : 26, // size of the weekday baseline window
MIN_DATA_POINTS : 15, // need at least this many past same-weekday points to score
MIN_CLICKS_YDAY : 10, // skip near-zero-volume campaigns (yesterday's clicks)
MIN_AVG_COST : 1.0, // skip campaigns whose baseline avg daily cost is below this
ONLY_ENABLED : true // consider enabled campaigns only
};
/* ========================================================================= */
function main() {
var tz = AdsApp.currentAccount().getTimeZone();
var cur = AdsApp.currentAccount().getCurrencyCode();
var YDAY = fmt(daysAgo(1), tz); // yesterday
var START = fmt(daysAgo(1 + CONFIG.LOOKBACK_WEEKS * 7), tz); // 26 weeks before
var targetWD = weekdayOf(YDAY); // 0=Sun .. 6=Sat
// Pull the whole 26-week window in ONE query, then bucket by weekday in code
// (far cheaper than issuing 26 separate weekday queries).
var query =
'SELECT campaign.id, campaign.name, segments.date, ' +
'metrics.clicks, metrics.impressions, metrics.cost_micros, ' +
'metrics.conversions, metrics.search_impression_share ' +
'FROM campaign ' +
"WHERE segments.date BETWEEN '" + START + "' AND '" + YDAY + "' " +
(CONFIG.ONLY_ENABLED ? "AND campaign.status = 'ENABLED' " : '') +
'AND metrics.impressions > 0';
var rows = AdsApp.search(query);
var camps = {}; // id -> { name, days: { 'yyyy-MM-dd': {metrics} } }
while (rows.hasNext()) {
var r = rows.next();
var id = String(r.campaign.id);
var date = r.segments.date;
var clicks = num(r.metrics.clicks);
var cost = num(r.metrics.costMicros) / 1e6;
var conv = num(r.metrics.conversions);
var sisRaw = r.metrics.searchImpressionShare;
var sis = (sisRaw === null || sisRaw === undefined) ? null : num(sisRaw);
if (!camps[id]) camps[id] = { name: r.campaign.name, days: {} };
camps[id].days[date] = {
clicks : clicks,
cost : cost,
conv : conv,
avg_cpc : clicks > 0 ? cost / clicks : null, // guard div-by-zero
conv_rate : clicks > 0 ? conv / clicks : null,
cpa : conv > 0 ? cost / conv : null,
sis : sis
};
}
var METRICS = [
{ key: 'avg_cpc', label: 'Avg CPC', money: true },
{ key: 'cost', label: 'Cost', money: true },
{ key: 'conv', label: 'Conversions', money: false },
{ key: 'conv_rate', label: 'Conversion rate', pct: true },
{ key: 'cpa', label: 'Cost / conversion', money: true },
{ key: 'sis', label: 'Search impression share', pct: true }
];
var flags = [];
for (var id in camps) {
var c = camps[id];
var yd = c.days[YDAY];
if (!yd) continue; // no data for yesterday
if (yd.clicks < CONFIG.MIN_CLICKS_YDAY) continue; // volume floor
// gather prior points on the SAME weekday (excluding yesterday itself)
var hist = [];
for (var d in c.days) {
if (d !== YDAY && weekdayOf(d) === targetWD) hist.push(c.days[d]);
}
if (hist.length < CONFIG.MIN_DATA_POINTS) continue; // not enough history
if (mean(pluck(hist, 'cost')) < CONFIG.MIN_AVG_COST) continue; // trivial spend
for (var m = 0; m < METRICS.length; m++) {
var mk = METRICS[m].key;
var yVal = yd[mk];
if (yVal === null || yVal === undefined) continue; // metric N/A yesterday
var vals = pluck(hist, mk);
if (vals.length < CONFIG.MIN_DATA_POINTS) continue;
var mu = mean(vals);
var sd = stdev(vals, mu);
if (sd === 0) continue; // zero variance — can't score
var z = (yVal - mu) / sd;
if (Math.abs(z) >= CONFIG.SD_THRESHOLD) {
flags.push({ camp: c.name, metric: METRICS[m].label, yVal: yVal, mean: mu, z: z, fmt: METRICS[m] });
}
}
}
if (!flags.length) {
Logger.log('No anomalies flagged for ' + YDAY + '. No email sent.');
return;
}
flags.sort(function(a, b){ return Math.abs(b.z) - Math.abs(a.z); }); // biggest deviation first
var acct = AdsApp.currentAccount().getName() || AdsApp.currentAccount().getCustomerId();
var word = flags.length > 1 ? 'anomalies' : 'anomaly';
var subject = '[Google Ads] ' + flags.length + ' ' + word + ' flagged — ' + acct + ' (' + YDAY + ')';
var out = [];
out.push('Day-of-week anomaly check — ' + acct);
out.push('Yesterday: ' + YDAY + ' (' + weekdayName(targetWD) + ') · baseline: last ' +
CONFIG.LOOKBACK_WEEKS + ' ' + weekdayName(targetWD) + 's · threshold: ' +
CONFIG.SD_THRESHOLD + ' SD');
out.push('');
for (var i = 0; i < flags.length; i++) {
var f = flags[i], arrow = f.z > 0 ? '▲ up' : '▼ down';
out.push(arrow + ' ' + f.camp);
out.push(' ' + f.metric + ': ' + showVal(f.yVal, f.fmt, cur) +
' (normal ' + showVal(f.mean, f.fmt, cur) + ')' +
' → ' + (f.z > 0 ? '+' : '') + f.z.toFixed(1) + ' SD');
}
out.push('');
out.push('An anomaly is a signal, not a verdict — investigate before acting.');
out.push('— Free Google Ads script by Digistex4u · https://www.digistex4u.com/google-ads-scripts');
out.push('Want us to run this for your brand? Get a free audit → https://www.digistex4u.com/free-audit');
MailApp.sendEmail(CONFIG.RECIPIENT_EMAIL, subject, out.join('\n'));
Logger.log('Sent ' + flags.length + ' anomaly flag(s) to ' + CONFIG.RECIPIENT_EMAIL);
}
/* ------------------------------- helpers --------------------------------- */
function daysAgo(n) { var d = new Date(); d.setDate(d.getDate() - n); return d; }
function fmt(d, tz) { return Utilities.formatDate(d, tz, 'yyyy-MM-dd'); }
function weekdayOf(s) { var p = s.split('-'); return new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])).getUTCDay(); }
function weekdayName(w) { return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][w]; }
function num(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }
function pluck(arr, k) { var o = []; for (var i = 0; i < arr.length; i++) { var v = arr[i][k]; if (v !== null && v !== undefined && !isNaN(v)) o.push(v); } return o; }
function mean(a) { if (!a.length) return 0; var s = 0; for (var i = 0; i < a.length; i++) s += a[i]; return s / a.length; }
function stdev(a, mu) { if (a.length < 2) return 0; var s = 0; for (var i = 0; i < a.length; i++) { var d = a[i] - mu; s += d * d; } return Math.sqrt(s / (a.length - 1)); }
function showVal(v, f, cur) {
if (f.pct) return (v * 100).toFixed(1) + '%';
if (f.money) return cur + ' ' + v.toFixed(2);
return v < 10 ? v.toFixed(2) : String(Math.round(v));
}
✂️ Optimisation & waste
Find and cut the money you're wasting on keywords and search terms.
Pause non-converting keywords
↻ Weekly
Finds keywords that spent over a limit in 30 days with zero conversions, and (optionally) pauses them. Dry-run by default.
KeywordsWaste
◂ Show the script▾ Hide the script
/**
* PAUSE NON-CONVERTING KEYWORDS
* Finds enabled keywords that spent over a limit in the last 30 days with ZERO conversions.
* Reports them (and can auto-pause). Run weekly.
*
* SAFETY: DRY_RUN = true by default — it only reports. Set DRY_RUN = false to actually pause.
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com',
MIN_COST: 20, // flag keywords that spent more than this (account currency) with 0 conversions
DATE_RANGE: 'LAST_30_DAYS',
DRY_RUN: true // <-- set to false to auto-pause the flagged keywords
};
function main() {
var q = 'SELECT ad_group_criterion.resource_name, ad_group_criterion.keyword.text, ' +
'campaign.name, ad_group.name, metrics.cost_micros, metrics.clicks ' +
'FROM keyword_view ' +
'WHERE segments.date DURING ' + CONFIG.DATE_RANGE + ' ' +
'AND ad_group_criterion.status = "ENABLED" AND campaign.status = "ENABLED" ' +
'AND metrics.conversions = 0 AND metrics.cost_micros > ' + Math.round(CONFIG.MIN_COST * 1e6);
var rows = AdsApp.search(q);
var resourceNames = [], report = [];
while (rows.hasNext()) {
var r = rows.next();
resourceNames.push(r.adGroupCriterion.resourceName);
report.push('<tr><td>' + r.campaign.name + '</td><td>' + r.adGroup.name + '</td><td>' +
r.adGroupCriterion.keyword.text + '</td><td>' + fmt(Number(r.metrics.costMicros) / 1e6) +
'</td><td>' + r.metrics.clicks + '</td></tr>');
}
Logger.log(resourceNames.length + ' non-converting keyword(s) found. DRY_RUN=' + CONFIG.DRY_RUN);
if (!resourceNames.length) return;
var paused = 0;
if (!CONFIG.DRY_RUN) {
var it = AdsApp.keywords().withResourceNames(resourceNames).get();
while (it.hasNext()) { it.next().pause(); paused++; }
}
var body = '<h3>' + resourceNames.length + ' non-converting keyword(s) — ' +
AdsApp.currentAccount().getName() + '</h3>' +
'<p>' + (CONFIG.DRY_RUN ? 'DRY RUN — nothing paused. Review below, then set DRY_RUN=false.'
: '✅ Auto-paused ' + paused + ' keyword(s).') + '</p>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse">' +
'<tr><th>Campaign</th><th>Ad group</th><th>Keyword</th><th>Cost</th><th>Clicks</th></tr>' +
report.join('') + '</table>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] Non-converting keywords', htmlBody: body });
}
function fmt(n) { return AdsApp.currentAccount().getCurrencyCode() + ' ' + Math.round(n).toLocaleString(); }
Search-term N-gram waste finder
↻ Weekly
Breaks every search term into 1- and 2-word n-grams and surfaces the words costing money with no return — your negative-keyword shortlist, in a Sheet.
Search termsNegatives
◂ Show the script▾ Hide the script
/**
* SEARCH-TERM N-GRAM WASTE FINDER
* Breaks every search term (last 30 days) into 1-word and 2-word "n-grams", sums cost & conversions
* per n-gram, and surfaces the words costing you money with little/no return — your negative-keyword
* shortlist. Writes a ranked table to a Google Sheet. Run weekly.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
SPREADSHEET_URL: 'PASTE_A_BLANK_GOOGLE_SHEET_URL_HERE',
DATE_RANGE: 'LAST_30_DAYS',
MIN_COST: 15, // only report n-grams that spent more than this
MAX_CONVERSIONS: 0.5 // ...and converted at or below this (0.5 = basically none)
};
function main() {
var rows = AdsApp.search(
'SELECT search_term_view.search_term, metrics.cost_micros, metrics.clicks, metrics.conversions ' +
'FROM search_term_view WHERE segments.date DURING ' + CONFIG.DATE_RANGE);
var grams = {}; // gram -> {cost, clicks, conv, terms}
while (rows.hasNext()) {
var r = rows.next();
var term = (r.searchTermView.searchTerm || '').toLowerCase();
var cost = Number(r.metrics.costMicros) / 1e6;
var clicks = Number(r.metrics.clicks);
var conv = Number(r.metrics.conversions);
ngrams(term).forEach(function (g) {
if (!grams[g]) grams[g] = { cost: 0, clicks: 0, conv: 0 };
grams[g].cost += cost; grams[g].clicks += clicks; grams[g].conv += conv;
});
}
var out = [];
for (var g in grams) {
var d = grams[g];
if (d.cost >= CONFIG.MIN_COST && d.conv <= CONFIG.MAX_CONVERSIONS) {
out.push([g, round(d.cost), d.clicks, round(d.conv), d.clicks ? round(d.cost / d.clicks) : 0]);
}
}
out.sort(function (a, b) { return b[1] - a[1]; });
Logger.log(out.length + ' wasteful n-grams found.');
var sheet = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL).getActiveSheet();
sheet.clear();
sheet.appendRow(['N-gram (candidate negative)', 'Cost', 'Clicks', 'Conversions', 'CPC']);
if (out.length) sheet.getRange(2, 1, out.length, 5).setValues(out);
sheet.getRange('A1:E1').setFontWeight('bold');
}
function ngrams(term) {
var words = term.split(/\s+/).filter(function (w) { return w.length > 1; });
var set = {};
for (var i = 0; i < words.length; i++) {
set[words[i]] = 1;
if (i < words.length - 1) set[words[i] + ' ' + words[i + 1]] = 1;
}
return Object.keys(set);
}
function round(n) { return Math.round(n * 100) / 100; }
Negative keyword conflict finder
↻ Weekly
Flags negative keywords that are silently blocking your own active keywords — a common cause of lost impressions.
KeywordsAudit
◂ Show the script▾ Hide the script
/**
* NEGATIVE KEYWORD CONFLICT FINDER
* Flags negative keywords that are blocking your own ENABLED keywords in the same ad group /
* campaign — a common, silent cause of lost impressions. Heuristic (exact & phrase logic),
* reports only. Run weekly.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = { EMAIL: 'you@yourbrand.com' };
function main() {
// 1) collect enabled positive keywords by ad group
var pos = {}; // adGroupId -> [ {text, match} ]
var kq = AdsApp.search(
'SELECT ad_group.id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type ' +
'FROM keyword_view WHERE ad_group_criterion.status = "ENABLED" AND campaign.status = "ENABLED"');
while (kq.hasNext()) {
var k = kq.next();
var id = k.adGroup.id;
(pos[id] = pos[id] || []).push({ text: (k.adGroupCriterion.keyword.text || '').toLowerCase() });
}
// 2) walk ad-group negatives and test them against those positives
var conflicts = [];
var nq = AdsApp.search(
'SELECT campaign.name, ad_group.name, ad_group.id, ' +
'ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type ' +
'FROM ad_group_criterion ' +
'WHERE ad_group_criterion.negative = true AND ad_group_criterion.type = "KEYWORD"');
while (nq.hasNext()) {
var n = nq.next();
var negText = (n.adGroupCriterion.keyword.text || '').toLowerCase();
var negMatch = n.adGroupCriterion.keyword.matchType;
var list = pos[n.adGroup.id] || [];
list.forEach(function (p) {
if (blocks(negText, negMatch, p.text)) {
conflicts.push('<tr><td>' + n.campaign.name + '</td><td>' + n.adGroup.name +
'</td><td>[-] ' + negText + ' (' + negMatch + ')</td><td>' + p.text + '</td></tr>');
}
});
}
Logger.log(conflicts.length + ' negative conflict(s) found.');
if (!conflicts.length) return;
var body = '<h3>' + conflicts.length + ' negative-keyword conflict(s) — ' +
AdsApp.currentAccount().getName() + '</h3>' +
'<p>These negatives are blocking your own active keywords. Remove or narrow them.</p>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse">' +
'<tr><th>Campaign</th><th>Ad group</th><th>Negative</th><th>Blocks keyword</th></tr>' +
conflicts.join('') + '</table>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] Negative keyword conflicts', htmlBody: body });
}
// does a negative (exact/phrase/broad) block a positive keyword's text?
function blocks(negText, negMatch, posText) {
if (negMatch === 'EXACT') return negText === posText;
// PHRASE and BROAD: every negative word must appear in the positive (phrase = consecutive)
if (negMatch === 'PHRASE') return (' ' + posText + ' ').indexOf(' ' + negText + ' ') > -1;
var negWords = negText.split(/\s+/), posWords = posText.split(/\s+/);
return negWords.every(function (w) { return posWords.indexOf(w) > -1; });
}
🛒 Shopping & Performance Max
Surface the products quietly draining your Shopping and PMax budget.
Zero-conversion products
↻ Weekly
Lists Shopping / Performance Max products that spent over a limit with no conversions — the SKUs draining your budget. Reports to a Sheet + email.
ShoppingPMaxWaste
◂ Show the script▾ Hide the script
/**
* ZERO-CONVERSION PRODUCTS (Shopping / Performance Max)
* Lists products that spent over a limit in the last 30 days with NO conversions — the SKUs
* quietly draining your Shopping/PMax budget. Exclude them or fix their feed/landing page.
* Reports to a Sheet + email. Run weekly.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com',
SPREADSHEET_URL: 'PASTE_A_BLANK_GOOGLE_SHEET_URL_HERE',
DATE_RANGE: 'LAST_30_DAYS',
MIN_COST: 25 // flag products that spent more than this with 0 conversions
};
function main() {
var rows = AdsApp.search(
'SELECT segments.product_item_id, segments.product_title, campaign.name, ' +
'metrics.cost_micros, metrics.clicks, metrics.conversions ' +
'FROM shopping_performance_view WHERE segments.date DURING ' + CONFIG.DATE_RANGE);
var agg = {}; // itemId -> {title, cost, clicks, conv, campaigns}
while (rows.hasNext()) {
var r = rows.next();
var id = r.segments.productItemId || '(unknown)';
if (!agg[id]) agg[id] = { title: r.segments.productTitle || '', cost: 0, clicks: 0, conv: 0 };
agg[id].cost += Number(r.metrics.costMicros) / 1e6;
agg[id].clicks += Number(r.metrics.clicks);
agg[id].conv += Number(r.metrics.conversions);
}
var out = [];
for (var id in agg) {
var d = agg[id];
if (d.cost >= CONFIG.MIN_COST && d.conv === 0) {
out.push([id, d.title, Math.round(d.cost * 100) / 100, d.clicks]);
}
}
out.sort(function (a, b) { return b[2] - a[2]; });
Logger.log(out.length + ' zero-conversion product(s) over ' + CONFIG.MIN_COST + '.');
if (CONFIG.SPREADSHEET_URL.indexOf('http') === 0) {
var sh = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL).getActiveSheet();
sh.clear();
sh.appendRow(['Product ID', 'Title', 'Cost', 'Clicks']);
if (out.length) sh.getRange(2, 1, out.length, 4).setValues(out);
sh.getRange('A1:D1').setFontWeight('bold');
}
if (out.length && CONFIG.EMAIL.indexOf('@') > 0) {
var top = out.slice(0, 25).map(function (r) {
return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td><td>' + fmt(r[2]) + '</td><td>' + r[3] + '</td></tr>';
}).join('');
MailApp.sendEmail({
to: CONFIG.EMAIL, subject: '[Google Ads] ' + out.length + ' products spending with 0 conversions',
htmlBody: '<h3>Zero-conversion products — ' + AdsApp.currentAccount().getName() + '</h3>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse">' +
'<tr><th>ID</th><th>Title</th><th>Cost</th><th>Clicks</th></tr>' + top + '</table>' + '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>'
});
}
}
function fmt(n) { return AdsApp.currentAccount().getCurrencyCode() + ' ' + Math.round(n).toLocaleString(); }
🩺 Quality & health
Never pay for a dead landing page; track Quality Score over time.
Broken landing page (404) checker
↻ Daily
Crawls the final URLs of your live ads and keywords and emails any that return a 404 or error — so you never pay for clicks to a dead page.
Landing pagesHealth
◂ Show the script▾ Hide the script
/**
* BROKEN LANDING PAGE (URL) CHECKER
* Crawls the final URLs of your enabled ads & keywords and emails any that return an error
* (404, 5xx, timeouts) — so you never pay for clicks to a dead page. Run daily.
*
* Based on Google's open-source Link Checker (Apache-2.0), rewritten & simplified.
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
EMAIL: 'you@yourbrand.com',
MAX_URLS: 400, // safety cap per run (scripts have a ~30-min limit)
BAD_CODES: [0, 400, 403, 404, 410, 500, 502, 503, 504]
};
function main() {
var urls = {};
collect('SELECT ad_group_ad.ad.final_urls FROM ad_group_ad WHERE ad_group_ad.status = "ENABLED" AND campaign.status = "ENABLED"',
function (r) { return r.adGroupAd && r.adGroupAd.ad ? r.adGroupAd.ad.finalUrls : null; }, urls);
collect('SELECT ad_group_criterion.final_urls FROM keyword_view WHERE ad_group_criterion.status = "ENABLED" AND campaign.status = "ENABLED"',
function (r) { return r.adGroupCriterion ? r.adGroupCriterion.finalUrls : null; }, urls);
var list = Object.keys(urls).slice(0, CONFIG.MAX_URLS);
var broken = [];
list.forEach(function (u) {
var code;
try {
code = UrlFetchApp.fetch(u, { muteHttpExceptions: true, followRedirects: true,
validateHttpsCertificates: false }).getResponseCode();
} catch (e) { code = 0; }
if (CONFIG.BAD_CODES.indexOf(code) > -1) broken.push({ url: u, code: code });
});
Logger.log('Checked ' + list.length + ' URLs, ' + broken.length + ' broken.');
if (!broken.length) return;
var body = '<h3>' + broken.length + ' broken landing page(s) — ' + AdsApp.currentAccount().getName() + '</h3>' +
'<table border="1" cellpadding="6" style="border-collapse:collapse"><tr><th>Status</th><th>URL</th></tr>' +
broken.map(function (b) { return '<tr><td>' + (b.code || 'ERR') + '</td><td>' + b.url + '</td></tr>'; }).join('') +
'</table><p>Pause the ads/keywords pointing here or fix the pages.</p>';
body += '<hr style="margin:16px 0;border:none;border-top:1px solid #e5e7eb"><p style="font-family:Arial,sans-serif;font-size:13px;color:#8a8f98;margin:0">Free Google Ads script by <b>Digistex4u</b>. Want us to run this for your brand? <a href="https://www.digistex4u.com/free-audit">Get a free audit →</a></p>';
MailApp.sendEmail({ to: CONFIG.EMAIL, subject: '[Google Ads] ' + broken.length + ' broken landing pages', htmlBody: body });
}
function collect(gaql, getUrls, into) {
var rows = AdsApp.search(gaql);
while (rows.hasNext()) {
var arr = getUrls(rows.next());
if (arr && arr.length) arr.forEach(function (u) { if (u) into[u] = 1; });
}
}
Quality Score tracker
↻ Daily
Logs every keyword's Quality Score to a Google Sheet each day, so you can see the trend Google itself hides.
Quality ScoreTracking
◂ Show the script▾ Hide the script
/**
* QUALITY SCORE TRACKER
* Appends today's Quality Score for every enabled keyword to a Google Sheet, so you can watch
* QS trend over time (Google only shows the current value). Run daily.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
SPREADSHEET_URL: 'PASTE_A_BLANK_GOOGLE_SHEET_URL_HERE',
MIN_IMPRESSIONS: 1 // keywords need impressions (last 30d) to have a QS
};
function main() {
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName('QS Log') || ss.insertSheet('QS Log');
if (sheet.getLastRow() === 0) sheet.appendRow(['Date', 'Campaign', 'Ad group', 'Keyword', 'Quality Score']);
var today = Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
var rows = AdsApp.search(
'SELECT campaign.name, ad_group.name, ad_group_criterion.keyword.text, ' +
'ad_group_criterion.quality_info.quality_score, metrics.impressions ' +
'FROM keyword_view ' +
'WHERE ad_group_criterion.status = "ENABLED" AND campaign.status = "ENABLED" ' +
'AND segments.date DURING LAST_30_DAYS AND metrics.impressions >= ' + CONFIG.MIN_IMPRESSIONS);
var out = [];
while (rows.hasNext()) {
var r = rows.next();
var qs = r.adGroupCriterion.qualityInfo ? r.adGroupCriterion.qualityInfo.qualityScore : '';
if (qs) out.push([today, r.campaign.name, r.adGroup.name, r.adGroupCriterion.keyword.text, qs]);
}
Logger.log('Logging QS for ' + out.length + ' keywords.');
if (out.length) sheet.getRange(sheet.getLastRow() + 1, 1, out.length, 5).setValues(out);
}
📊 Reporting
Your own always-fresh Google Sheets — no manual exports.
Daily performance → Google Sheet
↻ Daily
Appends yesterday's per-campaign performance (cost, conversions, ROAS, CPA…) to a Sheet — your own live reporting tab, zero manual exports.
ReportingSheets
◂ Show the script▾ Hide the script
/**
* DAILY PERFORMANCE → GOOGLE SHEET
* Appends yesterday's per-campaign performance (cost, clicks, conv, conv value, ROAS, CPA…) to a
* Google Sheet — your own always-fresh reporting tab, no manual exports. Run daily (morning).
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = { SPREADSHEET_URL: 'PASTE_A_BLANK_GOOGLE_SHEET_URL_HERE' };
function main() {
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName('Daily') || ss.insertSheet('Daily');
if (sheet.getLastRow() === 0) {
sheet.appendRow(['Date', 'Campaign', 'Channel', 'Cost', 'Clicks', 'Impr.', 'CTR%',
'Conversions', 'Conv. value', 'ROAS', 'CPA', 'Avg CPC']);
sheet.getRange('A1:L1').setFontWeight('bold');
}
var date = Utilities.formatDate(dateYesterday(), AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
var rows = AdsApp.search(
'SELECT campaign.name, campaign.advertising_channel_type, ' +
'metrics.cost_micros, metrics.clicks, metrics.impressions, ' +
'metrics.conversions, metrics.conversions_value ' +
'FROM campaign WHERE segments.date DURING YESTERDAY AND metrics.impressions > 0');
var out = [];
while (rows.hasNext()) {
var r = rows.next();
var cost = Number(r.metrics.costMicros) / 1e6;
var clicks = Number(r.metrics.clicks);
var impr = Number(r.metrics.impressions);
var conv = Number(r.metrics.conversions);
var val = Number(r.metrics.conversionsValue);
out.push([date, r.campaign.name, r.campaign.advertisingChannelType,
round(cost), clicks, impr, impr ? round(clicks / impr * 100) : 0,
round(conv), round(val), cost ? round(val / cost) : 0,
conv ? round(cost / conv) : 0, clicks ? round(cost / clicks) : 0]);
}
Logger.log('Appending ' + out.length + ' campaign rows for ' + date);
if (out.length) sheet.getRange(sheet.getLastRow() + 1, 1, out.length, 12).setValues(out);
}
function dateYesterday() { var d = new Date(); d.setDate(d.getDate() - 1); return d; }
function round(n) { return Math.round(n * 100) / 100; }
Ad schedule (dayparting) heatmap
↻ Weekly
Builds a day-of-week × hour performance heatmap in a Sheet, so you can set ad-schedule bid adjustments with real data.
DaypartingBidding
◂ Show the script▾ Hide the script
/**
* AD SCHEDULE (DAYPARTING) HEATMAP
* Builds a day-of-week × hour grid of performance (cost, conversions, CPA) over the last 30 days
* in a Google Sheet, so you can see exactly when your account converts — and set ad-schedule bid
* adjustments with confidence. Run weekly.
*
* Digistex4u — free & MIT. Test in PREVIEW first.
* Want us to run this for your brand? Free audit → https://www.digistex4u.com/free-audit
*/
var CONFIG = {
SPREADSHEET_URL: 'PASTE_A_BLANK_GOOGLE_SHEET_URL_HERE',
METRIC: 'CPA' // what to show in the grid: 'CPA', 'COST', or 'CONV'
};
var DAYS = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
function main() {
// grid[day][hour] = {cost, conv}
var grid = {};
DAYS.forEach(function (d) { grid[d] = {}; for (var h = 0; h < 24; h++) grid[d][h] = { cost: 0, conv: 0 }; });
var rows = AdsApp.search(
'SELECT segments.day_of_week, segments.hour, metrics.cost_micros, metrics.conversions ' +
'FROM campaign WHERE segments.date DURING LAST_30_DAYS');
while (rows.hasNext()) {
var r = rows.next();
var d = r.segments.dayOfWeek, h = Number(r.segments.hour);
if (!grid[d]) continue;
grid[d][h].cost += Number(r.metrics.costMicros) / 1e6;
grid[d][h].conv += Number(r.metrics.conversions);
}
var header = ['Hour \\ Day'].concat(DAYS.map(function (d) { return d.substr(0, 3); }));
var table = [header];
for (var h = 0; h < 24; h++) {
var row = [pad(h) + ':00'];
DAYS.forEach(function (d) {
var c = grid[d][h];
var v = CONFIG.METRIC === 'COST' ? c.cost : CONFIG.METRIC === 'CONV' ? c.conv
: (c.conv ? c.cost / c.conv : 0); // CPA
row.push(Math.round(v * 100) / 100);
});
table.push(row);
}
var ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
var sheet = ss.getSheetByName('Heatmap') || ss.insertSheet('Heatmap');
sheet.clear();
sheet.getRange(1, 1, table.length, table[0].length).setValues(table);
sheet.getRange(1, 1, 1, table[0].length).setFontWeight('bold');
// conditional colour scale over the data cells for a real "heatmap"
var dataRange = sheet.getRange(2, 2, 24, DAYS.length);
var rule = SpreadsheetApp.newConditionalFormatRule()
.setGradientMinpoint('#57bb8a').setGradientMidpointWithValue('#ffd666', SpreadsheetApp.InterpolationType.PERCENTILE, '50')
.setGradientMaxpoint('#e67c73').setRanges([dataRange]).build();
sheet.setConditionalFormatRules([rule]);
Logger.log('Heatmap (' + CONFIG.METRIC + ') written.');
}
function pad(n) { return (n < 10 ? '0' : '') + n; }
📦 All 13 scripts · one download
Get the full Google Ads script pack
Every script on this page in one folder, with setup notes and a config cheat-sheet — free. We'll send you new scripts as we add them.
Want these run for your brand — with the strategy behind them?
Scripts are the easy part. Digistex4u runs Google Ads for D2C brands end to end — feed, structure, bidding, creative and reporting as one system. Book a free call and we'll wire up the right automations for your account.
FAQ
Google Ads scripts — questions
Are these Google Ads scripts really free?
Yes — every script on this page is free to copy, use and modify (MIT licence). They're original implementations plus adaptations of Google's open-source examples. No sign-up, no paywall.
How do I add a Google Ads script to my account?
In Google Ads go to Tools → Bulk actions → Scripts, click the + button, paste the code, edit the CONFIG block at the top (your email, thresholds, Sheet URL), click Authorise, then Preview to test. Save and set a schedule once you're happy.
Are Google Ads scripts safe to run?
Yes, if you test first. Always click Preview (not Run) the first time — it shows what the script would do without making changes. Reporting and alert scripts never change your account. The one script that can pause keywords is set to dry-run by default until you explicitly enable it.
Do these work for Shopping and Performance Max?
Several do — the budget monitor, anomaly detector, broken-URL checker and reporting scripts run account-wide, and there's a dedicated zero-conversion products script for Shopping and PMax.
Can you set these up and manage them for my brand?
Yes. Digistex4u runs Google Ads for D2C brands end to end — scripts, structure, feed, creative and reporting as one system. Book a free call and we'll wire up the right automations for your account.