This is the legacy documentation for Google Ads scripts. Go to the current docs.

Weather-based Campaign Management

Bidding

This script shows a specific application of weather based campaign management. Click here for a more generic version of this script that you can adapt to your campaign management requirements.

Demand for certain products and services varies greatly depending on the weather. For example, users are much more likely to search for information on amusement parks on a hot, sunny day than if it's cold and raining. An amusement park company may want to increase their bids when the weather is nice, but doing so every day would require a lot of manual work. With Google Ads scripts, however, it's possible to programmatically fetch weather information and adjust bids in a matter of minutes.

This script uses Google Spreadsheets to store the list of campaigns and their associated locations. A call to the OpenWeatherMap API is made for each location and weather conditions are calculated using some basic rules. If a rule evaluates to true, then a corresponding location bid multiplier is applied to the location targeting for the campaign.

How it works

The script works by reading data off a spreadsheet. The spreadsheet consists of three individual sheets:

1. Campaign data

A set of rules determines the bid modifiers to be applied to campaigns when a weather condition is met. Required columns in this sheet are:

  • Campaign Name: The name of the campaign to be modified.
  • Weather Location: The location for which weather conditions should be checked.
  • Weather Condition: The weather condition for which this rule should be applied.
  • Bid Modifier: The location bid modifier to be applied if the weather condition is met.
  • Apply Modifier To: Whether the bid modifier should be applied only to campaign geo targets that match the 'Weather Location' or to all campaign geo targets.
  • Enabled: Specify Yes to enable a rule and No to disable it.
The example below illustrates different usage scenarios.

Test Campaign 1 illustrates a typical usage scenario. We suppose that the campaign is targeting Boston, MA. It has two rules:

  1. Apply a bid modifier of 1.3 if the weather in Boston, MA is Sunny.
  2. Apply a bid modifier of 0.8 if the weather in Boston, MA is Rainy.

Test Campaign 2 illustrates another typical usage scenario. We suppose that the campaign is targeting several cities in New York and Connecticut, but we are only interested in weather-based bidding for the Connecticut cities. It has two rules, and the 'Weather Location' of Connecticut is mapped to several cities (see below). As a result, only those Connecticut cities that the campaign targets will have their bid modifiers adjusted. The New York cities will be unaffected.

For Test Campaign 3, we suppose that the campaign targets many cities in Florida but not the state of Florida as a whole. Since the weather rules for Florida are mapped to the entire state, which is not a location that the campaign explicitly targets, the 'Apply Modifier To' is set to All Geo Targets so that the cities that the campaign targets are affected.

Note: If multiple rules match for a given campaign and location, the last matching rule wins.

2. Weather data

This sheet defines the weather conditions used in the Campaign data sheet. Required columns are:

  • Condition Name: The weather condition name (e.g. Sunny).
  • Temperature: The temperature in Fahrenheit.
  • Precipitation: The rain, in millimeters during the last 3 hours.
  • Wind: The wind speed, in miles per hour.

The sheet shown above defines two weather conditions:

  1. Sunny: Temperature is between 65 and 80 degrees Fahrenheit, precipitation below 1mm of rain in the last three hours, and wind speed less than 5 mph.
  2. Rainy: Precipitation is above 0mm of rain in the last three hours and wind speed less than 10 mph.

Defining weather conditions

When defining weather conditions, you may specify the values as follows:

  • below x: The specified value is below x (e.g. below 10)
  • above x: The specified value is above x (e.g. above 70)
  • x to y: The specified value is between x and y, inclusive (e.g. 65 to 80)

If you leave a cell blank, then the values of that parameter is not considered for calculation. For example, in the sheet shown above, the "Rainy" weather condition has an empty temperature column, so the temperature won't be considered when calculating this weather condition.

The weather conditions are ANDed together when calculating the weather condition. For this example, the "Sunny" weather condition is evaluated as:

var isSunny = (temperature >= 65 && temperature <= 80) && (precipitation < 1) && (wind < 5);

3. Weather location data

This sheet defines the weather locations used in the Campaign data sheet, and consists of only two columns:

  • Weather Location: This is a weather location name, as understood by the OpenWeatherMap API.
  • Geo Target Code: This is a geo target code, as understood by Google Ads.

The script allows you to specify multiple geo target codes for a single weather location. This is supported because weather locations are not always as granular as the targeting options available in Google Ads, and you may find it necessary to map a single weather location to multiple geo locations. Mapping a single weather location to multiple geo locations can be done by having multiple rows with the same weather location but different geo codes for each row.

