24/7 Bidding: Going Mobile With 100% More Shopping!

Former Googler Daniel Gilbert has an improved script to let you adjust your AdWords bids -- desktop and mobile -- every hour of every day.

Chat with SearchBot


Customer behavior changes at different times of day. AdWords ad scheduling, with only six unique bidding windows per day, doesn’t come close to the granularity we need to make our AdWords accounts shine. Most third-party bidding tools only allow two or three bid changes a day.

In response to this, I shared an AdWords script built by the team at Brainlabs (my employer) a few months ago which lets you optimize bids for every hour of the day.

Based on comments from Search Engine Land readers, we’ve beefed up our AdWords script, which you can now use for Google Shopping or regular campaigns. It also lets you change your mobile bid adjustments every hour – currently there is no other way to do this! You can find the script below: all you need to do is copy and paste it into your AdWords account and read the instructions to configure it to fit your needs.

The script works by looking up the bid modifier for each hour from a Google sheet. Make a duplicate of our example spreadsheet and set all the bid multipliers to the values you require. The first sheet in the spreadsheet will correspond to your ad scheduling (applied to all traffic) and your second sheet will correspond to your mobile bid adjustments.

Note that, unlike in the previous version of the script, the bid adjustment you type into the spreadsheet is the same as you would enter into AdWords: 0% means bids will be unchanged, so a $1 bid would stay $1; -10% means bids will be reduced by 10%, so a $1.00 bid would effectively be $0.90. You can also now use the script to turn off advertising at certain times: just put the bid adjustment down as -100%. (You can’t actually have a -100% modifier in an ad schedule, so if there’s a -100% in the ad scheduling sheet the script simply won’t create a schedule for that time slot.)

Also remember that mobile bids will be affected by both the ad scheduling sheet and the mobile sheet: if you have a bid of $1, the ad schedule adjustment is -10% and the mobile bid adjustment is -10%, then the effective bid on mobile would be $0.81 (as the adjustments multiply together to give -19%). To help you keep track of this, there’s an additional table in the mobile sheet for the compounded change. You don’t have to do anything to this table: it will be filled in by formulae.

Once you’ve copied the script into your account you will have to set the following options:

  • Enter the URL for the Google Sheet you used earlier for the spreadsheetUrl, replacing the value currently there.
  • Set the shoppingCampaigns variable to true if you want your script to affect only shopping campaigns. Set shoppingCampaigns to false if you want your script to affect only regular campaigns.
  • Set the runMobileBids variable to true if you want your script to affect the mobile bid adjustments of your campaigns. Set runMobileBids to false if you want your script to leave mobile bid adjustments alone.
  • Enter into the array excludeCampaignNameContains a list of phrases contained in campaign names which you would like to exclude. These have to be in quote marks and separated by commas: for example [“Brand”,”Competitor Names”] to exclude all campaigns with names containing ‘brand’ or ‘competitor names’. Leave blank, [], to not exclude any campaigns.
  • Enter into the array includeCampaignNameContains a list of phrases contained in campaign names which you would like to include. Like excludeCampaignNameContains these should be in quote marks and comma separated: for example [“Brand”,”Competitor Names”] to include only campaigns with names containing ‘brand’ or ‘competitor names’. Leave blank, [], to include all campaigns.

We’ve set the default value of shoppingCampaigns to false, so if you’d like to run this script over Shopping campaigns rather than regular campaigns, make sure to change this variable. If you’d like the script to work over both types of campaigns, then simply duplicate the script and have two instances running in your account.

Once the settings have been configured, you’re ready to set up a schedule for the script: set it up to run hourly.

When you no longer want to use this script, simply set the lastRun variable to true and run the script once. This will remove all of the ad schedules and set all campaign mobile bid adjustments to 0%. Also remember to stop the script’s schedule, so it doesn’t carry on running every hour.

