Large Manager Hierarchy Template

Tools icon

A Google Ads manager account can manage hundreds or even thousands of client accounts. Running a script across all of them can be challenging, as working on them in parallel using executeInParallel limits your script to a maximum of 50 accounts per execution, and iterating through them sequentially can cause your script to time out if you have too many accounts.

This script addresses this challenge by processing a different subset of accounts each time it runs. Each run stores its intermediate results to a temporary file on Google Drive so that subsequent runs know which accounts are left to be processed. Over the course of many runs, the script eventually cycles through and processes all client accounts. Once it finishes the last remaining accounts, it resets and starts a new processing cycle (subject to a frequency you specify).

The template is divided into two sections. The section labeled STANDARD TEMPLATE provides the functionality for cycling through your accounts over many script executions, and you should have no need to edit it.

The section labeled YOUR IMPLEMENTATION provides several placeholder functions or "hooks" for you to fill in with the processing logic you actually want to perform on your accounts. The source code below gives a sample implementation.

How it works

The core of the script is in the section labeled STANDARD TEMPLATE, which includes the script's main() function. A stateManager object exposes methods for loading and saving the state of the cycle from previous executions.

At the beginning of each execution, main() loads the previous state, starts a new cycle if enough time has passed since the last completed cycle, determines a subset of accounts to process, calls the initializeCycle() hook if appropriate, and calls the initializeExecution() hook. It then calls executeByMode(), which processes the subset of accounts using a sequential or parallel strategy. Both strategies call the processAccount() hook for each account in the subset, eventually passing the results to completeExecution(), which saves them to Drive. Lastly, the template calls the processIntermediateResults() hook and, if it is the end of the cycle, the processFinalResults hook as well.

Setup

Using the template consists of these steps:

  1. Create a new script from the template.
  2. Set template-level parameters.
  3. Provide your core logic.

Create a new script from the template

Set up a new script with the source code below. In the TEMPLATE_CONFIG object, provide a FILENAME for the file that is to be used to save intermediate results on Drive. The script creates this file the first time it executes and reuses the file in subsequent executions.

The FILENAME can be anything you like, but a good practice is to give it a name similar to the name of the script. This makes it easier for you to identify the file later on.

Set template-level parameters

There are several template-level parameters to set.

  • MIN_FREQUENCY: Controls the minimum frequency of the cycles. The next cycle does not start until at least this many days have passed since the start of the previous cycle. Note that the actual frequency can be longer if a cycle itself takes more time to complete than MIN_FREQUENCY. This might occur because, for example, there are a large number of accounts to process or the script itself is scheduled too infrequently to cycle through all of them within MIN_FREQUENCY days.
  • USE_PARALLEL_MODE: Controls whether the script processes accounts in parallel or iterates through accounts sequentially.
  • MAX_ACCOUNTS: Controls the maximum number of accounts the script attempts to process in a single execution. When running in parallel mode, the actual number of accounts processed is capped at 50.
  • ACCOUNT_CONDITIONS (optional): An array of ManagedAccountSelector conditions to control which accounts are processed by the script. For example, you can add a condition like customer_client.applied_labels CONTAINS ANY ("LABEL_RESOURCE_NAME") to process only accounts with a certain label.

See Scheduling for suggestions on how to set these parameters for different use cases.

Provide your core logic

The template offers five functions or "hooks" that are called at specific times during each cycle, as depicted below. You can provide an implementation for each hook to perform the processing you need for your script.

Flow diagram for script

For each hook, the template includes a sample implementation which should be replaced by your own logic. You can also add your own functions and variables to your script, just as you would normally, and use them from within these five hooks.

initializeCycle(customerIds)

This function is called once at the beginning of each cycle with the list of accounts that are to be processed (those that match ACCOUNT_CONDITIONS). Use it to perform any initialization your script needs for an entire cycle. Some possible use cases are:

  • Send an email to indicate a cycle has started.
  • Load or generate static data that you want to be the same for all of your script's executions during the cycle. You must save that data, such as on Drive or in a spreadsheet, so that it can be loaded later in each execution.

