Tuesday, November 29, 2016

Here’s an AdWords script to fill empty ad groups

full adwords ad groups

As you’ve probably noticed, Christmas is coming. It’s a busy time of year for many of us. But busy means both that it’s easy to make mistakes and that it’s important not to let them slide.

That’s why it’s important to automate what you can: when it gets busy, mistakes can be caught — and even corrected — without taking up your valuable time.

So you want to make sure you didn’t pause your Black Friday ads with no active ads to show in their place. You want to be sure you don’t have an ad group where all the ads are disapproved. And you want to be sure that you did create ads in those brand-new ad groups filled with new, seasonal keywords for new, seasonal products.

I’ve already shared a script to find ad groups with no ads. But with this newest script from Brainlabs (my employer), you can go one better: you can automatically create an ad, filling the empty ad group and making sure you don’t pass up valuable traffic.

Sure, it’s only a generic ad — and we all know it’s important to get ads as specific and relevant as possible. But the new ads are all labeled, so you can easily find them and rewrite them. And in the meantime, you’re getting clicks you’d otherwise have been missing out on.

What’s more, Google is eventually going to get rid of standard text ads. We don’t know when this is going to happen (You won’t be able to create new standard text ads after January the 31, but existing ones will still serve traffic), but it always pays to plan ahead. So you can also use this script to put an expanded text ad into all ad groups that don’t already contain an ETA.

To use the script for yourself, open a new Script in AdWords and copy in the code below. Then change the settings at the top:

  • campaignNameContains and campaignNameDoesNotContain filter which campaigns the script will look at. For example, if campaignNameContains is ["Brand", "Generic"], then only campaigns with names containing “brand” or “generic” are included. If campaignNameDoesNotContain is ["Display", "Competitor"], then any campaigns with names containing “display” or “competitor” are ignored.
    • This is not case-sensitive.
    • Leave blank, [], to include all campaigns.
    • If you need to put a double quote into campaignNameContains or campaignNameDoesNotContain, put a backslash before it.
  • ignorePausedCampaigns should be set to true if you only want to look at currently active campaigns, or false if you want to include paused campaigns as well.
  • ignorePausedAdGroups should be true if you only want to look at currently active ad groups, or false to look at active and paused ad groups.
  • checkedLabelName is the label to show which campaigns and ad groups the script knows aren’t empty (as either they were already full or they’ve just had a new ETA added). This means you can rerun the script as much as you need if your account is so big the script timed out — anything with this label will be ignored in future runs.
    • You could also manually label campaigns and ad groups with this label, to prevent the script from checking them.
  • newAdLabelName is the label given to the expanded text ads that the script creates.
  • If onlyLookForETAs is true, then the script will create an expanded text ad in any ad group that doesn’t contain one. If it’s false, the script will check if there is already an ad of any type — like a standard text ad or a banner ad — and only create an ETA if there are no ads of any type already there.
  • There are six variables to make the new ETA from:
    • headlinePart1 and headlinePart2 are the headlines (max of 30 characters).
    • description is the description (max of 60 characters).
    • finalUrl is the landing page. If this doesn’t start with “http://” or “https://” the script will add “http://” for you.
    • urlPath1 and urlPath2 are the paths that show in the display URL. They are optional, so leave as "" if you don’t want them (max of 15 characters).

AdWords Scripts can only run for 30 minutes at a time, so if you’ve got a big account, you may not be able to cover all campaigns in one go. That’s why the script labels the ad groups and campaigns once they’re processed: if the script times out, then you can just run it again and again, and it will always look only at ad groups it hasn’t checked before.

The script will check the ad text you’ve given, to make sure you’re within the character limits and you don’t have too many exclamation marks, but AdWords may still refuse to make your ad because of policy violations. This won’t be flagged in preview runs; once you’ve run the script, you need to check the logs for messages about ads not being created. Note that ad groups where ads couldn’t be created won’t be labeled, because they’re still empty — so if there are problems, you can just change the ad text and run the script again.

If you don’t want to use the same template ad everywhere, you could run the script multiple times: use the campaignNameContains to look at different campaigns each time, and change the template ETA each time to suit those campaigns.