There are a few things to note:

  • Important: For all affected campaigns, this script will delete all existing ad schedules!
  • If you are using the script with mobile bid adjustments, it will overwrite any existing campaign level bid adjustments in the affected campaigns. However it won’t touch ad group level mobile bid adjustments — you’ll need to remove these yourself. (Otherwise, they’ll override the campaign-level adjustments.)
  • The script uses your AdWords account’s time zone to decide what hour it is.
  • Remember, there are limits to the bid adjustments: ad scheduling can’t go higher than +900%, and mobile bid adjustments can’t go higher than 300%. While you can use -100% to represent your campaigns being off (or opting out of mobile) you can’t use adjustments between -100% and -90%.
  • If there’s an illegal bid adjustment (e.g. outside the limits or not a number) the script will default to an adjustment of 0% instead. This will be shown in the script’s log.
  • If there’s a blank cell, the script will assume that means the adjustment should be -100%.
  • This script’s timing is more precise than the previous version’s. A scheduled script could run at any point in the hour; in the previous script, the bid you want for 2:00 p.m. might be applied at any point between 2:00 p.m. and 3:00 p.m. Google’s ad scheduling allows you to change the adjustments exactly on the hour, but has a limited number of windows — so the new script keeps a rolling set of adjustment windows (this hour’s adjustment and the next few hours’ adjustments). Every hour, the script will remove last hour’s bid adjustment, and with the window that frees up it will create an adjustment for the future.
  • This means if you stop running the script, the schedules will stop your ads from showing most of the time. You need to run the script one last time with lastRun set to true, or manually change the ad schedules.
  • However, the above only applies to ad schedules, not mobile bid adjustments — they can only be changed when the script runs. So bear in mind that mobile bid adjustments may not change at exactly the right time! If any smart AdWords developer people have ideas to get around this, you’ve earned yourself a job at Brainlabs. At the moment, we just run the script a few times until it gets assigned a run time close to the hour mark — once it does, this it tends to keep that same time slot.
* Advanced ad scheduling
* This script will apply ad schedules to campaigns or shopping campaigns and set the
* ad schedule bid modifier and mobile bid modifier at each hour according to
* multiplier timetables in a Google sheet.
* Version: 2.0
* brainlabsdigital.com

