Audience Assistant

The Audience Assistant script for Google Ads accounts can run in either manager or single accounts.

As a Google Ads account evolves, new campaigns are continuously added. Keeping a Google Ads campaign in sync with an audience setup is a repetitive task for many advertisers.

The Audience Assistant solution is a Google Ads script to add your favorite Remarketing Lists and Similar Audiences within an account to the campaigns missing them. This solution does have the following caveats:

  • Only RLSA and Similar Audiences are supported at the moment.
  • Audience setups at the campaign level can be processed, and the script has an execution time limit. This limits the rate at which audiences can be checked per execution.

Tip: Run your script several times to process all campaigns in large accounts. The number of campaigns to be processed declines with every execution.

Configuration options

The script's main options can be set in the spreadsheet.

  • Scope: Select whether the script will add audiences in observation mode (recommended) or targeting mode and which accounts will be considered. Most users should include all their search & shopping accounts.
  • You can exclude specific campaigns if you want. In this case you need to specify a label in cell G2. If the label doesn´t exist in one of the accounts from the sheet, it will be created, but never applied to any campaign.

Scheduling

Each time the script runs, it automatically detects whether it should resume a cycle across all accounts already in progress, or start a new one. As a result, regardless of how often you want to launch a fresh cycle, schedule the script Hourly.

How it works

The script uses Google Ads Reports to build up a list of all enabled campaigns and a list of campaigns with their Audiences. As Manager Account scripts can only process 50 accounts in parallel, the script uses the Large Manager Hierarchy Template across several executions until all accounts have been inspected.

Configuration

Configuration consists of the following steps:

  1. Set up a spreadsheet-based script with the source code below. Make a copy of this template spreadsheet .
  2. Insert the account IDs (e.g., 123-456-7890) in column A — one per row and no ID more than once. Don't remove the hypens (-) in the account IDs.
  3. Insert your relevant Audience IDs, comma-separated without spaces in column B. You find those IDs in the Audience Manager if you click on a Remarketing List or Similar Audience.
  4. Create a new script and remove sample code in it.
    1. Sign in to your Google Ads account
    2. Click the Tools icon and select Scripts under BULK ACTIONS
    3. Press the + icon to add a new script
    4. Name it, remove existing code and paste the Audience Assistant code.
    5. When prompted, click AUTHORIZE so the script can access the account on your behalf. This has to be done once for each script.
  5. Don't forget to update INSERT_SPREADSHEET_URL_HERE (line 39) and INSERT_UNIQUE_FILENAME_HERE (line 45) in the source code.
  6. Click PREVIEW to run the script in preview mode: Results will appear in the CHANGES / LOGS panel.

Source code

// Copyright 2016, 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 Audience Assistant
 *
 * @fileoverview The Audience Assistant will read a set of accounts and audience
 * lists from an external spreadsheet and ensures that the lists are opted in
 * for all enabled campaigns. The script will also set the targeting to either
 * 'target & bid: Targeting' or 'bid only: Observation'
 *
 * It will never remove any audience list from campaigns.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.2
 *
 * @changelog
 * - version 1.2
 *   - Removed display and video campaigns due to quite different setup criteria
 * - version 1.1
 *   - Added display and video campaigns
 *   - users can exclude specific campaigns with a label
 * - version 1.0
 *   - Released initial version.
 */

var SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';

// The name of the file that will be created on Drive to store data
// between executions of the script. You must use a different
// filename for each each script running in the account, or data
// from different scripts may overwrite one another.
var UNIQUE_FILENAME = 'INSERT_UNIQUE_FILENAME_HERE';

// The possible statuses for the script as a whole or an individual account.
var STATUSES = {
  NOT_STARTED: 'Not Started',
  STARTED: 'Started',
  FAILED: 'Failed',
  COMPLETE: 'Complete'
};
// The configuration loaded from the spreadsheet <SPREADSHEET_URL>
var CONFIG = readInput();

