Keyword Labeler - Single Account

Tools icon

Making large-scale changes to your keywords, such as disabling ones that are underperforming, can be tedious. The Keyword Labeler script simplifies this task by automatically applying labels to keywords based on rules that you define. Once the labels are applied, you can easily filter the keywords in the Google Ads UI and apply your desired changes, or you can make changes to the keywords using another script.

Here are some examples of how this script can be used:

  • Label keywords that have recently underperformed. You can then filter for these keywords in the Google Ads UI and modify their bids, or pause/remove them.
  • Label keywords that contain a proper name associated with your brand such as the name of one of your products. You can later use this label to segment the keyword view to better understand your performance.
  • Label keywords that you intend to change bids on. You can then review the labels in the Google Ads UI before actually making the changes using Bulk edits.
  • Label all keywords with their quality score. Later, label keywords whose current quality score does not match its previous quality score (as reflected in the original label) so that you can review them.
  • Any combination of the above and more.

The script sends an email linking to a spreadsheet when new keywords have been labeled.

How it works

The script lets you define rules using a simple format with these fields:

  • conditions: An optional list of KeywordSelector conditions. These conditions can, for example, select keywords based on their performance statistics.
  • dateRange: An optional KeywordSelector date range to use when selecting based on statistics fields. If omitted, a default date range in the script is used.
  • filter: An optional function to apply to each keyword after it is retrieved to further define whether keywords match the rule. The filter can be used to express more complex logic on certain keyword fields that cannot be expressed in conditions.
  • labelName: A required label name to apply to any keywords that match the rule and do not already have that label.

Here is an example rule for identifying and labeling underperforming keywords associated with your brand:

{
  conditions: [
    'Ctr < 0.02',
    'AverageCpc > 1',
  ],
  dateRange: 'LAST_7_DAYS',
  filter: function(keyword) {
    const brands = ['Product A', 'Product B', 'Product C'];
    const text = keyword.getText();
    for (const i = 0; i < brands.length; i++) {
      if (text.indexOf(brand[i]) >= 0) {
        return true;
      }
    }
    return false;
  },
  labelName: 'Underperforming Branded'
}

This rule labels all keywords with a CTR below 2% and an average CPC above $1 over the past 7 days if the keyword text contains at least one of three product brand names.

Typically, you will only be interested in labeling keywords that are enabled and that reside in an ad group and campaign that are themselves enabled. Rather than specify these conditions in each rule, the script lets you provide global conditions that are used by every rule:

GLOBAL_CONDITIONS: [
  'campaign.status = ENABLED',
  'ad_group.status = ENABLED',
  'ad_group_criterion.status = ENABLED'
]

The core of the script is a function that takes a rule, builds a KeywordSelector using the global and rule-specific conditions, and applies the filter to each resulting keyword. See the getKeywordsForRule function in the script below for the full implementation.

The script sends an email linking to a spreadsheet listing all of the keywords it labeled. You can later sign in to the Google Ads UI to see if you want to modify those keywords, such as pausing them, removing them, or modifying their bids.

Scheduling

Whether and how often you schedule the script to run depends on your labeling goals. If, for example, you are labeling underperforming keywords, you might want to schedule the script to run weekly or even daily so that you can quickly identify and remedy any underperforming keywords. On the other hand, if you are using the script to facilitate one-off efforts that involve labeling keywords, there is no need to schedule the script to run regularly.

Setup

  • Create a new Google Ads script with the source code below. Use a copy of this template spreadsheet.
  • Update the RULES array to express your labeling rules. The source code below shows an example for labeling underperforming keywords associated with your brand as well as underperforming generic keywords.
  • Update SPREADSHEET_URL and RECIPIENT_EMAILS in your script.

Source code

// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Keyword Labeler
 *
 * @overview The Keyword Labeler script labels keywords based on rules that
 *     you define. For example, you can create a rule to label keywords that
 *     are underperforming. Later, you can filter for this label in Google Ads
 *     to decide whether to pause or remove those keywords. Rules don't have
 *     to be based solely on a keyword's performance. They can also be based
 *     on properties of a keyword such as its text or match type. For example,
 *     you could define "branded" keywords as those containing proper nouns
 *     associated with your brand, then label those keywords based on
 *     different performance thresholds versus "non-branded" keywords.
 *     Finally, the script sends an email linking to a spreadsheet when new
 *     keywords have been labeled. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/labels
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.1.2
 *   - Added validation for external spreadsheet setup.
 * - version 1.1.1
 *   - Improvements to time zone handling.
 * - version 1.1
 *   - Modified to allow generic rules and labeling.
 * - version 1.0
 *   - Released initial version.
 */

