Free tool · Library

Google Ads scripts that watch the account.

Six read-only scripts our specialists run on client accounts. Add your email and thresholds once, copy a script, paste it into Google Ads and schedule it. None of them change anything in the account.

Use the tool
  • Free
  • No sign-up
  • Runs in your browser
Your settingsFilled into every script
01

Monthly budget pacing alert

Emails you when month-to-date spend runs more than a set percentage ahead of or behind your monthly budget, with the projected month-end spend.

Run daily
Show the code28 lines
// Serann Marketing · Monthly budget pacing alert (read only, changes nothing)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = {
  EMAIL: '__EMAIL__',            // where alerts go, comma separate several
  MONTHLY_BUDGET: __BUDGET__,    // in the account currency
  ALERT_PERCENT: 10              // alert when pacing is off by this much
};

function main() {
  var acc = AdsApp.currentAccount(), tz = acc.getTimeZone(), now = new Date();
  var day = +Utilities.formatDate(now, tz, 'd');
  var y = +Utilities.formatDate(now, tz, 'yyyy'), m = +Utilities.formatDate(now, tz, 'M');
  var daysInMonth = new Date(y, m, 0).getDate();
  var rows = AdsApp.search('SELECT metrics.cost_micros FROM customer WHERE segments.date DURING THIS_MONTH');
  var spend = 0;
  while (rows.hasNext()) spend += Number(rows.next().metrics.costMicros || 0) / 1e6;
  var expected = CONFIG.MONTHLY_BUDGET * day / daysInMonth;
  var projected = day > 0 ? spend / day * daysInMonth : 0;
  var off = expected > 0 ? (spend - expected) / expected * 100 : 0;
  var msg = 'Account: ' + acc.getName() + ' (' + acc.getCustomerId() + ')\n'
    + 'Spend this month: ' + spend.toFixed(2) + ' ' + acc.getCurrencyCode() + '\n'
    + 'Expected by today: ' + expected.toFixed(2) + '\n'
    + 'Projected month end: ' + projected.toFixed(2) + ' against a budget of ' + CONFIG.MONTHLY_BUDGET;
  Logger.log(msg);
  if (Math.abs(off) >= CONFIG.ALERT_PERCENT && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, 'Budget pacing: ' + acc.getName() + ' is ' + Math.abs(off).toFixed(0) + '% ' + (off > 0 ? 'over' : 'under'), msg);
  }
}
Read only. It changes nothing in the account.
02

Broken landing page checker

Checks the final URLs of every live ad and Performance Max asset group and emails you any page that returns an error, such as a 404 after a product was removed.

Run daily
Show the code34 lines
// Serann Marketing · Broken landing page checker (read only, changes nothing)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = {
  EMAIL: '__EMAIL__',
  MAX_URLS: 500                  // stay inside the script time limit
};

function main() {
  var urls = {};
  collect("SELECT ad_group_ad.ad.final_urls FROM ad_group_ad WHERE ad_group_ad.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND campaign.status = 'ENABLED'",
    function (r) { return r.adGroupAd.ad.finalUrls; }, urls);
  collect("SELECT asset_group.final_urls FROM asset_group WHERE asset_group.status = 'ENABLED' AND campaign.status = 'ENABLED'",
    function (r) { return r.assetGroup.finalUrls; }, urls);
  var bad = [], n = 0;
  for (var u in urls) {
    if (n++ >= CONFIG.MAX_URLS) break;
    try {
      var code = UrlFetchApp.fetch(u, { muteHttpExceptions: true, followRedirects: true }).getResponseCode();
      if (code >= 400) bad.push(code + '  ' + u);
    } catch (e) { bad.push('No response  ' + u); }
  }
  Logger.log('Checked ' + Math.min(n, CONFIG.MAX_URLS) + ' URLs, ' + bad.length + ' with errors');
  if (bad.length && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, bad.length + ' broken landing pages in ' + AdsApp.currentAccount().getName(), bad.join('\n'));
  }
}

function collect(query, pick, out) {
  var rows = AdsApp.search(query);
  while (rows.hasNext()) {
    var list = pick(rows.next()) || [];
    for (var i = 0; i < list.length; i++) out[String(list[i]).split('#')[0]] = true;
  }
}
Read only. It changes nothing in the account.
03