In the spreadsheet above, there are three weather locations defined:

  1. Boston, MA: Geo code 10108127
  2. Connecticut: Geo codes 1014778, 1014743, and 1014843, corresponding to several cities in Connecticut
  3. Florida: Geo code 21142

Proximity Targeting

Campaign rules using Matching Geo Targets can be applied to targeted locations , targeted proximities, or both by using the TARGETING flag. Location targeting matches geocodes with location IDs. Proximity targeting verifies that the specified latitude and longitude coordinates are within the proximity radius using the Haversine formula.

Script logic

The script starts by reading rules from all three sheets. It then attempts to execute each rule from the Campaign sheet in sequence.

For each executed rule, the script checks if the campaign targets the specified location. If it does, then the script retrieves the current bid modifier. Next, the weather conditions for that location is retrieved by making a call to OpenWeatherMap API. The weather condition rules are then evaluated to see if the weather condition for the location matches what is specified in the rule. If it does, and the new bid modifier is different from the current bid modifier, the script modifies the bid modifier for that location.

No changes are made if the weather condition doesn't match, if the bid modifier values are the same, or if the rule's 'Apply Modifier To' is Matching Geo Targets but the campaign doesn't target the locations mapped to the rule.

Setup

  • Register for an API key at http://openweathermap.org/appid.
  • Make a copy of the template spreadsheet and edit your campaign and weather rules.
  • Create a new script with the source code below.
  • Update the OPEN_WEATHER_MAP_API_KEY, SPREADSHEET_URL, and TARGETING variables in the script.
  • Schedule to run as required.

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 Bid By Weather
 *
 * @overview The Bid By Weather script adjusts campaign bids by weather
 *     conditions of their associated locations. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/weather-based-campaign-management#bid-by-weather
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.2.2
 *
 * @changelog
 * - version 1.2.2
 *   - Add support for video and shopping campaigns.
 * - version 1.2.1
 *   - Added validation for external spreadsheet setup.
 * - version 1.2
 *   - Added proximity based targeting.  Targeting flag allows location
 *     targeting, proximity targeting or both.
 * - version 1.1
 *   - Added flag allowing bid adjustments on all locations targeted by
 *     a campaign rather than only those that match the campaign rule
 * - version 1.0
 *   - Released initial version.
 */

// Register for an API key at http://openweathermap.org/appid
// and enter the key below.
var OPEN_WEATHER_MAP_API_KEY = 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE';

// Create a copy of https://goo.gl/A59Uuc and enter the URL below.
var SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';

// A cache to store the weather for locations already lookedup earlier.
var WEATHER_LOOKUP_CACHE = {};

// Flag to pick which kind of targeting "LOCATION", "PROXIMITY", or "ALL".
var TARGETING = 'ALL';


/**
 * The code to execute when running the script.
 */
function main() {
  validateApiKey();
  // Load data from spreadsheet.
  var spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  var campaignRuleData = getSheetData(spreadsheet, 1);
  var weatherConditionData = getSheetData(spreadsheet, 2);
  var geoMappingData = getSheetData(spreadsheet, 3);

  // Convert the data into dictionaries for convenient usage.
  var campaignMapping = buildCampaignRulesMapping(campaignRuleData);
  var weatherConditionMapping =
      buildWeatherConditionMapping(weatherConditionData);
  var locationMapping = buildLocationMapping(geoMappingData);

  // Apply the rules.
  for (var campaignName in campaignMapping) {
    applyRulesForCampaign(campaignName, campaignMapping[campaignName],
        locationMapping, weatherConditionMapping);
  }
}

/**
 * Retrieves the data for a worksheet.
 *
 * @param {Object} spreadsheet The spreadsheet.
 * @param {number} sheetIndex The sheet index.
 * @return {Array} The data as a two dimensional array.
 */
function getSheetData(spreadsheet, sheetIndex) {
  var sheet = spreadsheet.getSheets()[sheetIndex];
  var range =
      sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
  return range.getValues();
}

/**
 * Builds a mapping between the list of campaigns and the rules
 * being applied to them.
 *
 * @param {Array} campaignRulesData The campaign rules data, from the
 *     spreadsheet.
 * @return {!Object.<string, Array.<Object>> } A map, with key as campaign name,
 *     and value as an array of rules that apply to this campaign.
 */