If you have no initialization to perform, leave your implementation blank.

initializeExecution(customerIds)

This function is called once at the beginning of each execution with the subset of accounts that are to be processed during that execution. Use it to perform any initialization your script needs for each execution. Some possible use cases are:

  • Send an email to indicate an execution has started.
  • Load static data that is intended to be the same for all executions (see above).

If you have no initialization to perform, leave your implementation blank.

processAccount()

Within a cycle, this function is called once on each Google Ads account. Use it to perform the main processing logic of your script, such as retrieving and analyzing reports, pausing/unpausing campaigns, modifying bids, creating ads, sending an email alert, saving data to a spreadsheet, or anything else you can do with Google Ads scripts.

Optionally, this function can return arbitrary results associated with processing the account. Returning results enables you to consolidate results across accounts before outputting them, such as sending a single email alert covering the results from all accounts rather than one email per account (see below).

processIntermediateResults(results)

This function is called once at the end of each execution with the results from the accounts processed during that execution. Use it to analyze, consolidate, and output the results from that execution. Some possible use cases are:

  • Add the results from this execution to a spreadsheet.
  • Create and send a summary email with all of the results from this execution.

If you don't have any results you want to output, or if you want to output them only after the entire cycle is complete, leave your implementation blank.

processFinalResults(results)

This function is called once at the end of each cycle with the results from all accounts processed during the cycle. Use it to analyze, consolidate, and output the results from all of the accounts you included in the cycle. Some possible use cases are:

  • Perform an analysis that requires you having results from all of your accounts.
  • Add all of the results to a spreadsheet at one time.
  • Create and send a summary email with all of the results across all accounts.

If you do not have any results you want to output, leave your implementation blank.

Scheduling

Scheduling a script that uses the Manager Account Template requires slightly more thought compared to other scripts because you must think in terms of cycles rather than individual executions. Most users want each cycle to complete as quickly as possible. To do so, follow these steps:

  • Schedule the script to run Hourly.
  • Estimate how long your processAccount() implementation takes on a single account.

    One way to form an estimate is by running the script in sequential mode with ACCOUNT_CONDITIONS set so that it operates on only a few accounts, then dividing the execution time by the number of accounts.

  • Set USE_PARALLEL_MODE and MAX_ACCOUNTSas follows.

    • A script running in sequential mode can run for 30 minutes (1,800 seconds) before timing out. A script running in parallel mode can operate on up to 50 accounts. Which mode processes more accounts per execution depends on how long your script takes to process each account.
    • If you estimate your processing time per account is less than 36 seconds (1,800 / 50), then using sequential mode is preferable. Likewise, if you estimate your processing is more than 36 seconds per account, using parallel mode is preferable. To allow for variability in per-account processing time and some padding for additional post-processing, a cutoff of 30 instead of 36 is reasonable.
    • If you are using parallel mode, set MAX_ACCOUNTS to 50. There is no benefit to specifying a larger number since it will be capped at 50.
    • If you're using sequential mode, set MAX_ACCOUNTS to as large a number as you can process within 30 minutes. For example, if you estimated each account can be processed in 10 seconds, you could set MAX_ACCOUNTS to
    • In practice, a slightly smaller number is better so as to leave time for your processIntermediateResults() and processFinalResults() implementations to complete.
  • Though you have scheduled individual executions in Google Ads to run hourly, you can set MIN_FREQUENCY to space out the cycles. For example, if you want to generate a spreadsheet report once per week covering all of your accounts, set MIN_FREQUENCY to 7.

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 Large Manager Hierarchy Template
 *
 * @overview The Large Manager Hierarchy Template script provides a general way
 *     to run script logic on all client accounts within a manager account
 *     hierarchy, splitting the work across multiple executions if necessary.
 *     Each execution of the script processes a subset of the hierarchy's client
 *     accounts that it hadn't previously processed, saving the results to a
 *     temporary file on Drive. Once the script processes the final subset of
 *     accounts, the consolidated results can be output and the cycle can begin
 *     again.
 *     See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/adsmanagerapp-manager-template
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.1
 *
 * @changelog
 * - version 2.1