const CONFIG = {
  // URL of the spreadsheet template.
  // This should be a copy of https://goo.gl/opviBb.
  SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL',

  // Array of addresses to be alerted via email if labels are applied.
  RECIPIENT_EMAILS: [
    'YOUR_EMAIL_HERE'
  ],

  // Selector conditions to apply for all rules.
  GLOBAL_CONDITIONS: [
    'campaign.status = ENABLED',
    'ad_group.status = ENABLED',
    'ad_group_criterion.status = ENABLED'
  ],

  // Default date range over which statistics fields are retrieved.
  // Used when fetching keywords if a rule doesn't specify a date range.
  DEFAULT_DATE_RANGE: 'LAST_7_DAYS'
};

/**
 * Defines the rules by which keywords will be labeled.
 * The labelName field is required. Other fields may be null.
 * @type {Array.<{
 *     conditions: Array.<string>,
 *     dateRange: string,
 *     filter: function(Object): boolean,
 *     labelName: string,
 *   }>
 * }
 */
const RULES = [
  {
    conditions: [
      'metrics.ctr < 0.02',
      'metrics.average_cpc > 1',
    ],
    filter: function(keyword) {
      const brands = ['Product A', 'Product B', 'Product C'];
      const text = keyword.getText();
      for (const brand of brands) {
        if(text.indexOf(brand) >= 0) {
          return true;
        }
      }
      return false;
    },
    labelName: 'Underperforming Branded'
  },

  {
    conditions: [
      'metrics.ctr < 0.01',
      'metrics.average_cpc > 2',
    ],
    labelName: 'Underperforming'
  }
];

function main() {
  validateEmailAddresses();
  const results = processAccount();
  processResults(results);
}

/**
 * Processes the rules on the current account.
 *
 * @return {Array.<Object>} An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function processAccount() {
  ensureAccountLabels();
  const changes = applyLabels();

  return changes;
}

/**
 * Processes the results of the script.
 *
 * @param {Array.<Object>} changes An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function processResults(changes) {
  if (changes.length > 0) {
    const spreadsheetUrl = saveToSpreadsheet(changes, CONFIG.RECIPIENT_EMAILS);
    sendEmail(spreadsheetUrl, CONFIG.RECIPIENT_EMAILS);
  } else {
    console.log('No labels were applied.');
  }
}

/**
 * Retrieves the names of all labels in the account.
 *
 * @return {Array.<string>} An array of label names.
 */
function getAccountLabelNames() {
  const labelNames = [];

  for (const label of AdsApp.labels()) {
    labelNames.push(label.getName());
  }

  return labelNames;
}

/**
 * Checks that the account has a label for each rule and
 * creates the rule's label if it does not already exist.
 * Throws an exception if a rule does not have a labelName.
 */
function ensureAccountLabels() {
  const labelNames = getAccountLabelNames();

  for (var i = 0; i < RULES.length; i++) {
    const labelName = RULES[i].labelName;

    if (!labelName) {
      throw `Missing labelName for rule #${i}`;
    }

    if (labelNames.indexOf(labelName) == -1) {
      AdsApp.createLabel(labelName);
      labelNames.push(labelName);
    }
  }
}

/**
 * Retrieves the keywords in an account satisfying a rule
 * and that do not already have the rule's label.
 *
 * @param {Object} rule An element of the RULES array.
 * @return {Array.<Object>} An array of keywords.
 */