function buildCampaignRulesMapping(campaignRulesData) {
  var campaignMapping = {};
  for (var i = 0; i < campaignRulesData.length; i++) {
    // Skip rule if not enabled.

    if (campaignRulesData[i][5].toLowerCase() == 'yes') {
      var campaignName = campaignRulesData[i][0];
      var campaignRules = campaignMapping[campaignName] || [];
      campaignRules.push({
          'name': campaignName,

          // location for which this rule applies.
          'location': campaignRulesData[i][1],

          // the weather condition (e.g. Sunny).
          'condition': campaignRulesData[i][2],

          // bid modifier to be applied.
          'bidModifier': campaignRulesData[i][3],

          // whether bid adjustments should by applied only to geo codes
          // matching the location of the rule or to all geo codes that
          // the campaign targets.
          'targetedOnly': campaignRulesData[i][4].toLowerCase() ==
                          'matching geo targets'
      });
      campaignMapping[campaignName] = campaignRules;
    }
  }
  Logger.log('Campaign Mapping: %s', campaignMapping);
  return campaignMapping;
}

/**
 * Builds a mapping between a weather condition name (e.g. Sunny) and the rules
 * that correspond to that weather condition.
 *
 * @param {Array} weatherConditionData The weather condition data from the
 *      spreadsheet.
 * @return {!Object.<string, Array.<Object>>} A map, with key as a weather
 *     condition name, and value as the set of rules corresponding to that
 *     weather condition.
 */
function buildWeatherConditionMapping(weatherConditionData) {
  var weatherConditionMapping = {};

  for (var i = 0; i < weatherConditionData.length; i++) {
    var weatherConditionName = weatherConditionData[i][0];
    weatherConditionMapping[weatherConditionName] = {
      // Condition name (e.g. Sunny)
      'condition': weatherConditionName,

      // Temperature (e.g. 50 to 70)
      'temperature': weatherConditionData[i][1],

      // Precipitation (e.g. below 70)
      'precipitation': weatherConditionData[i][2],

      // Wind speed (e.g. above 5)
      'wind': weatherConditionData[i][3]
    };
  }
  Logger.log('Weather condition mapping: %s', weatherConditionMapping);
  return weatherConditionMapping;
}

/**
 * Builds a mapping between a location name (as understood by OpenWeatherMap
 * API) and a list of geo codes as identified by Google Ads scripts.
 *
 * @param {Array} geoTargetData The geo target data from the spreadsheet.
 * @return {!Object.<string, Array.<Object>>} A map, with key as a locaton name,
 *     and value as an array of geo codes that correspond to that location
 *     name.
 */
function buildLocationMapping(geoTargetData) {
  var locationMapping = {};
  for (var i = 0; i < geoTargetData.length; i++) {
    var locationName = geoTargetData[i][0];
    var locationDetails = locationMapping[locationName] || {
      'geoCodes': []      // List of geo codes understood by Google Ads scripts.
    };

    locationDetails.geoCodes.push(geoTargetData[i][1]);
    locationMapping[locationName] = locationDetails;
  }
  Logger.log('Location Mapping: %s', locationMapping);
  return locationMapping;
}

/**
 * Applies rules to a campaign.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {Object} campaignRules The details of the campaign. See
 *     buildCampaignMapping for details.
 * @param {Object} locationMapping Mapping between a location name (as
 *     understood by OpenWeatherMap API) and a list of geo codes as
 *     identified by Google Ads scripts. See buildLocationMapping for details.
 * @param {Object} weatherConditionMapping Mapping between a weather condition
 *     name (e.g. Sunny) and the rules that correspond to that weather
 *     condition. See buildWeatherConditionMapping for details.
 */
function applyRulesForCampaign(campaignName, campaignRules, locationMapping,
                               weatherConditionMapping) {
  for (var i = 0; i < campaignRules.length; i++) {
    var bidModifier = 1;
    var campaignRule = campaignRules[i];

    // Get the weather for the required location.
    var locationDetails = locationMapping[campaignRule.location];
    var weather = getWeather(campaignRule.location);
    Logger.log('Weather for %s: %s', locationDetails, weather);

    // Get the weather rules to be checked.
    var weatherConditionName = campaignRule.condition;
    var weatherConditionRules = weatherConditionMapping[weatherConditionName];

    // Evaluate the weather rules.
    if (evaluateWeatherRules(weatherConditionRules, weather)) {
      Logger.log('Matching Rule found: Campaign Name = %s, location = %s, ' +
          'weatherName = %s,weatherRules = %s, noticed weather = %s.',
          campaignRule.name, campaignRule.location,
          weatherConditionName, weatherConditionRules, weather);
      bidModifier = campaignRule.bidModifier;

      if (TARGETING == 'LOCATION' || TARGETING == 'ALL') {
        // Get the geo codes that should have their bids adjusted.
        var geoCodes = campaignRule.targetedOnly ?
          locationDetails.geoCodes : null;
        adjustBids(campaignName, geoCodes, bidModifier);
      }

      if (TARGETING == 'PROXIMITY' || TARGETING == 'ALL') {
        var location = campaignRule.targetedOnly ? campaignRule.location : null;
        adjustProximityBids(campaignName, location, bidModifier);
      }

    }
  }
  return;
}