function main() {
 //The Google sheet to use
 //The default value is the example sheet linked to in the article
 var spreadsheetUrl = "https://docs.google.com/a/brainlabsdigital.com/spreadsheets/d/1JDGBPs2qyGdHd94BRZw9lE9JFtoTaB2AmlL7xcmLx2g/edit#gid=0";
 //Shopping or regular campaigns
 //Use true if you want to run script on shopping campaigns (not regular campaigns).
 //Use false for regular campaigns.
 var shoppingCampaigns = false;
 //Use true if you want to set mobile bid adjustments as well as ad schedules.
 //Use false to just set ad schedules.
 var runMobileBids = true;
 //Optional parameters for filtering campaign names. The matching is case insensitive.
 //Select which campaigns to exclude e.g ["foo", "bar"] will ignore all campaigns
 //whose name contains 'foo' or 'bar'. Leave blank [] to not exclude any campaigns.
 var excludeCampaignNameContains = [];
 //Select which campaigns to include e.g ["foo", "bar"] will include only campaigns
 //whose name contains 'foo' or 'bar'. Leave blank [] to include all campaigns.
 var includeCampaignNameContains = [];
 //When you want to stop running the ad scheduling for good, set the lastRun
 //variable to true to remove all ad schedules and mobile bid modifiers.
 var lastRun = false;
 //Initialise for use later.
 var adScheduleCodes = [];
 var campaignIds = [];
 //Retrieving up hourly data
 var scheduleRange = "B2:H25";
 var accountName = AdWordsApp.currentAccount().getName();
 var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
 var sheets = spreadsheet.getSheets();
 var timeZone = AdWordsApp.currentAccount().getTimeZone();
 var date = new Date();
 var dayOfWeek = parseInt(Utilities.formatDate(date, timeZone, "uu"), 10) - 1;
 var hour = parseInt(Utilities.formatDate(date, timeZone, "HH"), 10);
 var sheet = sheets[0];
 var data = sheet.getRange(scheduleRange).getValues();
 //This hour's bid multiplier.
 var thisHourMultiplier = data[hour][dayOfWeek];
 var lastHourCell = "I2";
 //The next few hours' multipliers
 var timesAndModifiers = [];
 for (var h=0; h<4; h++) {
 var newHour = (hour + h)%24;
 if (hour + h > 23) {
 var newDay = (dayOfWeek + 1)%7;
 } else {
 var newDay = dayOfWeek;
 var bidModifier = data[newHour][newDay];
 if (isNaN(bidModifier) || (bidModifier < -0.9 && bidModifier > -1) || bidModifier > 9) {
 Logger.log("Bid modifier '" + bidModifier + "' for " + weekDays[newDay] + " " + newHour + " is not valid.");
 timesAndModifiers.push([newHour, weekDays[newDay], 0]);
 } else if (bidModifier != -1 && bidModifier.length != 0) {
 timesAndModifiers.push([newHour, weekDays[newDay], bidModifier]);
 if (timesAndModifiers.length === 0){
 // If there are no modifiers then the campaigns should not be running for the next few hours.
 // If we just removed all schedules, then ads would start running (with no modifier)
 // so an arbitrary ad schedule is created (scheduled for yesterday so it does not affect anything).
 //Pull a list of all relevant campaign IDs in the account.
 var campaignSelector = ConstructIterator(shoppingCampaigns)
 for(var i = 0; i < excludeCampaignNameContains.length; i++){
 campaignSelector = campaignSelector.withCondition('Name DOES_NOT_CONTAIN_IGNORE_CASE "' + excludeCampaignNameContains[i] + '"');
 var campaignIterator = campaignSelector.get();
 var campaign = campaignIterator.next();
 var campaignName = campaign.getName();
 var includeCampaign = false;
 if(includeCampaignNameContains.length === 0){
 includeCampaign = true;
 for(var i = 0; i < includeCampaignNameContains.length; i++){
 var index = campaignName.toLowerCase().indexOf(includeCampaignNameContains[i].toLowerCase());
 if(index !== -1){
 includeCampaign = true;
 var campaignId = campaign.getId();
 //Return if there are no campaigns.
 if(campaignIds.length === 0){
 Logger.log("There are no campaigns matching your criteria.");
 //Remove all ad scheduling and mobile bid adjustments for the last run.
 checkAndRemoveAdSchedules(campaignIds, []);
 ModifyMobileBidAdjustment(campaignIds, 0);
 // Change the mobile bid adjustment 
 if (sheets.length < 2) {
 Logger.log("Mobile ad schedule sheet was not found in the Google spreadsheet.");
 } else {
 var sheet = sheets[1];
 var data = sheet.getRange(scheduleRange).getValues();
 var thisHourMultiplier_Mobile = data[hour][dayOfWeek];
 if (thisHourMultiplier_Mobile.length === 0) {
 thisHourMultiplier_Mobile = -1;
 if (isNaN(thisHourMultiplier_Mobile) || (thisHourMultiplier_Mobile < -0.9 && thisHourMultiplier_Mobile > -1) || thisHourMultiplier_Mobile > 3) {
 Logger.log("Mobile bid modifier '" + thisHourMultiplier_Mobile + "' for " + weekDays[dayOfWeek] + " " + hour + " is not valid.");
 thisHourMultiplier_Mobile = 0;
 if(thisHourMultiplier === ""){
 var totalMultiplier = "";
 else if(thisHourMultiplier == -1 || thisHourMultiplier_Mobile == -1){
 var totalMultiplier = -1;
 var totalMultiplier = (1+thisHourMultiplier_Mobile)*(1+thisHourMultiplier) -1;
 ModifyMobileBidAdjustment(campaignIds, thisHourMultiplier_Mobile);
 // Check the existing ad schedules, removing those no longer necessary
 var existingSchedules = checkAndRemoveAdSchedules(campaignIds, timesAndModifiers);
 // Add in the new ad schedules
 AddHourlyAdSchedules(campaignIds, timesAndModifiers, existingSchedules, shoppingCampaigns);

* Function to add ad schedules for the campaigns with the given IDs, unless the schedules are 
* referenced in the existingSchedules array. The scheduling will be added as a hour long periods
* as specified in the passed parameter array and will be given the specified bid modifier.
* @param array campaignIds array of campaign IDs to add ad schedules to
* @param array timesAndModifiers the array of [hour, day, bid modifier] for which to add ad scheduling
* @param array existingSchedules array of strings identifying already existing schedules.
* @param bool shoppingCampaigns using shopping campaigns?
* @return void
function AddHourlyAdSchedules(campaignIds, timesAndModifiers, existingSchedules, shoppingCampaigns){
 // times = [[hour,day],[hour,day]]
 var campaignIterator = ConstructIterator(shoppingCampaigns)
 var campaign = campaignIterator.next();
 for(var i = 0; i < timesAndModifiers.length; i++){
 if (existingSchedules.indexOf(
 timesAndModifiers[i][0] + "|" + (timesAndModifiers[i][0]+1) + "|" + timesAndModifiers[i][1]
 + "|" + Utilities.formatString("%.2f",(timesAndModifiers[i][2]+1)) + "|" + campaign.getId())
 > -1) {
 dayOfWeek: timesAndModifiers[i][1],
 startHour: timesAndModifiers[i][0],
 startMinute: 0,
 endHour: timesAndModifiers[i][0]+1,
 endMinute: 0,
 bidModifier: 1+timesAndModifiers[i][2]

* Function to remove ad schedules from all campaigns referenced in the passed array
* which do not correspond to schedules specified in the passed timesAndModifiers array.
* @param array campaignIds array of campaign IDs to remove ad scheduling from
* @param array timesAndModifiers array of [hour, day, bid modifier] of the wanted schedules
* @return array existingWantedSchedules array of strings identifying the existing undeleted schedules
function checkAndRemoveAdSchedules(campaignIds, timesAndModifiers) {
 var adScheduleIds = [];
 var report = AdWordsApp.report(
 'SELECT CampaignId, Id ' + 
 'WHERE CampaignId IN ["' + campaignIds.join('","') + '"]'
 var rows = report.rows();
 var row = rows.next();
 var adScheduleId = row['Id'];
 var campaignId = row['CampaignId'];
 var chunkedArray = [];
 var chunkSize = 10000;
 for(var i = 0; i < adScheduleIds.length; i += chunkSize){
 chunkedArray.push(adScheduleIds.slice(i, i + chunkSize));
 var wantedSchedules = [];
 var existingWantedSchedules = [];
 for (var j=0; j<timesAndModifiers.length; j++) {
 wantedSchedules.push(timesAndModifiers[j][0] + "|" + (timesAndModifiers[j][0]+1) + "|" + timesAndModifiers[j][1] + "|" + Utilities.formatString("%.2f",timesAndModifiers[j][2]+1));
 for(var i = 0; i < chunkedArray.length; i++){
 var unwantedSchedules = [];
 var adScheduleIterator = AdWordsApp.targeting()
 while (adScheduleIterator.hasNext()) {
 var adSchedule = adScheduleIterator.next();
 var key = adSchedule.getStartHour() + "|" + adSchedule.getEndHour() + "|" + adSchedule.getDayOfWeek() + "|" + Utilities.formatString("%.2f",adSchedule.getBidModifier());
 if (wantedSchedules.indexOf(key) > -1) {
 existingWantedSchedules.push(key + "|" + adSchedule.getCampaign().getId());
 } else {
 for(var j = 0; j < unwantedSchedules.length; j++){
 return existingWantedSchedules;

* Function to construct an iterator for shopping campaigns or regular campaigns.
* @param bool shoppingCampaigns Using shopping campaigns?
* @return AdWords iterator Returns the corresponding AdWords iterator
function ConstructIterator(shoppingCampaigns){
 if(shoppingCampaigns === true){
 return AdWordsApp.shoppingCampaigns();
 return AdWordsApp.campaigns();

* Function to set a mobile bid modifier for a set of campaigns
* @param array campaignIds An array of the campaign IDs to be affected
* @param Float bidModifier The multiplicative mobile bid modifier
* @return void
function ModifyMobileBidAdjustment(campaignIds, bidModifier){
 var platformIds = [];
 for(var i = 0; i < campaignIds.length; i++){
 var platformIterator = AdWordsApp.targeting()
 while (platformIterator.hasNext()) {
 var platform = platformIterator.next();

Opinions expressed in this article are those of the guest author and not necessarily Search Engine Land. Staff authors are listed here.

About the author

Daniel Gilbert
Daniel Gilbert is the CEO at Brainlabs, the best paid media agency in the world (self-declared). He has started and invested in a number of big data and technology startups since leaving Google in 2010.

Get the must-read newsletter for search marketers.