*    - Bugfixes.
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0
 *   - Released initial version.
 */

/*************** START OF YOUR IMPLEMENTATION ***************/

const TEMPLATE_CONFIG = {
  // 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.
  FILENAME: 'UNIQUE_FILENAME_HERE',

  // The minimum number of days between the start of each cycle.
  MIN_FREQUENCY: 1,

  // Controls whether child accounts will be processed in parallel (true)
  // or sequentially (false).
  USE_PARALLEL_MODE: true,

  // Controls the maximum number of accounts that will be processed in a
  // single script execution.
  MAX_ACCOUNTS: 50,

  // A list of ManagedAccountSelector conditions to restrict the population
  // of child accounts that will be processed. Leave blank or comment out
  // to include all child accounts.
  ACCOUNT_CONDITIONS: []
};

// The possible statuses for the script as a whole or an individual account.
const Statuses = {
  NOT_STARTED: 'Not Started',
  STARTED: 'Started',
  FAILED: 'Failed',
  COMPLETE: 'Complete'
};

let stateManager;

/**
 * Your main logic for initializing a cycle for your script.
 *
 * @param {Array.<string>} customerIds The customerIds that this cycle
 *     will process.
 */
function initializeCycle(customerIds) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this cycle.
  console.log('Accounts to be processed this cycle:');
  for (const customerId of customerIds) {
    console.log(customerId);
  }
}

/**
 * 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) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this execution.
  console.log('Accounts to be processed this execution:');
  for (const customerId of customerIds) {
    console.log(customerId);
  }
}

/**
 * Your main logic for processing a single Google Ads account. This function
 * can perform any sort of processing on the account, followed by
 * outputting results immediately (e.g., sending an email, saving to a
 * spreadsheet, etc.) and/or returning results to be output later, e.g.,
 * to be combined with the output from other accounts.
 *
 * @return {Object} An object containing any results of your processing
 *    that you want to output later.
 */
function processAccount() {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply returns the number of campaigns and ad groups
  // in the account.
  return {
    numCampaigns: AdsApp.campaigns().get().totalNumEntities(),
    numAdGroups: AdsApp.adGroups().get().totalNumEntities()
  };
}

/**
 * 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) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply logs the number of campaigns and ad groups
  // in each of the accounts successfully processed in this execution.
  console.log('Results of this execution:');
  for (const customerId in results) {
    const result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      console.log(`${customerId}: ${result.returnValue.numCampaigns}` +
                 ` campaigns, ${result.returnValue.numAdGroups}` +
                 ` ad groups`);
    } else if (result.status == Statuses.STARTED) {
      console.error(`${customerId}: timed out`);
    } else {
      console.error(`${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) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This template simply logs the total number of campaigns and ad
  // groups across all accounts successfully processed in the cycle.
  let numCampaigns = 0;
  let numAdGroups = 0;

  console.log('Results of this cycle:');
  for (const customerId in results) {
    const result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      console.log(`${customerId}: successful`);
      numCampaigns += result.returnValue.numCampaigns;
      numAdGroups += result.returnValue.numAdGroups;
    } else if (result.status == Statuses.STARTED) {
      console.error(`${customerId}: timed out`);
    } else {
      console.error(`${customerId}: failed due to "${result.error}"`);
    }
  }

  console.log(`Total number of campaigns: ${numCampaigns}`);
  console.log(`Total number of ad groups: ${numAdGroups}`);
}

/**************** END OF YOUR IMPLEMENTATION ****************/