/**
 * Converts a temperature value from kelvin to fahrenheit.
 *
 * @param {number} kelvin The temperature in Kelvin scale.
 * @return {number} The temperature in Fahrenheit scale.
 */
function toFahrenheit(kelvin) {
  return (kelvin - 273.15) * 1.8 + 32;
}

/**
 * Evaluates the weather rules.
 *
 * @param {Object} weatherRules The weather rules to be evaluated.
 * @param {Object.<string, string>} weather The actual weather.
 * @return {boolean} True if the rule matches current weather conditions,
 *     False otherwise.
 */
function evaluateWeatherRules(weatherRules, weather) {
  // See https://openweathermap.org/weather-data
  // for values returned by OpenWeatherMap API.
  var precipitation = 0;
  if (weather.rain && weather.rain['3h']) {
    precipitation = weather.rain['3h'];
  }
  var temperature = toFahrenheit(weather.main.temp);
  var windspeed = weather.wind.speed;

  return evaluateMatchRules(weatherRules.temperature, temperature) &&
      evaluateMatchRules(weatherRules.precipitation, precipitation) &&
      evaluateMatchRules(weatherRules.wind, windspeed);
}

/**
 * Evaluates a condition for a value against a set of known evaluation rules.
 *
 * @param {string} condition The condition to be checked.
 * @param {Object} value The value to be checked.
 * @return {boolean} True if an evaluation rule matches, false otherwise.
 */
function evaluateMatchRules(condition, value) {
  // No condition to evaluate, rule passes.
  if (condition == '') {
    return true;
  }
  var rules = [matchesBelow, matchesAbove, matchesRange];

  for (var i = 0; i < rules.length; i++) {
    if (rules[i](condition, value)) {
      return true;
    }
  }
  return false;
}

/**
 * Evaluates whether a value is below a threshold value.
 *
 * @param {string} condition The condition to be checked. (e.g. below 50).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is less than what is specified in
 * condition, false otherwise.
 */
function matchesBelow(condition, value) {
  conditionParts = condition.split(' ');

  if (conditionParts.length != 2) {
    return false;
  }

  if (conditionParts[0] != 'below') {
    return false;
  }

  if (value < conditionParts[1]) {
    return true;
  }
  return false;
}

/**
 * Evaluates whether a value is above a threshold value.
 *
 * @param {string} condition The condition to be checked. (e.g. above 50).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is greater than what is specified in
 *     condition, false otherwise.
 */
function matchesAbove(condition, value) {
  conditionParts = condition.split(' ');

  if (conditionParts.length != 2) {
    return false;
  }

  if (conditionParts[0] != 'above') {
    return false;
  }

  if (value > conditionParts[1]) {
    return true;
  }
  return false;
}

/**
 * Evaluates whether a value is within a range of values.
 *
 * @param {string} condition The condition to be checked (e.g. 5 to 18).
 * @param {number} value The value to be checked.
 * @return {boolean} True if the value is in the desired range, false otherwise.
 */
function matchesRange(condition, value) {
  conditionParts = condition.replace('\w+', ' ').split(' ');

  if (conditionParts.length != 3) {
    return false;
  }

  if (conditionParts[1] != 'to') {
    return false;
  }

  if (conditionParts[0] <= value && value <= conditionParts[2]) {
    return true;
  }
  return false;
}

/**
 * Retrieves the weather for a given location, using the OpenWeatherMap API.
 *
 * @param {string} location The location to get the weather for.
 * @return {Object.<string, string>} The weather attributes and values, as
 *     defined in the API.
 */
function getWeather(location) {
  if (location in WEATHER_LOOKUP_CACHE) {
    Logger.log('Cache hit...');
    return WEATHER_LOOKUP_CACHE[location];
  }

  var url = Utilities.formatString(
      'http://api.openweathermap.org/data/2.5/weather?APPID=%s&q=%s',
      encodeURIComponent(OPEN_WEATHER_MAP_API_KEY),
      encodeURIComponent(location));
  var response = UrlFetchApp.fetch(url);
  if (response.getResponseCode() != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s, Location searched: %s.',
        response.getContentText(), location);
  }

  var result = JSON.parse(response.getContentText());

  // OpenWeatherMap's way of returning errors.
  if (result.cod != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s,  Location searched: %s.',
        response.getContentText(), location);
  }

  WEATHER_LOOKUP_CACHE[location] = result;
  return result;
}