/**
*
* Empty Ad Group Filler
*
* Checks for ad groups with no approved and active ads (or no approved and active
* ETAs) and creates a template ad in them.
*
* Version: 1.1
* Updated 2017-01-05: changed 'CreativeApprovalStatus' to 'CombinedApprovalStatus'
* Google AdWords Script maintained on brainlabsdigital.com
*
**/
//////////////////////////////////////////////////////////////////////////////
// Options
var campaignNameDoesNotContain = [];
// Use this if you want to exclude some campaigns.
// For example ["Display"] would ignore any campaigns with 'Display' in the name,
// while ["Display","Shopping"] would ignore any campaigns with 'Display' or
// 'Shopping' in the name.
// Leave as [] to not exclude any campaigns.
var campaignNameContains = [];
// Use this if you only want to look at some campaigns.
// For example ["Brand"] would only look at campaigns with 'Brand' in the name,
// while ["Brand","Generic"] would only look at campaigns with 'Brand' or 'Generic'
// in the name.
// Leave as [] to include all campaigns.
var ignorePausedCampaigns = true;
// Set this to true to only look at currently active campaigns.
// Set to false to also include campaigns that are currently paused.
var ignorePausedAdGroups = true;
// Set this to true to only look at currently active ad groups.
// Set to false to also include ad groups that are currently paused.
var checkedLabelName = "Checked for empty ad groups";
// Ad groups and campaigns that have been checked (and had ads added where
// necessary) will be labelled with this.
var newAdLabelName = "New ad to fill empty ad group";
// The ads this script creates will be labelled with this, so you can find
// them easily.
var onlyLookForETAs = false;
// If this is true, the script will create ads in ad groups with no expanded
// text ads (ignoring any standard text ads or other types of ad).
// If false, ads will be created in ad groups with no ads whatsoever.
var headlinePart1 = "Headline 1";
var headlinePart2 = "Headline 2";
var description = "Description";
var finalUrl = "www.example.com/Your-Landing-Page";
var urlPath1 = "Path Text 1";
var urlPath2 = "Path Text 2";
// The text for your template ad
//////////////////////////////////////////////////////////////////////////////
function main() {
// Check the template ad text for any issues
checkAdText();
// This is used to filter out campaigns and ad groups that have been checked in
// previous runs. The function will also create the label if it doesn't already
// exist, so we can apply it to entities we've checked or added ads to.
var checkedLabelId = getOrCreateLabelId(checkedLabelName);
// We don't need the ID, but this function will make sure the label exists
// so we can apply it later
var newAdLabelId = getOrCreateLabelId(newAdLabelName);
// Get the campaigns that have yet to be labelled with checkedLabelName
var campaignIds = getCampaignIdsWithoutLabel(checkedLabelId);
// Check batches of 100 campaigns at a time
for (var i=0; i<campaignIds.length; i += 100) {
var campaignBatch = campaignIds.slice(i, i+100);
var failedCampaignsInBatch = [];
var adGroupIds = getAdGroupIdsWithoutLabel(campaignBatch, checkedLabelId);
// Check batches of 1000 ad groups at a time
for (var j=0; j<adGroupIds.length; j += 1000) {
var adGroupBatch = adGroupIds.slice(j, j+1000);
var adGroupsWithAds = getAdGroupsWithAds(adGroupBatch);
if (adGroupBatch.length - adGroupsWithAds.length != 0) {
var failedIds = createTemplateAds(adGroupBatch, adGroupsWithAds, newAdLabelName);
} else {
var failedIds = {failedGroups:[], failedCampaigns:[]};
}
// Label the ad groups, except those where ads couldn't be created
applyLabel(checkedLabelName, "adGroups", adGroupBatch, failedIds["failedGroups"]);
Logger.log(adGroupsWithAds.length + " groups already had ads; " +
(adGroupBatch.length - adGroupsWithAds.length - failedIds["failedGroups"].length) + " ads created");
if (failedIds["failedGroups"].length > 0) {
Logger.log(failedIds["failedGroups"].length + " ads could not be created.");
}
// Record the campaign IDs of any ad groups where ad creation failed
for (var c in failedIds["failedCampaigns"]) {
if (failedCampaignsInBatch.indexOf(failedIds["failedCampaigns"][c]) < 0) {
failedCampaignsInBatch.push(failedIds["failedCampaigns"][c]);
}
}
}
// Label the campaigns where all ad groups were processed successfully
applyLabel(checkedLabelName, "campaigns", campaignBatch, failedCampaignsInBatch);
Logger.log( (campaignBatch.length - failedCampaignsInBatch.length) + " campaigns checked successfully.");
}
Logger.log("Account finished.");
}
// Checks the ad text to make sure no required fields are blank,
// no maximum lengths are exceeded
// and there are not too many exclamation marks.
// Also adds 'http://' to the start of the finalUrl if http or https is missing
function checkAdText() {
var components = {};
components["headlinePart1"] = headlinePart1.trim();
components["headlinePart2"] = headlinePart2.trim();
components["description"] = description.trim();
components["finalUrl"] = finalUrl.trim();
for (var name in components) {
if (components[name].length == 0) {
throw(name + " is blank.");
}
}
components["urlPath1"] = urlPath1.trim();
components["urlPath2"] = urlPath2.trim();
var maxLengths = {}
maxLengths["headlinePart1"] = 30;
maxLengths["headlinePart2"] = 30;
maxLengths["description"] = 60;
maxLengths["urlPath1"] = 15;
maxLengths["urlPath2"] = 15;
for (var name in maxLengths) {
if (components[name].length > maxLengths[name]) {
throw(name + " is " + components[name].length + " characters long - the maximum length is " + maxLengths[name]);
}
}
var exclamationMarkCount = {};
for (var name in components) {
exclamationMarkCount[name] = components[name].split("!").length - 1;
}
if (exclamationMarkCount["headlinePart1"] > 0 || exclamationMarkCount["headlinePart2"] > 0) {
throw("No exclamation marks are allowed in either headline part.");
}
if (exclamationMarkCount["description"] > 1) {
throw("description has " + exclamationMarkCount["description"] + " exclamation marks. Only 1 is allowed.");
}
if (exclamationMarkCount["urlPath1"] + exclamationMarkCount["urlPath2"] > 1) {
throw("The URL paths have " + exclamationMarkCount["description"] + " exclamation marks. Only 1 is allowed.");
}
if (finalUrl.substr(0,7) != "http://" && finalUrl.substr(0,8) != "https://") {
Logger.log("finalUrl does not start with http:// or https:// - adding http:// to the start");
finalUrl = "http://" + finalUrl;
}
}
// Create the label if it doesn't exist, and return its ID.
// (Returns a dummy ID if the label does not exist and this is a preview run,
// because we can't create or apply the label)
function getOrCreateLabelId(labelName) {
var labels = AdWordsApp.labels().withCondition("Name = '" + labelName + "'").get();
if (!labels.hasNext()) {
AdWordsApp.createLabel(labelName);
labels = AdWordsApp.labels().withCondition("Name = '" + labelName + "'").get();
}
if (AdWordsApp.getExecutionInfo().isPreview() && !labels.hasNext()) {
var labelId = 0;
} else {
var labelId = labels.next().getId();
}
return labelId;
}
// Get the IDs of campaigns which match the given options and do not contain the given label
function getCampaignIdsWithoutLabel(labelId) {
var whereStatement = "WHERE ";
var whereStatementsArray = [];
var campaignIds = [];
if (ignorePausedCampaigns) {
whereStatement += "CampaignStatus = ENABLED ";
} else {
whereStatement += "CampaignStatus IN ['ENABLED','PAUSED'] ";
}
for (var i=0; i<campaignNameDoesNotContain.length; i++) {
whereStatement += "AND CampaignName DOES_NOT_CONTAIN_IGNORE_CASE '" + campaignNameDoesNotContain[i].replace(/"/g,'\\\"') + "' ";
}
if (campaignNameContains.length == 0) {
whereStatementsArray = [whereStatement];
} else {
for (var i=0; i<campaignNameContains.length; i++) {
whereStatementsArray.push(whereStatement + 'AND CampaignName CONTAINS_IGNORE_CASE "' + campaignNameContains[i].replace(/"/g,'\\\"') + '" ');
}
}
for (var i=0; i<whereStatementsArray.length; i++) {
var campaignReport = AdWordsApp.report(
"SELECT CampaignId " +
"FROM CAMPAIGN_PERFORMANCE_REPORT " +
whereStatementsArray[i] +
"AND Labels CONTAINS_NONE [" + labelId + "] " +
"DURING LAST_30_DAYS");
var rows = campaignReport.rows();
while (rows.hasNext()) {
var row = rows.next();
campaignIds.push(row['CampaignId']);
}
}
if (campaignIds.length == 0) {
throw("No campaigns found with the given settings. Either the settings are too restrictive or the script has already checked and labelled all campaigns.");
}
Logger.log(campaignIds.length + " campaigns found");
return campaignIds;
}
// Get the IDs of ad groups in the given campaigns which do not use the given label
function getAdGroupIdsWithoutLabel(campaignIds, labelId) {
if (ignorePausedAdGroups) {
var whereStatement = "AdGroupStatus = ENABLED ";
} else {
var whereStatement = "AdGroupStatus IN ['ENABLED','PAUSED'] ";
}
var adGroupIds = [];
var report = AdWordsApp.report(
"SELECT AdGroupId " +
"FROM ADGROUP_PERFORMANCE_REPORT " +
"WHERE CampaignId IN [" + campaignIds.join(",") + "] AND " +
whereStatement +
"AND Labels CONTAINS_NONE [" + labelId + "] " +
"DURING LAST_30_DAYS");
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
adGroupIds.push(row['AdGroupId']);
}
Logger.log(adGroupIds.length + " ad groups found in " + campaignIds.length + " campaigns");
return adGroupIds;
}
// Finds the ad groups (in adGroupIds) that already have active, approved ads
function getAdGroupsWithAds(adGroupIds) {
var adGroupsWithAds = {};
if (onlyLookForETAs) {
var typeStatement = "AND AdType = EXPANDED_TEXT_AD ";
} else {
var typeStatement = "";
}
var adReport = AdWordsApp.report(
"SELECT AdGroupId " +
"FROM AD_PERFORMANCE_REPORT " +
"WHERE Status = ENABLED AND CombinedApprovalStatus != DISAPPROVED " +
"AND AdGroupId IN [" + adGroupIds.join(",") + "] " +
typeStatement +
"DURING LAST_7_DAYS");
var rows = adReport.rows();
while (rows.hasNext()) {
var row = rows.next();
adGroupsWithAds[row["AdGroupId"]] = true;
}
return Object.keys(adGroupsWithAds);
}
// Create the template ad in the ad groups that are listed in adGroupIds
// but not in adGroupsWithAds, then label with newAdLabelName
// Returns arrays of ad group IDs and campaign IDs where ads could not be
// made.
function createTemplateAds(adGroupIds, adGroupsWithAds, newAdLabelName) {
var selector = AdWordsApp.adGroups()
.withIds(adGroupIds);
if (adGroupsWithAds.length > 0) {
selector = selector.withCondition("AdGroupId NOT_IN [" + adGroupsWithAds.join(",") + "]");
}
var adGroupIterator = selector.get();
var count = 0;
var failedGroups = [];
var failedCampaigns = [];
while (adGroupIterator.hasNext()) {
var adGroup = adGroupIterator.next();
var adBuilder = adGroup.newAd().expandedTextAdBuilder()
.withHeadlinePart1(headlinePart1)
.withHeadlinePart2(headlinePart2)
.withDescription(description)
.withFinalUrl(finalUrl);
if (urlPath1 != "") {
adBuilder = adBuilder.withPath1(urlPath1);
}
if (urlPath2 != "") {
adBuilder = adBuilder.withPath2(urlPath2);
}
var adOperation = adBuilder.build();
if (adOperation.isSuccessful()) {
adOperation.getResult().applyLabel(newAdLabelName);
count++;
} else {
Logger.log("Error creating ad in ad group " + adGroup.getName() + ", in campaign " + adGroup.getCampaign().getName() + " : " + adOperation.getErrors());
failedGroups.push(adGroup.getId());
failedCampaigns.push(adGroup.getCampaign().getId());
}
}
return {failedGroups:failedGroups, failedCampaigns:failedCampaigns};
}
// Applies a label to entities of the given type
function applyLabel(labelName, entityType, entityIdsToInclude, entityIdsToAvoid) {
var selector = AdWordsApp[entityType]()
.withIds(entityIdsToInclude);
if (entityIdsToAvoid.length > 0) {
selector = selector.withCondition(entityType.substr(0,1).toUpperCase() + entityType.substr(1,entityType.length-2) + "Id NOT_IN [" + entityIdsToAvoid.join(",") + "]");
}
var iterator = selector.get()
while (iterator.hasNext()) {
var entity = iterator.next();
entity.applyLabel(labelName);
}
}

The post Here’s an AdWords script to fill empty ad groups appeared first on Search Engine Land.

No comments:

Post a Comment