/**************** START OF STANDARD TEMPLATE ****************/

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

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

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

function main() {
  const mode = getMode();
  stateManager = new StateManager();
  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()) >
        TEMPLATE_CONFIG.MIN_FREQUENCY) {
      stateManager.resetState();
    } else {
      console.log(`Waiting until ${TEMPLATE_CONFIG.MIN_FREQUENCY}` +
                  ` 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.
  const 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.
  let accountLimit = TEMPLATE_CONFIG.MAX_ACCOUNTS;

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

  const 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 Google Ads account.
 */
function executeByMode(mode, customerIds) {
  let results;
  switch (mode) {
    case Modes.SINGLE:
      results = {};
      results[customerIds[0]] = tryProcessAccount();
      completeExecution(results);
      break;

    case Modes.MANAGER_SEQUENTIAL:
      const accounts = AdsManagerApp.accounts().withIds(customerIds).get();
      results = {};

      const managerAccount = AdsApp.currentAccount();
      for (const account of accounts) {
        AdsManagerApp.select(account);
        results[account.getCustomerId()] = tryProcessAccount();
      }
      AdsManagerApp.select(managerAccount);

      completeExecution(results);
      break;

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

/**
 * Attempts to process the current Google 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() {
  const 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) {
  const results = {};

  for (const executionResult of executionResults) {
    let 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 = new StateManager();
  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 (const customerId in results) {
    const 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) {
    if (TEMPLATE_CONFIG.USE_PARALLEL_MODE) {
      return Modes.MANAGER_PARALLEL;
    } else {
      return Modes.MANAGER_SEQUENTIAL;
    }
  } 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) {
    const customerIds = [];

    let selector = AdsManagerApp.accounts();
    const conditions = TEMPLATE_CONFIG.ACCOUNT_CONDITIONS || [];
    for (const condition of conditions) {
      selector = selector.withCondition(condition);
    }

    const accounts = selector.get();
    for (const account of accounts) {
      customerIds.push(account.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) {
  const files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    return null;
  } else {
    const 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 {obj} obj The object to save.
 */
function saveObject(filename, obj) {
  const files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    DriveApp.createFile(filename, JSON.stringify(obj));
  } else {
    const 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.`;
}

class StateManager {

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

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

  /**
   * Saves the state of the script to Drive.
   */
  saveState() {
    saveObject(TEMPLATE_CONFIG.FILENAME, this.state);
  };

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

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

    this.state.accounts = {};
    const customerIds = getCustomerIdsPopulation();

    for (const customerId of customerIds) {
      this.state.accounts[customerId] = {
        status: Statuses.NOT_STARTED,
        lastUpdate: date
      };
    }
  };

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

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

    if (status == Statuses.STARTED &&
        this.state.cycle.status == Statuses.NOT_STARTED) {
      this.state.cycle.startTime = date;
    }

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

  /**
   * Gets the start time of the current cycle.
   *
   * @return {Object} Date object for the start of the last cycle.
   */
  getLastStartTime() {
    return new Date(this.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.
   */
  getAccountsWithStatus(status) {
    const customerIds = [];

    for (const customerId in this.state.accounts) {
      if (!status || this.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.
   */
  setAccountsWithStatus(customerIds, status) {
    const date = Date();

    for (const customerId of customerIds) {

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

  /**
   * Registers the processing of a particular account with a result.
   *
   * @param {string} customerId The account that was processed.
   * @param {{
   *       status: string,
   *       returnValue: !Object,
   *       error: string
   *     }} result The object to save for that account.
   */
  setAccountWithResult(customerId, result) {
    if (this.state.accounts[customerId]) {
      this.state.accounts[customerId].status = result.status;
      this.state.accounts[customerId].returnValue = result.returnValue;
      this.state.accounts[customerId].error = result.error;
      this.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.
   */
  getResults() {
    return this.state.accounts;
  };
}

/***************** END OF STANDARD TEMPLATE *****************/