/**
 * Adjusts the bidModifier for a list of geo codes for a campaign.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {Array} geoCodes The list of geo codes for which bids should be
 *     adjusted.  If null, all geo codes on the campaign are adjusted.
 * @param {number} bidModifier The bid modifier to use.
 */
function adjustBids(campaignName, geoCodes, bidModifier) {
  // Get the campaign.
  var campaign = getCampaign(campaignName);
  if (!campaign) return null;

  // Get the targeted locations.
  var locations = campaign.targeting().targetedLocations().get();
  while (locations.hasNext()) {
    var location = locations.next();
    var currentBidModifier = location.getBidModifier().toFixed(2);

    // Apply the bid modifier only if the campaign has a custom targeting
    // for this geo location or if all locations are to be modified.
    if (!geoCodes || (geoCodes.indexOf(location.getId()) != -1 &&
      currentBidModifier != bidModifier)) {
        Logger.log('Setting bidModifier = %s for campaign name = %s, ' +
            'geoCode = %s. Old bid modifier is %s.', bidModifier,
            campaignName, location.getId(), currentBidModifier);
        location.setBidModifier(bidModifier);
    }
  }
}

/**
 * Adjusts the bidModifier for campaigns targeting by proximity location
 * for a given weather location.
 *
 * @param {string} campaignName The name of the campaign.
 * @param {string} weatherLocation The weather location for which bids should be
 *     adjusted.  If null, all proximity locations on the campaign are adjusted.
 * @param {number} bidModifier The bid modifier to use.
 */
function adjustProximityBids(campaignName, weatherLocation, bidModifier) {
  // Get the campaign.
  var campaign = getCampaign(campaignName);
  if(campaign === null) return;

  // Get the proximity locations.
  var proximities = campaign.targeting().targetedProximities().get();
  while (proximities.hasNext()) {
    var proximity = proximities.next();
    var currentBidModifier = proximity.getBidModifier().toFixed(2);

    // Apply the bid modifier only if the campaign has a custom targeting
    // for this geo location or if all locations are to be modified.
    if (!weatherLocation ||
        (weatherNearProximity(proximity, weatherLocation) &&
      currentBidModifier != bidModifier)) {
        Logger.log('Setting bidModifier = %s for campaign name = %s, with ' +
            'weatherLocation = %s in proximity area. Old bid modifier is %s.',
            bidModifier, campaignName, weatherLocation, currentBidModifier);
        proximity.setBidModifier(bidModifier);
      }
  }
}

/**
 * Checks if weather location is within the radius of the proximity location.
 *
 * @param {Object} proximity The targeted proximity of campaign.
 * @param {string} weatherLocation Name of weather location to check within
 * radius.
 * @return {boolean} Returns true if weather location is within radius.
 */
function weatherNearProximity(proximity, weatherLocation) {
  // See https://en.wikipedia.org/wiki/Haversine_formula for details on how
  // to compute spherical distance.
  var earthRadiusInMiles = 3960.0;
  var degreesToRadians = Math.PI / 180.0;
  var radiansToDegrees = 180.0 / Math.PI;
  var kmToMiles = 0.621371;

  var radiusInMiles = proximity.getRadiusUnits() == 'MILES' ?
    proximity.getRadius() : proximity.getRadius() * kmToMiles;

  // Compute the change in latitude degrees for the radius.
  var deltaLat = (radiusInMiles / earthRadiusInMiles) * radiansToDegrees;
  // Find the radius of a circle around the earth at given latitude.
  var r = earthRadiusInMiles * Math.cos(proximity.getLatitude() *
      degreesToRadians);
  // Compute the change in longitude degrees for the radius.
  var deltaLon = (radiusInMiles / r) * radiansToDegrees;

  // Retrieve weather location for lat/lon coordinates.
  var weather = getWeather(weatherLocation);
  // Check if weather condition is within the proximity boundaries.
  return (weather.coord.lat >= proximity.getLatitude() - deltaLat &&
          weather.coord.lat <= proximity.getLatitude() + deltaLat &&
          weather.coord.lon >= proximity.getLongitude() - deltaLon &&
          weather.coord.lon <= proximity.getLongitude() + deltaLon);
}