// Possible campaign types
var CAMPAIGN_TYPE = {
  search: 'SEARCH',
  shopping: 'SHOPPING'
};

/**
 * Your main logic for initializing a cycle for your script.
 *
 * @param {Array.<string>!} customerIds The customerIds that this cycle
 *     will process.
 */
function initializeCycle(customerIds) {
  // This example prints the accounts that will be processed in
  // this cycle.
  Logger.log('Accounts to be processed this cycle:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * Your main logic for initializing a single execution of the script.
 *
 * @param {Array.<string>!} customerIds The customerIds that this
 *     execution will process.
 */
function initializeExecution(customerIds) {
  // This example prints the accounts that will be processed in
  // this execution.
  Logger.log('Accounts to be processed this execution:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * This function is executed for every account selected and selects the type of
 * campaigns that have to be processed.
 *
 * @return {Object!} information for callback
 */
function processAccount() {
  var numCampaigns = 0, numShoppingCampaigns = 0;
  var accountId = AdsApp.currentAccount().getCustomerId();
  if (CONFIG.accountAudienceListMap[accountId] == undefined) {
    return {numCampaigns: 0, numShoppingCampaigns: 0};
  }
  var accountContainsShopping =
      (AdsApp.shoppingCampaigns().get().totalNumEntities() > 0) ? true : false;
  var accountContainsSearch =
      (AdsApp.campaigns().get().totalNumEntities() > 0) ? true : false;

  if (accountContainsSearch) {
    numCampaigns = processCampaigns(CAMPAIGN_TYPE.search);
  }
  if (accountContainsShopping) {
    numShoppingCampaigns = processCampaigns(CAMPAIGN_TYPE.shopping);
  }

  return {
    numCampaigns: numCampaigns,
    numShoppingCampaigns: numShoppingCampaigns
  };
}

/**
 * This processes all the campaigns for a combination of audience lists and
 * campaigns. It will focus on a single type of campaign per call. The function
 * uses the report functionality to build an object that contains all targeted
 * campaigns per audience list id and generates a complement of these campaigns
 * and the complete list of enabled campaigns.
 * On this complement all the action is focussed
 *
 * @param {string} campaignType The campaign type currently in focus.
 * @return {Object!} The number of processed campaigns
 */
function processCampaigns(campaignType) {
  var accountId = AdsApp.currentAccount().getCustomerId();
  var audienceListIds = CONFIG.accountAudienceListMap[accountId];
  var touchedCampaigns = 0;
  var targetingString = CONFIG.bidOnly ? 'TARGET_ALL_TRUE' : 'TARGET_ALL_FALSE';

  // For each audienceList defined in the configuration we are interested in all
  // campaigns that have not yet been targeted in order to be added to the
  // audience list. Therefore, we are using a list of all enabled campaigns and
  // subtract all campaigns that are already targeted.
  var allEnabledCampaignIds = getAllEnabledCampaigns(campaignType);
  var audienceToCampaignMap =
      getAudienceListCampaignMap(audienceListIds, allEnabledCampaignIds);

  // For every audienceList from the configuration we are iterating over all
  // relevant campaigns to add the audienceList.
  for (var audienceList in audienceToCampaignMap) {
    var audienceOperations = [];
    var campaignIds = getRelevantCampaignIds(
        allEnabledCampaignIds, audienceToCampaignMap[audienceList]);
    if (campaignIds && campaignIds.length > 0) {
      var i, j, tmpCampaignIds, partitionSize = 10000;
      // In case of a large list of campaigns we need to split the list into
      // smaller parts not exceeding 10,000.
      for (i = 0, j = campaignIds.length; i < j; i += partitionSize) {
        tmpCampaignIds = campaignIds.slice(i, i + partitionSize);
        var campaignsSelector = getCampaignSelectorByCampaignType(campaignType);
        var campaignIterator = campaignsSelector.withIds(tmpCampaignIds)
                                   .withCondition('Status = ENABLED')
                                   .get();
        while (campaignIterator.hasNext()) {
          var campaign = campaignIterator.next();
          audienceOperations.push({
            audienceListIdString: audienceList.toString(),
            campaign: campaign,
            audienceListBuilder:
                campaign.targeting().newUserListBuilder().withAudienceId(
                    parseInt(audienceList)).build()
          });
        }
        // Process the list of audienceOperations.
        touchedCampaigns +=
              processAudienceOperations(audienceOperations, targetingString);
      }
    }
  }

  return touchedCampaigns;
}

/**
 * Processes the audience operations for an audience lists
 *
 * @param {Array.<Array.<string, string, Object!>!>!} audienceOperations
 * @param {string} targetingString
 * @return {Int!} the number of campaigns that received an additional audience
 */
function processAudienceOperations(audienceOperations, targetingString) {
  var touchedCampaigns = 0;
  // Object to remember campaigns that have already been set to the correct
  // targeting setting
  var campaignBiddingSet = {};
  var accountId = AdsApp.currentAccount().getCustomerId();

  // Iterate over all audience operations and set campaign targeting if
  // nescessary
  for (var i = 0; i < audienceOperations.length; i++) {
    var campaign = audienceOperations[i].campaign;
    var campaignName = campaign.getName();
    if (audienceOperations[i].audienceListBuilder.isSuccessful()) {
      if (!campaignBiddingSet[campaignName]) {
        campaign.targeting().setTargetingSetting(
            'USER_INTEREST_AND_LIST', targetingString);
        campaignBiddingSet[campaignName] = true;
      }
      touchedCampaigns++;
    } else {
      Logger.log(
          accountId + ' - Could not add audience \'%s\' to campaign %s',
          audienceOperations[i].audienceListIdString, campaignName);
    }
  }

  return touchedCampaigns;
}

/**
 * Get the campaign selector by campaign type
 *
 * @param {string} campaignType
 * @return {Object!} the correct campaign selector
 */
function getCampaignSelectorByCampaignType(campaignType) {
  if (campaignType == CAMPAIGN_TYPE.search) {
    return AdsApp.campaigns();
  } else if (campaignType == CAMPAIGN_TYPE.shopping) {
    return AdsApp.shoppingCampaigns();
  }
}

/**
 * Get a list of campaign ids that are not yet targeted by the audience list
 * currently worked on. In case the audience list is not added to
 * any campaign all enabled campaigns are returned. In any other case the
 * difference between all enabled campaigns and the already targeted campaigns
 * is returned.
 *
 * @param {Array.<Int!>!} allEnabledCampaignIds
 * @param {Array.<Int!>!} targetedCampaignIds
 * @return {Array.<Int!>!} list of campaign ids
 */
function getRelevantCampaignIds(allEnabledCampaignIds, targetedCampaignIds) {
  if (allEnabledCampaignIds === targetedCampaignIds) {
    return allEnabledCampaignIds;
  } else {
    return complement(allEnabledCampaignIds, targetedCampaignIds);
  }
}

/**
 * Get a map of targeted campaings for each audience list provided by the
 * configuration object
 *
 * @param {Array.<int!>!} audienceListIds List of all audience lists defined by
 *     the user configuration
 * @param {Array.<int!>!} allEnabledCampaignIds List of all enabled campaign ids
 * @return {Object.<Array.<int!>!>!} Map with audience list id as key and list
 *     of campaign ids as value
 */
function getAudienceListCampaignMap(audienceListIds, allEnabledCampaignIds) {
  var audienceToCampaignMap = {};
  var reportQuery =
      'SELECT Criteria, CampaignId FROM AUDIENCE_PERFORMANCE_REPORT WHERE ' +
      'Criteria IN [\'boomuserlist::' +
      audienceListIds.join('\',\'boomuserlist::') +
      '\'] AND CampaignStatus = ENABLED';
  var audienceReportRows = AdsApp.report(reportQuery).rows();
  while (audienceReportRows.hasNext()) {
    var row = audienceReportRows.next();
    var tmpAudienceList = row['Criteria'];
    var tmpAudienceListId = tmpAudienceList.replace('boomuserlist::', '');
    var campaignId = row['CampaignId'];
    if (!audienceToCampaignMap[tmpAudienceListId]) {
      audienceToCampaignMap[tmpAudienceListId] = [];
    }
    audienceToCampaignMap[tmpAudienceListId].push(parseInt(campaignId));
  }
  for (var i = 0; i < audienceListIds.length; i++) {
    if (audienceToCampaignMap[audienceListIds[i]] === undefined) {
      audienceToCampaignMap[audienceListIds[i]] = allEnabledCampaignIds;
    }
  }

  return audienceToCampaignMap;
}

/**
 * Get all enabled campaigns for specified campaign type for a given account
 *
 * @param {string} campaignType The campaign type currently in focus.
 * @return {Array.<Int!>!} The list of all enabled campaigns
 */
function getAllEnabledCampaigns(campaignType) {
  var allEnabledCampaignIds = [];
  if (CONFIG.excludedCampaignLabel != '') {
    var labelId = getLabelId();
    var reportRows =
        AdsApp
            .report(
                'SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT WHERE ' +
                'Labels CONTAINS_NONE [\'' + labelId + '\'] ' +
                'AND CampaignStatus = \'ENABLED\' ' +
                'AND AdvertisingChannelType = \'' +
                campaignType + '\'')
            .rows();
  } else {
    var reportRows =
        AdsApp
            .report(
                'SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT WHERE ' +
                'CampaignStatus = \'ENABLED\' ' +
                'AND AdvertisingChannelType = \'' +
                campaignType + '\'')
            .rows();
  }
  while (reportRows.hasNext()) {
    var row = reportRows.next();
    var campaignId = row['CampaignId'];
    allEnabledCampaignIds.push(parseInt(campaignId));
  }

  return allEnabledCampaignIds;
}

/**
 * Helper function to calculate the complement of two arrays.
 * Returns array a without array b
 *
 * @param{Array!} a first array
 * @param{Array!} b second array
 * @return{Array!} complement of two arrays
 */
function complement(a, b) {
  (b) || (b = a, a = this);
  return (Array.isArray(a) && Array.isArray(b)) ? a.filter(function(x) {
    return b.indexOf(x) === -1;
  }) : undefined;
}

/**
 * Your main logic for consolidating or outputting results after
 * a single execution of the script. These single execution results may
 * reflect the processing on only a subset of your accounts.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object!,
 *       error: string
 *     }>!} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    STATUSES.COMPLETE if the account was processed successfully,
 *    STATUSES.FAILED if there was an error, and STATUSES.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    STATUSES.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    STATUSES.FAILED.
 */
function processIntermediateResults(results) {
  // This example simply logs the number of campaigns and ad groups
  // in each of the accounts successfully processed in this execution.
  Logger.log('Results of this execution:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == STATUSES.COMPLETE) {
      Logger.log(
          customerId + ': ' + result.returnValue.numCampaigns +
          ' campaigns and ' + result.returnValue.numShoppingCampaigns +
          ' shopping campaigns processed');
    } else if (result.status == STATUSES.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }
}

/**
 * Your main logic for consolidating or outputting results after
 * the script has executed a complete cycle across all of your accounts.
 * This function will only be called once per complete cycle.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object!,
 *       error: string
 *     }>!} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    STATUSES.COMPLETE if the account was processed successfully,
 *    STATUSES.FAILED if there was an error, and STATUSES.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    STATUSES.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    STATUSES.FAILED.
 */
function processFinalResults(results) {
  // This template simply logs the total number of campaigns and ad
  // groups across all accounts successfully processed in the cycle.
  var numCampaigns = 0;
  var numShoppingCampaigns = 0;

  Logger.log('Results of this cycle:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == STATUSES.COMPLETE) {
      Logger.log(customerId + ': successful');
      numCampaigns += result.returnValue.numCampaigns;
      numShoppingCampaigns += result.returnValue.numShoppingCampaigns;
    } else if (result.status == STATUSES.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }

  Logger.log('Total number of campaigns processed: ' + numCampaigns);
  Logger.log(
      'Total number of shopping campaigns processed: ' + numShoppingCampaigns);
}

/**
 * reads the configurations from the
 * spreadsheet defined above
 *
 * @return {Object!} account/audience list mapping, targeting type flag and
 * excluded campaign label
 */
function readInput() {
  var accountAudienceListMap = {};
  if ('INSERT_SPREADSHEET_URL_HERE' === 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.');
  }
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var accountIds =
      spreadsheet.getRangeByName('ACCOUNTS_AND_AUDIENCES').getValues();
  var isBidOnly = spreadsheet.getRangeByName('TARGETING_SETTING').getValue() ==
          'bid only: Observation' ? true : false;
  var excludedCampaignLabel = spreadsheet.getRangeByName('EXCLUDED_LABEL')
                                          .getValue();

  for (var i = 0; i < accountIds.length; i++) {
    if (accountIds[i][0] === accountIds[i][1]) {
      break;
    }
    accountAudienceListMap[accountIds[i][0]] =
        accountIds[i][1]
            .replace(/\r?\n|\r/g, '')
            .split(',');  // remove new line characters
  }

  return {
    'bidOnly': isBidOnly,
    'accountAudienceListMap': accountAudienceListMap,
    'excludedCampaignLabel': excludedCampaignLabel
  };
}

/**
 * Ensure that campaign label exists in the account if specified.
 *
 * @return {id} The id of the specific label
 */
function getLabelId() {
  var labelIterator =
      AdsApp.labels()
          .withCondition('Name = \'' + CONFIG.excludedCampaignLabel + '\'')
          .get();
  if (labelIterator.hasNext()) {
    var label = labelIterator.next();
    var labelId = label.getId();
    return labelId;
  } else {
    AdsApp.createLabel(
        CONFIG.excludedCampaignLabel,
        'used to exclude specific campaigns from the Audience Assistant',
        '#808080');
    var labelIterator =
        AdsApp.labels()
            .withCondition('Name = \'' + CONFIG.excludedCampaignLabel + '\'')
            .get();
    if (labelIterator.hasNext()) {
      var label = labelIterator.next();
      var labelId = label.getId();
      return labelId;
    }
  }
}

// Whether or not the script is running in a manager account.
var IS_MANAGER = typeof AdsManagerApp !== 'undefined';

// The maximum number of accounts that can be processed when using
// executeInParallel().
var MAX_PARALLEL = 50;

// The possible modes in which the script can execute.
var Modes = {SINGLE: 'Single', MANAGER_PARALLEL: 'Manager Parallel'};

/**
 * Main method
 */
function main() {
  var mode = getMode();
  stateManager.loadState();

  // The last execution may have attempted the final set of accounts but
  // failed to actually complete the cycle because of a timeout in
  // processIntermediateResults(). In that case, complete the cycle now.
  if (stateManager.getAccountsWithStatus().length > 0) {
    completeCycleIfNecessary();
  }

  // If the cycle is complete and enough time has passed since the start of
  // the last cycle, reset it to begin a new cycle.
  if (stateManager.getStatus() == STATUSES.COMPLETE) {
    if (dayDifference(stateManager.getLastStartTime(), new Date()) > 0) {
      stateManager.resetState();
    } else {
      Logger.log(
          'Waiting until ' + 0 +
          ' days have elapsed since the start of the last cycle.');
      return;
    }
  }

  // Find accounts that have not yet been processed. If this is the
  // beginning of a new cycle, this will be all accounts.
  var customerIds = stateManager.getAccountsWithStatus(STATUSES.NOT_STARTED);

  // The status will be STATUSES.NOT_STARTED if this is the very first
  // execution or if the cycle was just reset. In either case, it is the
  // beginning of a new cycle.
  if (stateManager.getStatus() == STATUSES.NOT_STARTED) {
    stateManager.setStatus(STATUSES.STARTED);
    stateManager.saveState();

    initializeCycle(customerIds);
  }

  // Don't attempt to process more accounts than specified, and
  // enforce the limit on parallel execution if necessary.
  var accountLimit = 50;

  if (mode == Modes.MANAGER_PARALLEL) {
    accountLimit = Math.min(MAX_PARALLEL, accountLimit);
  }

  var customerIdsToProcess = customerIds.slice(0, accountLimit);

  // Save state so that we can detect when an account timed out by it still
  // being in the STARTED state.
  stateManager.setAccountsWithStatus(customerIdsToProcess, STATUSES.STARTED);
  stateManager.saveState();

  initializeExecution(customerIdsToProcess);
  executeByMode(mode, customerIdsToProcess);
}

/**
 * Runs the script on a list of accounts in a given mode.
 *
 * @param {string} mode The mode the script should run in.
 * @param {Array.<string>!} customerIds The customerIds that this execution
 *     should process. If mode is Modes.SINGLE, customerIds must contain
 *     a single element which is the customerId of the Ads account.
 */
function executeByMode(mode, customerIds) {
  switch (mode) {
    case Modes.SINGLE:
      var results = {};
      results[customerIds[0]] = tryProcessAccount();
      completeExecution(results);
      break;

    case Modes.MANAGER_PARALLEL:
      if (customerIds.length == 0) {
        completeExecution({});
      } else {
        var accountSelector = AdsManagerApp.accounts().withIds(customerIds);
        accountSelector.executeInParallel(
            'parallelFunction', 'parallelCallback');
      }
      break;
  }
}

/**
 * Attempts to process the current Ads account.
 *
 * @return {Object!} The result of the processing if successful, or
 *     an object with status STATUSES.FAILED and the error message
 *     if unsuccessful.
 */
function tryProcessAccount() {
  try {
    return {status: STATUSES.COMPLETE, returnValue: processAccount()};
  } catch (e) {
    return {status: STATUSES.FAILED, error: e.message};
  }
}

/**
 * The function given to executeInParallel() when running in parallel mode.
 * This helper function is necessary so that the return value of
 * processAccount() is transformed into a string as required by
 * executeInParallel().
 *
 * @return {string} JSON string representing the return value of
 *     processAccount().
 */
function parallelFunction() {
  var returnValue = processAccount();

  return JSON.stringify(returnValue);
}

/**
 * The callback given to executeInParallel() when running in parallel mode.
 * Processes the execution results into the format used by all execution
 * modes.
 *
 * @param {Array.<Object!>!} executionResults An array of execution results
 *     from a parallel execution.
 */
function parallelCallback(executionResults) {
  var results = {};
  for (var i = 0; i < executionResults.length; i++) {
    var executionResult = executionResults[i];
    var status;
    if (executionResult.getStatus() == 'OK') {
      status = STATUSES.COMPLETE;
    } else if (executionResult.getStatus() == 'TIMEOUT') {
      status = STATUSES.STARTED;
    } else {
      status = STATUSES.FAILED;
    }
    results[executionResult.getCustomerId()] = {
      status: status,
      returnValue: JSON.parse(executionResult.getReturnValue()),
      error: executionResult.getError()
    };
  }
  // After executeInParallel(), variables in global scope are reevaluated,
  // so reload the state.
  stateManager.loadState();

  completeExecution(results);
}

/**
 * Completes a single execution of the script by saving the results and
 * calling the intermediate and final result handlers as necessary.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object!,
 *       error: string
 *     }>!} results The results of the current execution of the script.
 */
function completeExecution(results) {
  for (var customerId in results) {
    var result = results[customerId];
    stateManager.setAccountWithResult(customerId, result);
  }
  stateManager.saveState();

  processIntermediateResults(results);
  completeCycleIfNecessary();
}

/**
 * Completes a full cycle of the script if all accounts have been attempted
 * but the cycle has not been marked as complete yet.
 */
function completeCycleIfNecessary() {
  if (stateManager.getAccountsWithStatus(STATUSES.NOT_STARTED).length == 0 &&
      stateManager.getStatus() != STATUSES.COMPLETE) {
    stateManager.setStatus(STATUSES.COMPLETE);
    stateManager.saveState();
    processFinalResults(stateManager.getResults());
  }
}

/**
 * Determines what mode the script should run in.
 *
 * @return {string} The mode to run in.
 */
function getMode() {
  if (IS_MANAGER) {
    return Modes.MANAGER_PARALLEL;
  } else {
    return Modes.SINGLE;
  }
}

/**
 * Finds all customer IDs that the script could process. For a single account,
 * this is simply the account itself.
 *
 * @return {Array.<string>!} A list of customer IDs.
 */
function getCustomerIdsPopulation() {
  if (IS_MANAGER) {
    var customerIds = [];

    var selector = AdsManagerApp.accounts()
                       .forDateRange('LAST_30_DAYS')
                       .orderBy('Clicks DESC')
                       .orderBy('Cost DESC');

    var accounts = selector.get();
    while (accounts.hasNext()) {
      customerIds.push(accounts.next().getCustomerId());
    }

    return customerIds;
  } else {
    return [AdsApp.currentAccount().getCustomerId()];
  }
}

/**
 * Returns the number of days between two dates.
 *
 * @param {Object!} from The older Date object.
 * @param {Object!} to The newer (more recent) Date object.
 * @return {number} The number of days between the given dates (possibly
 *     fractional).
 */
function dayDifference(from, to) {
  return (to.getTime() - from.getTime()) / (24 * 3600 * 1000);
}

/**
 * Loads a JavaScript object previously saved as JSON to a file on Drive.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object was previously saved.
 * @return {Object!} The JavaScript object, or null if the file was not found.
 */
function loadObject(filename) {
  if ('INSERT_UNIQUE_FILENAME_HERE' === filename) {
    throw new Error(
        'Please specify a unique filename for the script to create. You must ' +
        'use a different filename for each script running in the account.');
  }
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    return null;
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    return JSON.parse(file.getBlob().getDataAsString());
  }
}

/**
 * Saves a JavaScript object as JSON to a file on Drive. An existing file with
 * the same name is overwritten.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object should be saved.
 * @param {Object!} obj The object to save.
 */
function saveObject(filename, obj) {
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    DriveApp.createFile(filename, JSON.stringify(obj));
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    file.setContent(JSON.stringify(obj));
  }
}

/**
 * Throws an exception if there are multiple files with the same name.
 *
 * @param {string} filename The filename that caused the error.
 */
function throwDuplicateFileException(filename) {
  throw 'Multiple files named ' + filename + ' detected. Please ensure ' +
      'there is only one file named ' + filename + ' and try again.';
}

var stateManager = (function() {
  /**
   * @type {{
   *   cycle: {
   *     status: string,
   *     lastUpdate: string,
   *     startTime: string
   *   },
   *   accounts: Object.<string, {
   *     status: string,
   *     lastUpdate: string,
   *     returnValue: Object!
   *   }>!
   * }}
   */
  var state;

  /**
   * Loads the saved state of the script. If there is no previously
   * saved state, sets the state to an initial default.
   */
  var loadState = function() {
    state = loadObject(UNIQUE_FILENAME);
    if (!state) {
      resetState();
    }
  };

  /**
   * Saves the state of the script to Drive.
   */
  var saveState = function() {
    saveObject(UNIQUE_FILENAME, state);
  };

  /**
   * Resets the state to an initial default.
   */
  var resetState = function() {
    state = {};
    var date = Date();

    state.cycle = {
      status: STATUSES.NOT_STARTED,
      lastUpdate: date,
      startTime: date
    };

    state.accounts = {};
    var customerIds = getCustomerIdsPopulation();

    for (var i = 0; i < customerIds.length; i++) {
      state.accounts[customerIds[i]] = {
        status: STATUSES.NOT_STARTED,
        lastUpdate: date
      };
    }
  };

  /**
   * Gets the status of the current cycle.
   *
   * @return {string} The status of the current cycle.
   */
  var getStatus = function() {
    return state.cycle.status;
  };

  /**
   * Sets the status of the current cycle.
   *
   * @param {string} status The status of the current cycle.
   */
  var setStatus = function(status) {
    var date = Date();

    if (status == STATUSES.IN_PROGRESS &&
        state.cycle.status == STATUSES.NOT_STARTED) {
      state.cycle.startTime = date;
    }

    state.cycle.status = status;
    state.cycle.lastUpdate = date;
  };

  /**
   * Gets the start time of the current cycle.
   *
   * @return {Object!} Date object for the start of the last cycle.
   */
  var getLastStartTime = function() {
    return new Date(state.cycle.startTime);
  };

  /**
   * Gets accounts in the current cycle with a particular status.
   *
   * @param {string} status The status of the accounts to get.
   *     If null, all accounts are retrieved.
   * @return {Array.<string>!} A list of matching customerIds.
   */
  var getAccountsWithStatus = function(status) {
    var customerIds = [];

    for (var customerId in state.accounts) {
      if (!status || state.accounts[customerId].status == status) {
        customerIds.push(customerId);
      }
    }

    return customerIds;
  };

  /**
   * Sets accounts in the current cycle with a particular status.
   *
   * @param {Array.<string>!} customerIds A list of customerIds.
   * @param {string} status A status to apply to those customerIds.
   */
  var setAccountsWithStatus = function(customerIds, status) {
    var date = Date();

    for (var i = 0; i < customerIds.length; i++) {
      var customerId = customerIds[i];

      if (state.accounts[customerId]) {
        state.accounts[customerId].status = status;
        state.accounts[customerId].lastUpdate = date;
      }
    }
  };

  /**
   * Registers the processing of a particular account with a result.
   *
   * @param {string} customerId The account that was processed.
   * @param {Object.<string, {
   *       status: string,
   *       returnValue: Object!,
   *       error: string
   *     }>!} result The object to save for that account.
   */
  var setAccountWithResult = function(customerId, result) {
    if (state.accounts[customerId]) {
      state.accounts[customerId].status = result.status;
      state.accounts[customerId].returnValue = result.returnValue;
      state.accounts[customerId].error = result.error;
      state.accounts[customerId].lastUpdate = Date();
    }
  };

  /**
   * Gets the current results of the cycle for all accounts.
   *
   * @return {Object.<string, {
   *       status: string,
   *       lastUpdate: string,
   *       returnValue: Object!,
   *       error: string
   *     }>!} The results processed by the script during the cycle,
   *    keyed by account.
   */
  var getResults = function() {
    return state.accounts;
  };

  return {
    loadState: loadState,
    saveState: saveState,
    resetState: resetState,
    getStatus: getStatus,
    setStatus: setStatus,
    getLastStartTime: getLastStartTime,
    getAccountsWithStatus: getAccountsWithStatus,
    setAccountsWithStatus: setAccountsWithStatus,
    setAccountWithResult: setAccountWithResult,
    getResults: getResults
  };
})();

Send feedback about...

Google Ads scripts
Google Ads scripts
Need help? Visit our support page.