Search terms with spend and no sales

Lists the search terms of the last 30 days that spent more than your threshold without a single conversion, so you can review them as negatives.

Run weekly
Show the code25 lines
// Serann Marketing · Search terms with spend and no conversions (read only)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = {
  EMAIL: '__EMAIL__',
  MIN_COST: __MINCOST__,         // in the account currency
  DATE_RANGE: 'LAST_30_DAYS'
};

function main() {
  var q = 'SELECT search_term_view.search_term, campaign.name, ad_group.name, metrics.clicks, metrics.cost_micros, metrics.conversions'
    + ' FROM search_term_view WHERE segments.date DURING ' + CONFIG.DATE_RANGE
    + ' AND metrics.conversions = 0 AND metrics.cost_micros > ' + Math.round(CONFIG.MIN_COST * 1e6)
    + ' ORDER BY metrics.cost_micros DESC LIMIT 200';
  var rows = AdsApp.search(q), out = [], total = 0;
  while (rows.hasNext()) {
    var r = rows.next(), cost = Number(r.metrics.costMicros) / 1e6;
    total += cost;
    out.push(cost.toFixed(2) + '  |  ' + r.metrics.clicks + ' clicks  |  ' + r.searchTermView.searchTerm + '  |  ' + r.campaign.name + ' > ' + r.adGroup.name);
  }
  Logger.log(out.join('\n'));
  if (out.length && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, out.length + ' search terms spent ' + total.toFixed(2) + ' without a conversion',
      'Review these before adding negatives:\n\n' + out.join('\n'));
  }
}
Read only. It changes nothing in the account.
04

Daily drop and spike alert

Compares yesterday with the same weekday over the previous four weeks and emails you when clicks or conversions fall, or cost jumps, by more than your threshold.

Run daily
Show the code28 lines
// Serann Marketing · Daily drop and spike alert (read only, changes nothing)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = {
  EMAIL: '__EMAIL__',
  CHANGE_PERCENT: 40             // alert when a metric moves this much
};

function main() {
  var acc = AdsApp.currentAccount(), tz = acc.getTimeZone();
  function day(n) { return Utilities.formatDate(new Date(Date.now() - n * 864e5), tz, 'yyyy-MM-dd'); }
  var rows = AdsApp.search("SELECT segments.date, metrics.clicks, metrics.cost_micros, metrics.conversions FROM customer WHERE segments.date BETWEEN '" + day(29) + "' AND '" + day(1) + "'");
  var by = {};
  while (rows.hasNext()) {
    var r = rows.next();
    by[r.segments.date] = { clicks: Number(r.metrics.clicks), cost: Number(r.metrics.costMicros) / 1e6, conv: Number(r.metrics.conversions) };
  }
  var y = by[day(1)] || { clicks: 0, cost: 0, conv: 0 }, base = { clicks: 0, cost: 0, conv: 0 };
  [8, 15, 22, 29].forEach(function (n) { var x = by[day(n)] || { clicks: 0, cost: 0, conv: 0 }; base.clicks += x.clicks / 4; base.cost += x.cost / 4; base.conv += x.conv / 4; });
  var notes = [];
  function chg(a, b) { return b > 0 ? (a - b) / b * 100 : 0; }
  if (chg(y.clicks, base.clicks) <= -CONFIG.CHANGE_PERCENT) notes.push('Clicks ' + y.clicks + ' against a usual ' + base.clicks.toFixed(0));
  if (chg(y.conv, base.conv) <= -CONFIG.CHANGE_PERCENT) notes.push('Conversions ' + y.conv.toFixed(1) + ' against a usual ' + base.conv.toFixed(1));
  if (chg(y.cost, base.cost) >= CONFIG.CHANGE_PERCENT) notes.push('Cost ' + y.cost.toFixed(2) + ' against a usual ' + base.cost.toFixed(2));
  Logger.log(notes.length ? notes.join('\n') : 'Nothing unusual yesterday');
  if (notes.length && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, 'Unusual day in ' + acc.getName() + ' (' + day(1) + ')', notes.join('\n') + '\n\nCompared with the same weekday over the last four weeks.');
  }
}
Read only. It changes nothing in the account.
05