/**
 * Finds a campaign by name, whether it is a regular, video, or shopping
 * campaign, by trying all in sequence until it finds one.
 *
 * @param {string} campaignName The campaign name to find.
 * @return {Object} The campaign found, or null if none was found.
 */
function getCampaign(campaignName) {
  var selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
      AdsApp.shoppingCampaigns()];
  for(var i = 0; i < selectors.length; i++) {
    var campaignIter = selectors[i].
        withCondition('CampaignName = "' + campaignName + '"').
        get();
    if (campaignIter.hasNext()) {
      return campaignIter.next();
    }
  }
  return null;
}

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

/**
 * Validates the provided spreadsheet URL to make sure that it's 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 hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'INSERT_SPREADSHEET_URL_HERE') {
    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(spreadsheeturl);
  return spreadsheet;
}

/**
 * Validates the provided API key to make sure that it's not the default. Throws
 * a descriptive error message if validation fails.
 *
 * @throws {Error} If the configured API key hasn't been set.
 */
function validateApiKey() {
  if (OPEN_WEATHER_MAP_API_KEY == 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE') {
    throw new Error('Please specify a valid API key for OpenWeatherMap. You ' +
        'can acquire one here: http://openweathermap.org/appid');
  }
}

Generic

This is a generic script that you can customize to perform weather-based campaign management. Click here for the "Bid-by-weather" script, a specialized script that focuses on weather-based bid management.

The Bid-by-Weather script shows how to manage your campaigns’ bids depending on the weather condition of targeted locations. However, that script only affects bid management, and updating it to perform a different campaign management task (e.g. pausing a group of campaigns) is not entirely trivial. This guide introduces a more generic weather management script that allows you to perform campaign management tasks based on weather information.

Retrieving weather data

The script retrieves weather info from OpenWeatherMap servers. You can request current weather data, or the week’s forecast. You can supply OpenWeatherMap with free form city names:

// Get current weather information for New York City.
var nyWeather = getWeatherForLocation("New York City, US");

// Get the weather forecast for the next week in Toronto.
var torontoWeather = getWeatherForecastForLocation("Toronto, CA");

Understanding the data format

The weather information returned by the getWeatherForLocation() method has the following format:

{
  name: "New York",
  weather: {
    clouds: 48,
    windspeed: 2.66,
    status: {
      summary: "Clouds",
      id: 802,
      description: "scattered clouds"
    },
    snow: 0.0,
    rain: 0.0,
    temperature: 286.16
  },
  country: "United States of America"
}

The various properties may be interpreted as follows:

Field Detail
name Canonical name of the location, as returned by OpenWeatherMap
clouds Cloudiness, as a percentage value.
windspeed Wind speed, in m/s.
snow Snow volume for last 3 hours, mm
rain Rain volume for last 3 hours, mm
summary A summary of the weather. See http://openweathermap.org/weather-conditions for valid values.
description A description of the weather. See http://openweathermap.org/weather-conditions for valid values.
id An ID for the weather condition, as returned by OpenWeathermap. See http://openweathermap.org/weather-conditions for valid values
temperature The temperature in Kelvin scale

Similarly, the weather forecast information returned by the getWeatherForecastForLocation() method has the following format

{
  name: "Toronto",
  country: "CA"
  forecast: {
    "20150416": {
       clouds: 0.0,
       windspeed: 2.06,
       status: {
         summary: "Rain",
         id: 500,
         description: "light rain"
       },
       snow: 0.0,
       rain: 1.89,
       temperature: 0.0
    },
    "20150417": {
      ...
    }
  }
}

The properties in the forecast feed have the same meaning as the weather feed. The extra forecast property is a map, with key being the date in yyyyMMdd format, and value as the weather forecast data for that day. By default, weather forecasts for seven days are returned. The script simplifies the JSON that is returned by the OpenWeatherMap API.

Managing your campaigns

Here are some examples on how you can manage your campaigns based on this weather information:

Current weather

Example 1: Use weather summary provided by OpenWeathermap.

var nyWeather = getWeatherForLocation("New York City, US");
if (nyWeather.weather.status.summary === "Rain") {
  var campaign = AdsApp.campaigns()
      .withCondition("CampaignName = 'Sunny Trips'")
      .get()
      .next();
  campaign.pause();
}

Example 2: Check more specific weather parameters.

