Flexible Budgets - Single Account

Tools icon

Google Ads lets you set a daily budget amount for each campaign. However, some marketing initiatives will have a fixed cost associated with them; for example, "I want to spend $5000 leading up to our fall sale". The bidding strategy gives you some control over how the daily budget is spent, but no control over how the budget is consumed during the campaign.

For example, if we want to spend only $5000 to advertise our fall sale and we want to advertise for 10 days, we could set a daily budget of $500 to use up the entire budget. However, this assumes that we will spend the entire amount each day AND we wish to spend it evenly. It's not possible to tell Google Ads that you want to spend the bulk of your budget during the last few days.

This script will dynamically adjust your campaign budget daily with a custom budget distribution scheme.

How it works

Testing budget strategies

The script includes some test code to simulate the effects of running for multiple days. This gives you a better idea of what might happen when the script is scheduled to run daily over a period of time.

By default, this script simulates an even budget distribution of $500 spent over 10 days.

function main() {
  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  // setNewBudget(calculateBudgetEvenly, CAMPAIGN_NAME, TOTAL_BUDGET, START_DATE, END_DATE);
}

The setNewBudget function call is commented out, indicating that it'll only run the test code. Here is the output from the example:

Day 1.0 of 10.0, new budget 50.0, cost so far 0.0
Day 2.0 of 10.0, new budget 50.0, cost so far 50.0
Day 3.0 of 10.0, new budget 50.0, cost so far 100.0
Day 4.0 of 10.0, new budget 50.0, cost so far 150.0
Day 5.0 of 10.0, new budget 50.0, cost so far 200.0
Day 6.0 of 10.0, new budget 50.0, cost so far 250.0
Day 7.0 of 10.0, new budget 50.0, cost so far 300.0
Day 8.0 of 10.0, new budget 50.0, cost so far 350.0
Day 9.0 of 10.0, new budget 50.0, cost so far 400.0
Day 10.0 of 10.0, new budget 50.0, cost so far 450.0
Day 11.0 of 10.0, new budget 0.0, cost so far 500.0

Each day the script calculates a new budget to make sure that budget spend is evenly distributed. When the allotted budget limit is reached, the budget is set to zero, halting spend.

You can change the budget strategy used by changing which function is used, or modifying the function itself. The script comes with two pre-built strategies: calculateBudgetEvenly and calculateBudgetWeighted. To set a weighted test budget strategy, change testBudgetStrategy like so:

testBudgetStrategy(calculateBudgetWeighted, 10, 500);

Click Preview and check the logger output. Notice that this budget strategy allocates less budget early in the period and more during the last few days.

You can use this test method to simulate changes to the budget calculation functions and try your own approach to distributing a budget.

Allocating a budget

The calculateBudgetWeighted budget strategy is implemented through the following function:

function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1) ;
  }
}

This function takes these arguments:

costSoFar
Campaign's accrued cost from START_DATE to today.
totalBudget
Allocated spend from START_DATE to END_DATE.
daysSoFar
Days elapsed from START_DATE to today.
totalDays
Total number of days between START_DATE and END_DATE.

You can write your own function as long as it takes these arguments. Using these values, you can compare how much money you've spent so far against how much to spend overall and determine where you currently are within the timeline for the entire budget.

In particular, this budget strategy figures out how much budget remains (totalBudget - costSoFar) and divides that by twice the number of days remaining. This weighs the budget distribution towards the end of the campaign. By using the cost since START_DATE, it also takes into account "slow days" where the set budget is not entirely spent.

Budgeting for real

Once you're happy with your budget strategy, you need to make a few changes before scheduling this script to run daily.

First, update the constants at the top of the file:

  • START_DATE: Set this to the start of your budget strategy. This should be the current date or a day in the past.
  • END_DATE: Set this to the last day you want to advertise with this budget.
  • TOTAL_BUDGET: The total amount you're trying to spend. This value is in account currency and might be exceeded depending on when the script is scheduled to run.
  • CAMPAIGN_NAME: The name of the campaign to apply the budget strategy to.

Next, disable the test and enable the logic to actually change the budget:

