ads-system.store

Disapproved ads alert

read-only

Lists enabled ads that are not fully approved, with the policy topics behind each decision, and separately flags ad groups where no ad is servable at all, which is the failure that actually stops traffic.

requires
Any account with enabled ads.
writes
Nothing. Read-only.
schedule
Daily
change first
EMAIL and INCLUDE_LIMITED at the top.
disapproved-ads-alert.js
// Disapproved ads alert
// Lists every enabled ad in an enabled ad group and campaign whose approval status is
// not APPROVED, with the policy topics behind it. Also flags ad groups that have no
// servable ad left, which is the failure that actually stops traffic.
// Read-only: it reports, it pauses nothing.

var EMAIL          = '';    // leave empty to log only
var INCLUDE_LIMITED = true; // APPROVED_LIMITED still serves, but with restrictions

function main() {
  var rows = AdsApp.search(
    'SELECT campaign.name, ad_group.id, 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.policy_topic_entries ' +
    'FROM ad_group_ad ' +
    'WHERE ad_group_ad.status = "ENABLED" ' +
    '  AND ad_group.status = "ENABLED" ' +
    '  AND campaign.status = "ENABLED"'
  );

  var problems  = [];
  var perGroup  = {};   // ad group id -> { name, campaign, total, servable }
  var seen      = 0;

  while (rows.hasNext()) {
    var r  = rows.next();
    seen  += 1;

    var gid    = String(r.adGroup.id);
    var status = r.adGroupAd.policySummary
      ? String(r.adGroupAd.policySummary.approvalStatus)
      : 'UNKNOWN';

    if (!perGroup[gid]) {
      perGroup[gid] = {
        name:     String(r.adGroup.name),
        campaign: String(r.campaign.name),
        total:    0,
        servable: 0
      };
    }
    perGroup[gid].total += 1;
    if (status === 'APPROVED' || status === 'APPROVED_LIMITED') {
      perGroup[gid].servable += 1;
    }

    var report = (status !== 'APPROVED') &&
                 (INCLUDE_LIMITED || status !== 'APPROVED_LIMITED');

    if (report) {
      problems.push({
        campaign: String(r.campaign.name),
        adGroup:  String(r.adGroup.name),
        adId:     String(r.adGroupAd.ad.id),
        adType:   String(r.adGroupAd.ad.type),
        status:   status,
        topics:   policyTopics(r)
      });
    }
  }

  Logger.log(seen + ' enabled ads checked.');
  if (!seen) {
    Logger.log('No enabled ads found. That is itself worth checking.');
    return;
  }

  var empty = [];
  for (var gid2 in perGroup) {
    if (perGroup[gid2].servable === 0) { empty.push(perGroup[gid2]); }
  }

  if (!problems.length && !empty.length) {
    Logger.log('Every enabled ad is approved.');
    return;
  }

  var lines = [];

  if (empty.length) {
    lines.push('AD GROUPS WITH NO SERVABLE AD (' + empty.length + ')');
    lines.push('campaign\tad group\tads');
    for (var e = 0; e < empty.length; e++) {
      lines.push(empty[e].campaign + '\t' + empty[e].name + '\t' + empty[e].total);
    }
    lines.push('');
  }

  if (problems.length) {
    lines.push('ADS NOT FULLY APPROVED (' + problems.length + ')');
    lines.push('campaign\tad group\tad id\ttype\tstatus\tpolicy topics');
    for (var p = 0; p < problems.length; p++) {
      var x = problems[p];
      lines.push([x.campaign, x.adGroup, x.adId, x.adType, x.status, x.topics].join('\t'));
    }
  }

  var body = AdsApp.currentAccount().getName() + '\n' + lines.join('\n') +
    '\n\nAn ad group with no servable ad is not serving at all. Fix those first.';

  Logger.log(body);
  if (EMAIL) {
    MailApp.sendEmail(
      EMAIL,
      'Ad approval issues (' + (problems.length + empty.length) + ') - ' + AdsApp.currentAccount().getName(),
      body
    );
  }
}

function policyTopics(row) {
  var s = row.adGroupAd.policySummary;
  if (!s || !s.policyTopicEntries || !s.policyTopicEntries.length) { return ''; }
  var names = [];
  for (var i = 0; i < s.policyTopicEntries.length; i++) {
    var entry = s.policyTopicEntries[i];
    var topic = entry.topic ? String(entry.topic) : '';
    var type  = entry.type  ? String(entry.type)  : '';
    names.push(type ? (topic + ' (' + type + ')') : topic);
  }
  return names.join('; ');
}

The number that matters is not the disapproval count

A single disapproved ad in an ad group with three others is a housekeeping task. An ad group where every ad is disapproved is not serving at all, and no alert built on counting disapprovals will distinguish between the two.

This script reports both, and puts the ad groups with nothing servable at the top. That section is the one that needs an answer today.

Approval statuses, and which ones to care about

Status Serving Action
APPROVED Yes None
APPROVED_LIMITED Yes, with restrictions Worth reading. Often a geographic or audience restriction you did not intend.
DISAPPROVED No Fix or replace
AREA_OF_INTEREST_ONLY Only in some regions Check whether those regions are the ones you target

INCLUDE_LIMITED controls whether the limited statuses appear. Leave it on for the first month: limited approvals are the ones nobody looks at, and they quietly remove reach in specific markets.

Policy topics

The last column lists the policy topics behind each decision, so the email tells you whether this is a trademark issue, a destination problem or a restricted category, rather than sending you back into the interface to find out.

Topics repeating across many ads usually mean one cause: a landing page change, a claim in a shared description asset, or a policy applied to the domain rather than the ad.

What it does not cover

Performance Max assets are not included

Performance Max has no ads in the ad_group_ad sense, so its assets do not appear here. Asset-level approval within asset groups has to be checked in the interface. This is a real gap and no script closes it cleanly.

The script also reports only enabled ads in enabled ad groups and enabled campaigns. A disapproved ad in a paused campaign is not a problem worth an email at 07:00.

Notes

  • Run it daily. Disapprovals arrive without warning, frequently after an unrelated landing page edit.
  • The script sends nothing when everything is approved. Check the run history occasionally, or a script that has been failing for three weeks will look identical to a clean account.
  • It never pauses anything. A disapproved ad costs nothing to leave in place, and pausing it automatically removes the record of why it was there.