var nyWeather = getWeatherForLocation("New York City, US");
if (nyWeather.weather.snow > 5 && nyWeather.weather.temperature < 273) {
  var adGroup = AdsApp.adGroups()
     .withCondition("CampaignName = 'New York Shoes'")
     .withCondition("AdGroupName='Snow boots'")
     .get()
     .next();
  adGroup.bidding().setCpc(adGroup.bidding().getCpc() + 0.3);
}

Weather forecast

Example 1: Use weather summary provided by OpenWeathermap.

var nyWeather = getWeatherForecastForLocation("New York City, US");
var rainyDays = 0;
for (var date in nyWeather.forecast) {
  var forecast = nyWeather.forecast[date];
  if (forecast.status.summary === "Rain") {
    rainyDays = rainyDays + 1;
  }
}

if (rainyDays > 4) {
  var campaign = AdsApp.campaigns()
      .withCondition("CampaignName = 'Sunny Trips'")
      .get()
      .next();
  campaign.pause();
}

Example 2: Check more specific weather parameters.

var nyWeather = getWeatherForecastForLocation("New York City, US");
var coldDays = 0;
for (var date in nyWeather.forecast) {
  var forecast = nyWeather.forecast[date];

  if (forecast.snow > 5 && forecast.temperature < 273) {
    coldDays = coldDays + 1;
  }
}

if (coldDays > 4) {
  var adGroup = AdsApp.adGroups()
      .withCondition("CampaignName = 'New York Shoes'")
      .withCondition("AdGroupName='Snow boots'")
      .get()
      .next();
  adGroup.bidding().setCpc(adGroup.bidding().getCpc() + 0.3);
}

Setup

  • Register for an API key at http://openweathermap.org/appid.
  • Create a new script with the source code below.
  • Update the OPEN_WEATHER_MAP_API_KEY
  • Uncomment the appropriate block of code in the main() method depending on whether you want to run this script on an Advertiser account, or a manager account.
  • Uncomment the appropriate block of code in the processSingleAccount() method depending on whether you want to use current weather information, or weather forecast information.
  • Replace the contents of manageCampaignsBasedOnCurrentWeather() or manageCampaignsBasedOnWeatherForecast() methods with your campaign management logic.
  • Schedule to run as required.

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 Weather Based Campaign Management
 *
 * @overview The Weather Based Campaign management script allows you to perform
 *     various campaign management tasks based on weather information. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/weather-based-campaign-management#generic-weather
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.0
 *
 * @changelog
 * - version 1.0
 *   - Released initial version.
 */

// Register for an API key at http://openweathermap.org/appid
// and enter the key below.
var OPEN_WEATHER_MAP_API_KEY = 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE';

var OPEN_WEATHER_MAP_SERVER_URL = 'http://api.openweathermap.org/data/2.5';
var FORECAST_ENDPOINT = OPEN_WEATHER_MAP_SERVER_URL + '/forecast/daily';
var WEATHER_ENDPOINT = OPEN_WEATHER_MAP_SERVER_URL + '/weather';

function main() {
  // Uncomment the following lines if you are writing code for a single account.
  // processSingleAccount();

  // Uncomment the following lines if you are writing code for an MCC account.
  // var accounts = AdsManagerApp.accounts().withIds(['1234567890', '3456789012']);
  // accounts.executeInParallel("processSingleAccount");
}

/**
 * Process a single account.
 */
function processSingleAccount() {
  // Uncomment this block if you want to do campaign management based on
  // current weather.
  // manageCampaignsBasedOnCurrentWeather();

  // Uncomment this block if you want to do campaign management based on
  // weather forecast.
  // manageCampaignsBasedOnWeatherForecast();
}

/**
 * Manage your campaigns based on current weather. The contents of
 * this method are for your reference; replace it with your campaign
 * management logic.
 */
function manageCampaignsBasedOnCurrentWeather() {
  var nyWeather = getWeatherForLocation('New York City, US');

  // Example 1: Use weather summary provided by OpenWeathermap.
  // See http://openweathermap.org/weather-conditions for more details.
  if (nyWeather.weather.status.summary === 'Rain') {
    // Add your logic here.
  }

  // Example 2: Check more specific weather parameters.
  if (nyWeather.weather.snow > 5 && nyWeather.weather.temperature < 273) {
    // Add your logic here.
  }
}

/**
 * Manage your campaigns based on weather forecast. The contents of
 * this method are for your reference; replace it with your campaign
 * management logic.
 */