function main() {
  // testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  setNewBudget(calculateBudgetWeighted, CAMPAIGN_NAME, TOTAL_BUDGET, START_DATE, END_DATE);
}

Scheduling

Schedule this script to run daily, at or shortly after midnight in the local timezone so as to direct as much as possible the upcoming day's budget. Note, however, that retrieved reports data such as cost could be delayed by about 3 hours, so the costSoFar parameter might be referencing yesterday's total for a script that is scheduled to run after midnight.

Setup

  • Click the button below to create the script in your Google Ads account.

    Install the script template

  • Save the script and click the Preview button. This script (by default) simulates a budget strategy with $500 over 10 days. The logger output reflects the day being simulated, the allocated budget for that day, and the total amount spent to date.

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 Flexible Budgets
 *
 * @overview The Flexible budgets script dynamically adjusts campaign budget for
 *     an advertiser account with a custom budget distribution scheme on a daily
 *     basis. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/flexible-budgets
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.1
 *
 * @changelog
 * - version 2.1
 *   - Split into info, config, and code.
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0.3
 *   - Add support for video and shopping campaigns.
 * - version 1.0.2
 *   - Use setAmount on the budget instead of campaign.setBudget.
 * - version 1.0.1
 *   - Improvements to time zone handling.
 * - version 1.0
 *   - Released initial version.
 */

/**
 * Configuration to be used for the Flexible Budgets script.
 */

CONFIG = {
  'total_budget': 500,
  'campaign_name': 'Special Promotion',
  'start_date': 'November 1, 2021 0:00:00 -0500',
  'end_date': 'December 1, 2021 0:00:00 -0500'
};

const TOTAL_BUDGET = CONFIG.total_budget;
const CAMPAIGN_NAME = CONFIG.campaign_name;
const START_DATE = new Date(CONFIG.start_date);
const END_DATE = new Date(CONFIG.end_date);

function main() {
  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
//  setNewBudget(calculateBudgetEvenly, CAMPAIGN_NAME, TOTAL_BUDGET,
//      START_DATE, END_DATE);
}

function setNewBudget(budgetFunction, campaignName, totalBudget, start, end) {
  const today = new Date();
  if (today < start) {
    console.log('Not ready to set budget yet');
    return;
  }
  const campaign = getCampaign(campaignName);
  const costSoFar = campaign.getStatsFor(
        getDateStringInTimeZone('yyyyMMdd', start),
        getDateStringInTimeZone('yyyyMMdd', end)).getCost();
  const daysSoFar = datediff(start, today);
  const totalDays = datediff(start, end);
  const newBudget = budgetFunction(costSoFar, totalBudget, daysSoFar,
                                   totalDays);
  campaign.getBudget().setAmount(newBudget);
}

function calculateBudgetEvenly(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / daysRemaining;
  }
}

function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar,
    totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1);
  }
}

function testBudgetStrategy(budgetFunc, totalDays, totalBudget) {
  let daysSoFar = 0;
  let costSoFar = 0;
  while (daysSoFar <= totalDays + 2) {
    const newBudget = budgetFunc(costSoFar, totalBudget, daysSoFar, totalDays);
    console.log(`Day ${daysSoFar + 1} of ${totalDays}, new budget ` +
                `${newBudget}, cost so far ${costSoFar}`);
    costSoFar += newBudget;
    daysSoFar += 1;
  }
}

/**
 * Returns number of days between two dates, rounded up to nearest whole day.
 */
function datediff(from, to) {
  const millisPerDay = 1000 * 60 * 60 * 24;
  return Math.ceil((to - from) / millisPerDay);
}

function getDateStringInTimeZone(format, date, timeZone) {
  date = date || new Date();
  timeZone = timeZone || AdsApp.currentAccount().getTimeZone();
  return Utilities.formatDate(date, timeZone, format);
}

/**
 * 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) {
  const selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
      AdsApp.shoppingCampaigns()];
  for (const selector of selectors) {
    const campaignIter = selector
        .withCondition(`CampaignName = "${campaignName}"`)
        .get();
    if (campaignIter.hasNext()) {
      return campaignIter.next();
    }
  }
  throw new Error(`Could not find specified campaign: ${campaignName}`);
}