function getKeywordsForRule(rule) {
  let selector = AdsApp.keywords();
  let includedLabel;

  // Add global conditions.
  for (const globalCondition of CONFIG.GLOBAL_CONDITIONS) {
    selector = selector.withCondition(globalCondition);
  }

  // Add selector conditions for this rule.
  if (rule.conditions) {
    for (const ruleCondition of rule.conditions) {
      selector = selector.withCondition(ruleCondition);
    }
  }

  if(rule.labelName){
    includedLabel = AdsApp.labels()
        .withCondition(`label.name = '${rule.labelName}'`)
        .get();
    if(includedLabel.hasNext()){
      includedLabel=includedLabel.next();
    }
  }

  // Exclude keywords that already have the label.
  selector.withCondition(`LabelNames CONTAINS_NONE ["${rule.labelName}"]`);

  // Add a date range.
  selector = selector.forDateRange(rule.dateRange || CONFIG.DEFAULT_DATE_RANGE);

  // Get the keywords.
  const iterator = selector.get();
  const keywords = [];

  // Check filter conditions for this rule.
  for (const keyword of iterator) {
    if (!rule.filter || rule.filter(keyword)) {
      keywords.push(keyword);
    }
  }

  return keywords;
}

/**
 * For each rule, determines the keywords matching the rule and which
 * need to have a label newly applied, and applies it.
 *
 * @return {Array.<Object>} An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function applyLabels() {
  const changes = [];
  const customerId = AdsApp.currentAccount().getCustomerId();

  for (const rule of RULES) {
    const keywords = getKeywordsForRule(rule);
    const labelName = rule.labelName;

    for (const keyword of keywords) {

      keyword.applyLabel(labelName);

      changes.push({
        customerId: customerId,
        campaignName: keyword.getCampaign().getName(),
        adGroupName: keyword.getAdGroup().getName(),
        labelName: labelName,
        keywordText: keyword.getText(),
      });
    }
  }

  return changes;
}

/**
 * Outputs a list of applied labels to a new spreadsheet and gives editor access
 * to a list of provided emails.
 *
 * @param {Array.<Object>} changes An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 * @param {Array.<Object>} emails An array of email addresses.
 * @return {string} The URL of the spreadsheet.
 */
function saveToSpreadsheet(changes, emails) {
  const template = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  const spreadsheet = template.copy('Keyword Labels Applied');

  // Make sure the spreadsheet is using the account's timezone.
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());

  console.log(`Saving changes to spreadsheet at ${spreadsheet.getUrl()}`);

  const headers = spreadsheet.getRangeByName('Headers');
  const outputRange = headers.offset(1, 0, changes.length);

  const outputValues = [];
  for (const change of changes) {
    outputValues.push([
      change.customerId,
      change.campaignName,
      change.adGroupName,
      change.keywordText,
      change.labelName
    ]);
  }
  outputRange.setValues(outputValues);

  spreadsheet.getRangeByName('RunDate').setValue(new Date());

  for (const email of emails) {
    spreadsheet.addEditor(email);
  }

  return spreadsheet.getUrl();
}

/**
 * Sends an email to a list of email addresses with a link to a spreadsheet.
 *
 * @param {string} spreadsheetUrl The URL of the spreadsheet.
 * @param {Array.<Object>} emails An array of email addresses.
 */
function sendEmail(spreadsheetUrl, emails) {
  MailApp.sendEmail(emails.join(','), 'Keywords Newly Labeled',
      `Keywords have been newly labeled in your` +
      `Google Ads account(s). See ` +
      `${spreadsheetUrl} for details.`);
}

/**
 * DO NOT EDIT ANYTHING BELOW THIS LINE.
 * Please modify your spreadsheet URL and email addresses at the top of the file
 * only.
 */

/**
 * Validates the provided spreadsheet URL and email address
 * to make sure that they're set up properly. Throws a descriptive error message
 * if validation fails.
 *
 * @param {string} spreadsheeturl The URL of the spreadsheet to open.
 * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
 * @throws {Error} If the spreadsheet URL or email hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
    throw new Error('Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  return SpreadsheetApp.openByUrl(spreadsheeturl);
}

/**
 * Validates the provided email address to make sure it's not the default.
 * Throws a descriptive error message if validation fails.
 *
 * @throws {Error} If the list of email addresses is still the default
 */
function validateEmailAddresses() {
  if (CONFIG.RECIPIENT_EMAILS &&
      CONFIG.RECIPIENT_EMAILS[0] == 'YOUR_EMAIL_HERE') {
    throw new Error('Please specify a valid email address.');
  }
}