function manageCampaignsBasedOnWeatherForecast() {
  var nyWeather = getWeatherForecastForLocation('Toronto, CA');

  // Example 1: Use weather summary provided by OpenWeathermap.
  var rainyDays = 0;
  for (var date in nyWeather.forecast) {
    var forecast = nyWeather.forecast[date];
    if (forecast.status.summary === 'Rain') {
      rainyDays = rainyDays + 1;
    }
  }

  if (rainyDays > 4) {
    // Add your logic here.
  }

  // Example 2: Check more specific weather parameters.
  var coldDays = 0;
  for (var date in nyWeather.forecast) {
    var forecast = nyWeather.forecast[date];

    if (forecast.snow > 5 && forecast.temperature < 273) {
      coldDays = coldDays + 1;
    }
  }

  if (coldDays > 4) {
    // Add your logic here.
  }
}

/**
 * Make a call to the OpenWeatherMap server.
 *
 * @param {string} endpoint the server endpoint.
 * @param {string} location the location for which weather
 *     information is retrieved.
 * @return {Object} the server response.
 */
function callWeatherServer(endpoint, location) {
    var url = Utilities.formatString(
      '%s?APPID=%s&q=%s',
      endpoint,
      encodeURIComponent(OPEN_WEATHER_MAP_API_KEY),
      encodeURIComponent(location));
  var response = UrlFetchApp.fetch(url);
  if (response.getResponseCode() != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s, Location searched: %s.',
        response.getContentText(), location);
  }

  var result = JSON.parse(response.getContentText());

  // OpenWeatherMap's way of returning errors.
  if (result.cod != 200) {
    throw Utilities.formatString(
        'Error returned by API: %s,  Location searched: %s.',
        response.getContentText(), location);
  }
  return result;
}

/**
 * Parse the weather response from the OpenWeatherMap server.
 *
 * @param {Object} weather the weather information from
 *     OpenWeatherMap server.
 * @return {!Object} the parsed weather response.
 */
function parseWeather(weather) {
  var retval = {
    'rain': 0,
    'temperature': 0,
    'windspeed': 0,
    'snow': 0,
    'clouds': 0,
    'status': {
      'id': 0,
      'summary': '',
      'description': ''
    }
  };

  if (weather.rain) {
    if (typeof weather.rain === 'object' && weather.rain['3h']) {
      retval.rain = weather.rain['3h'];
    } else {
      retval.rain = weather.rain;
    }
  }

  if (weather.snow) {
    if (typeof weather.snow === 'object' && weather.snow['3h']) {
      retval.snow = weather.snow['3h'];
    } else {
      retval.snow = weather.snow;
    }
  }

  if (weather.clouds && weather.clouds.all) {
    retval.clouds = weather.clouds.all;
  }

  if (weather.main) {
    retval.temperature = weather.main.temp.toFixed(2);
  } else if (main.temp) {
    retval.temperature = weather.temp.toFixed(2);
  }

  if (weather.wind) {
    retval.windspeed = weather.wind.speed;
  } else if (weather.speed) {
    retval.windspeed = weather.speed;
  }

  if (weather.weather && weather.weather.length > 0) {
    retval.status.id = weather.weather[0].id;
    retval.status.summary = weather.weather[0].main;
    retval.status.description = weather.weather[0].description;
  }
  return retval;
}

/**
 * Get the weather forecast for a location for the next 7 days.
 *
 * @param {string} location the location for which weather
 *     forecast information is retrieved.
 * @return {!Object} the parsed weather response.
 */
function getWeatherForecastForLocation(location) {
  var result = callWeatherServer(FORECAST_ENDPOINT, location);

  var retval = {
    'name': result.city.name,
    'country': result.city.country,
    'forecast': {
    }
  };

  for (var i = 0; i < result.list.length; i++) {
    var forecast = result.list[i];
    var date = formatDate(forecast.dt);
    retval.forecast[date] = parseWeather(forecast);
  }

  return retval;
}

/**
 * Get the current weather information for a location.
 *
 * @param {string} location the location for which weather
 *     information is retrieved.
 * @return {!Object} the parsed weather response.
 */
function getWeatherForLocation(location) {
  var result = callWeatherServer(WEATHER_ENDPOINT, location);

  var retval = {
    'name': result.name,
    'country': result.sys.country,
    'weather': parseWeather(result)
  };

  return retval;
}

/**
 * Formats the date in yyyyMMdd format.
 *
 * @param {Number} dt unix timestamp from OpenWeatherMap server.
 * @return {string} the formatted date.
 */
function formatDate(dt) {
  var date = new Date(dt * 1000);
  return Utilities.formatDate(date, 'GMT', 'yyyyMMdd');
}