The Scripts Desk
RSA Asset Auditor: list every headline Google has quietly rated LOW
A read-only script that reads responsive search ad asset ratings, lists every headline and description Google has labelled LOW to a Sheet, and never touches the ads.
Google rates every headline and description in a responsive search ad, and quietly buries the rating three clicks deep in the ad strength panel, one ad at a time. Nobody audits an account that way, so weak copy sits in rotation for months. This script pulls the asset performance labels for every enabled search ad, collects everything rated LOW, and writes it to a Sheet grouped by campaign and ad group so thin copy is visible at a glance. It never pauses an ad or edits an asset. It flags the wording, a human rewrites it.
Before you start
- You need access to the account and permission to authorise scripts.
- Create a Google Sheet for the output and copy its URL into the CONFIG block.
- A LOW rating needs enough impressions behind it to mean anything. A headline that has barely served will often show as PENDING or LEARNING rather than a real verdict; the script reports what Google has rated, it cannot make Google rate faster.
The script
/**
* RSA Asset Auditor
* The SEM Dispatch Vault, semdispatch.com/vault
*
* Reads responsive search ad asset performance labels and lists every
* headline and description rated LOW, grouped by campaign and ad group.
* Read-only: this script never pauses ads or edits assets.
*/
var CONFIG = {
// Paste the URL of a Google Sheet the script can write to.
SHEET_URL: "PASTE_YOUR_SHEET_URL_HERE",
// Optional. An email address for the summary, e.g. "you@example.com".
// Leave empty to log and write the Sheet only.
EMAIL: "",
// true = only enabled ads are audited. false = also include paused ads.
ENABLED_ADS_ONLY: true
};
function main() {
var account = AdsApp.currentAccount();
var query =
"SELECT ad_group_ad_asset_view.field_type, " +
"ad_group_ad_asset_view.performance_label, " +
"ad_group_ad_asset_view.enabled, " +
"asset.text_asset.text, " +
"ad_group_ad.ad.id, " +
"ad_group.name, " +
"campaign.name " +
"FROM ad_group_ad_asset_view " +
"WHERE ad_group_ad_asset_view.field_type IN ('HEADLINE','DESCRIPTION') " +
(CONFIG.ENABLED_ADS_ONLY ? "AND ad_group_ad.status = 'ENABLED' " : "") +
"AND campaign.advertising_channel_type = 'SEARCH'";
var rows = AdsApp.report(query).rows();
var byAd = {};
var lowCount = 0;
while (rows.hasNext()) {
var row = rows.next();
var label = row["ad_group_ad_asset_view.performance_label"];
if (label !== "LOW") continue;
lowCount += 1;
var adId = row["ad_group_ad.ad.id"];
var campaign = row["campaign.name"];
var adGroup = row["ad_group.name"];
var fieldType = row["ad_group_ad_asset_view.field_type"];
var text = row["asset.text_asset.text"];
var key = campaign + " | " + adGroup + " | " + adId;
if (!byAd[key]) {
byAd[key] = {
campaign: campaign,
adGroup: adGroup,
adId: adId,
lowHeadlines: [],
lowDescriptions: []
};
}
if (fieldType === "HEADLINE") {
byAd[key].lowHeadlines.push(text);
} else if (fieldType === "DESCRIPTION") {
byAd[key].lowDescriptions.push(text);
}
}
var ads = [];
for (var k in byAd) {
var record = byAd[k];
record.lowTotal = record.lowHeadlines.length + record.lowDescriptions.length;
ads.push(record);
}
ads.sort(function (a, b) {
if (a.campaign !== b.campaign) return a.campaign < b.campaign ? -1 : 1;
if (a.adGroup !== b.adGroup) return a.adGroup < b.adGroup ? -1 : 1;
return b.lowTotal - a.lowTotal;
});
Logger.log("RSA Asset Auditor, " + account.getName());
Logger.log("LOW-rated assets found: " + lowCount + " across " + ads.length + " ads");
ads.forEach(function (record) {
Logger.log(
record.campaign + " / " + record.adGroup + " / ad " + record.adId + ": " +
record.lowHeadlines.length + " LOW headline(s), " +
record.lowDescriptions.length + " LOW description(s)"
);
});
if (CONFIG.SHEET_URL && CONFIG.SHEET_URL.indexOf("http") === 0) {
var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL).getSheets()[0];
sheet.clearContents();
sheet.appendRow([
"Campaign", "Ad group", "Ad ID", "LOW headlines", "LOW descriptions",
"LOW headline text", "LOW description text"
]);
ads.forEach(function (record) {
sheet.appendRow([
record.campaign, record.adGroup, record.adId,
record.lowHeadlines.length, record.lowDescriptions.length,
record.lowHeadlines.join(" / "), record.lowDescriptions.join(" / ")
]);
});
}
if (CONFIG.EMAIL !== "" && ads.length > 0) {
var worst = ads.slice().sort(function (a, b) { return b.lowTotal - a.lowTotal; }).slice(0, 10);
var lines = ["RSA Asset Auditor, " + account.getName(), "", "Ads with the most LOW assets:"];
worst.forEach(function (record) {
lines.push(
record.campaign + " / " + record.adGroup + " (ad " + record.adId + "): " +
record.lowTotal + " LOW asset(s)"
);
});
MailApp.sendEmail(
CONFIG.EMAIL,
"[RSA audit] " + account.getName() + ": " + lowCount + " LOW-rated assets",
lines.join("\n")
);
}
}
Set it up
- In Google Ads, open Tools, then Bulk actions, then Scripts.
- Create a new script, delete the placeholder, and paste the code above.
- Set
SHEET_URLto a Sheet you own. LeaveENABLED_ADS_ONLYas true unless you specifically want paused ads included too. - Authorise the script, then use Preview to check the log output before scheduling.
- Schedule it weekly. Performance labels settle over days, not hours, so a daily run mostly repeats the same list.
How to read the output
The Sheet groups every LOW-rated headline and description by campaign, ad group and ad, with a count of each so you can see at a glance which ads are carrying the most dead weight. An ad with two or three LOW headlines out of the fifteen Google allows is worth a rewrite; an ad with most of its headlines rated LOW is worth rebuilding from scratch. The text columns give you the exact wording that is underperforming, so the rewrite starts from what is actually failing rather than a guess.
One honest limitation: a LOW label needs meaningful serving history to be trustworthy, so a newly launched ad group will show mostly PENDING or LEARNING and the audit will look emptier than it will in a month. Re-run it on a schedule rather than reading a single pass as the final word, and remember it flags wording, not strategy; an ad can carry strong individual assets and still underperform for reasons this script cannot see.