Disapproved ads report

Emails a list of enabled ads in enabled campaigns that Google has disapproved, with the campaign and ad group, so nothing sits switched off unnoticed.

Run daily
Show the code19 lines
// Serann Marketing · Disapproved ads report (read only, changes nothing)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = { EMAIL: '__EMAIL__' };

function main() {
  var q = "SELECT campaign.name, ad_group.name, ad_group_ad.ad.id FROM ad_group_ad"
    + " WHERE ad_group_ad.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND campaign.status = 'ENABLED'"
    + " AND ad_group_ad.policy_summary.approval_status = 'DISAPPROVED'";
  var rows = AdsApp.search(q), out = [];
  while (rows.hasNext()) {
    var r = rows.next();
    out.push(r.campaign.name + ' > ' + r.adGroup.name + '  |  ad ID ' + r.adGroupAd.ad.id);
  }
  Logger.log(out.length + ' disapproved ads\n' + out.join('\n'));
  if (out.length && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, out.length + ' disapproved ads in ' + AdsApp.currentAccount().getName(),
      out.join('\n') + '\n\nOpen Ads, filter by Status: Disapproved, to see the policy reason.');
  }
}
Read only. It changes nothing in the account.
06

Shopping products with spend and no sales

Finds products in Shopping and Performance Max that spent more than your threshold in the last 30 days without a conversion. Useful for feed and bid reviews.

Run weekly
Show the code24 lines
// Serann Marketing · Shopping products with spend and no sales (read only)
// https://serann.marketing/tools/google-ads-scripts/
var CONFIG = {
  EMAIL: '__EMAIL__',
  MIN_COST: __MINCOST__,         // in the account currency
  DATE_RANGE: 'LAST_30_DAYS'
};

function main() {
  var q = 'SELECT segments.product_item_id, segments.product_title, metrics.clicks, metrics.cost_micros, metrics.conversions'
    + ' FROM shopping_performance_view WHERE segments.date DURING ' + CONFIG.DATE_RANGE
    + ' AND metrics.conversions = 0 AND metrics.cost_micros > ' + Math.round(CONFIG.MIN_COST * 1e6)
    + ' ORDER BY metrics.cost_micros DESC LIMIT 200';
  var rows = AdsApp.search(q), out = [], total = 0;
  while (rows.hasNext()) {
    var r = rows.next(), cost = Number(r.metrics.costMicros) / 1e6;
    total += cost;
    out.push(cost.toFixed(2) + '  |  ' + r.metrics.clicks + ' clicks  |  ' + r.segments.productItemId + '  |  ' + r.segments.productTitle);
  }
  Logger.log(out.join('\n'));
  if (out.length && CONFIG.EMAIL.indexOf('@') > 0) {
    MailApp.sendEmail(CONFIG.EMAIL, out.length + ' products spent ' + total.toFixed(2) + ' without a sale', out.join('\n'));
  }
}
Read only. It changes nothing in the account.

Runs in your browser. Nothing you type or upload is stored or sent.

How to read it

Install a script in two minutes.

01

Open Scripts

In Google Ads go to Tools, Bulk actions, Scripts, and click the plus button to add a new script.

02

Paste and authorise

Delete the sample code, paste the script, name it and click Authorise. Google asks for permission to read the account and send email as you.

03

Preview first

Click Preview. The log shows what the script found, without sending anything twice. If the log looks right, save it.

04

Schedule it

Set the frequency shown on each script, daily or weekly, ideally early in the morning so the email is waiting when you start work.

Questions

Before you ask.

Can these scripts break my account?

No. All six only read data and send email. None of them pause, change or create anything.

Do they work in a manager account?

They are written for a single account. In a manager account, add them inside each client account, or ask us for the manager versions.

Why did I get no email?

Each script only emails when it finds something. Check the log after a preview: if it lists results, check that the email setting is correct.

Is this code free to use?

Yes. Use and change it in your own and your clients' accounts. Keep the link at the top if you share it.

Next step

Want someone watching every day?

Send a short brief. A specialist reads your account with read-only access and comes back with the three changes we would make first.

01You send a short brief about the business.
02We read the account with read-only access.
03You get three changes in writing, yours to keep either way.
Get in touch