Cheap VPS & Xen Server

Residential Proxy Network - Hourly & Monthly Packages

Here’s a script that copies AdWords extensions to all your campaigns

Here’s a script that copies AdWords extensions to all your campaigns

You might not usually think of shared negative lists and campaign extensions together, because their uses are so different: lists are used for targeting, while extensions spruce up your ads.

But if you look at them with AdWords Scripts, they’re actually pretty similar — they’re both objects that exists on their own and then get attached to campaigns. And, for both of them, it’s a pain to make sure they’re attached to all the campaigns you want.

And where there’s pain, there’s an opportunity for automation.

Last month, I shared a script to make sure shared lists were applied to all campaigns. But because extensions and lists are pretty similar in AdWords Scripts, the tech team at Brainlabs (my employer) has expanded the code so it can copy extensions as well!

As well as meaning you have coverage with all your extensions, it also means you’re always using the same extensions — you won’t accidentally get one campaign using a sitelink with the same text but a different URL. You’ll know that updating an extension updates it in all campaigns, because it’s definitely the same extension attached to all campaigns.

Like the shared list script, it won’t remove anything — so you can add extra specific extensions to some campaigns and they won’t be removed if you do another run later.

If you want to give it a go, copy the code below into a new AdWords Script in your account. Then change some of the options:

  • Filter the campaigns things will be applied to with the two arrays, campaignNameContains and campaignNameDoesNotContain. For example if campaignNameContains is [“Brand”, “Generic”] then only campaigns with names containing “brand” or “generic” are included. If campaignNameDoesNotContain is [“Display”, “Competitor”] then any campaigns with names containing “display” or “competitor” are ignored.
    • This is not case-sensitive.
    • Leave blank, [], to not exclude any campaigns.
    • If you need to put a double quote into campaignNameContains or campaignNameDoesNotContain, put a backslash before it.
  • If ignorePausedCampaigns is true, then the script will only look at currently active campaigns. Set this to false if you want to apply extensions and lists to currently paused campaigns.
  • campaignToCopy is the name of the template campaign, whose lists and extensions will be copied. This is case-sensitive.
    • The template campaign can be paused, but it can’t be removed.
  • extensionsAndLists is an array of which types of extension and list the Script will copy. The possible values are “sitelinks”, “callouts”, “reviews”, “mobileApps” (app extensions), “phoneNumbers” (call extensions), “excludedPlacementLists” and “negativeKeywordLists”.
  • labelName is the name of the label which will be applied to campaigns once they’ve had the extensions/lists added. This means you can see which campaigns have been covered. Campaigns that already have the label will be ignored: if the Script doesn’t cover them all in one run it can go again until it covers them all.

You may want to run the script more than once for different extensions and lists. For example, if your account is split by match type, you may want to run once for extensions (as you probably want the same extensions on Exact and Broad campaigns), but then a second run for negative keyword lists that ignores Exact campaigns (by adding to campaignNameDoesNotContain “-Exact” or “|EM” or whatever it is you use to denote a campaign is Exact).

Make sure you change the labelName, otherwise the second run won’t add lists to the campaigns you just added extensions to. And think about saving both versions of the settings in a text file so you can repeat this later after you’ve added more campaigns!


  • This only works on Search, Display and Search with Display Select campaigns.
  • The script can’t tell the difference between Search, Display and Search with Display Select campaigns. You have to use the campaign names to make sure negative lists intended for Search don’t end up in Display campaigns.
  • The Script doesn’t do anything with ad group extensions or account extensions – only campaign level ones.
  • Scripts can only run for 30 minutes, which may not be enough if you’ve got lots of campaigns or lots of extensions. But if the script times out, you can just run it again – it will say in the logs when everything has been covered.
* Extension and List Copying
* This script takes the ad extensions, shared campaign negative lists and excluded
* placement lists applied to one template campaign and applies them to all other
* campaigns that match the filters. Campaigns are then labelled.
* Version: 1.0
* Google AdWords Script maintained on
var campaignNameDoesNotContain = [];
// Use this if you want to exclude some campaigns.
// For example [“Display”] would ignore any campaigns with ‘Display’ in the name,
// while [“Display”,”Competitors”] would ignore any campaigns with ‘Display’ or
// ‘Competitors’ in the name. Case insensitive.
// Leave as [] to not exclude any campaigns.
var campaignNameContains = [];
// Use this if you only want to look at some campaigns.
// For example [“Brand”] would only look at campaigns with ‘Brand’ in the name,
// while [“Brand”,”Generic”] would only look at campaigns with ‘Brand’ or ‘Generic’
// in the name. Case insensitive.
// Leave as [] to include all campaigns.
var ignorePausedCampaigns = false;
// Set this to true to only look at currently active campaigns.
// Set to false to also include campaigns that are currently paused.
// This is the name of the template campaign which has the correct lists already applied.
// All lists shared with this campaign will be shared with the other campaigns.
// Case sensitive!
var extensionsAndLists = [sitelinks, callouts, reviews, mobileApps, phoneNumbers, excludedPlacementLists, negativeKeywordLists];
// Which extensions and lists to copy.
// Possible values: “sitelinks”, “callouts”, “reviews”, “mobileApps”, “phoneNumbers”,
// “excludedPlacementLists”, “negativeKeywordLists”
// “mobileApps” are app extensions
// “phoneNumbers” are call extensions.
var labelName = Extensions and shared lists done;
// Once a campaign has had all the lists added, it will be labelled with this.
function main() {
// Make sure the array of extensions and lists has entries,
// that they’re all recognised extensions/lists,
// and that they’re capitalised correctly
if (extensionsAndLists.length == 0) {
throw(No extension or list types entered. Please have at least one in the extensionsAndLists array.);
var allowedExtensionsAndLists = [sitelinks, callouts, reviews, mobileApps, phoneNumbers, excludedPlacementLists, negativeKeywordLists];
extensionsAndLists = checkListOfNames(allowedExtensionsAndLists, extensionsAndLists, extension(s)/list(s));
// Get the lists shared with the template campaign
var templateCampaigns = AdWordsApp.campaigns()
.withCondition(CampaignName = “ + campaignToCopy.replace(//g,\\\”) + )
.withCondition(CampaignStatus IN [ENABLED, PAUSED])
if (!templateCampaigns.hasNext()) {
throw(No template campaign called ‘ + campaignToCopy + ‘ found.);
// There should be precisely one campaign in the iterator, because there should be
// precisely one template campaign to look at. So we don’t use a while loop, we just
// look at the first campaign in the iterator.
var templateCampaign =;
var objectsToAdd = [];
for (var i=0; i<extensionsAndLists.length; i++) {
if (extensionsAndLists[i] == excludedPlacementLists || extensionsAndLists[i] == negativeKeywordLists) {
var iterator = templateCampaign[extensionsAndLists[i]]().get();
} else {
var iterator = templateCampaign.extensions()[extensionsAndLists[i]]().get();
objectsToAdd[i] = [];
while (iterator.hasNext()) {
var extension =
var totalObjectsToAdd = 0;
for (var i=0; i<extensionsAndLists.length; i++) {
Logger.log(objectsToAdd[i].length + + extensionsAndLists[i] + found);
totalObjectsToAdd += objectsToAdd[i].length;
if (totalObjectsToAdd == 0) {
throw(No + extensionsAndLists.join(, ) + found to copy. Please check they are applied to template campaign ‘ + campaignToCopy + ‘.);
// Get all the campaign IDs (based on campaignNameDoesNotContain, campaignNameContains
// and ignorePausedCampaigns options).
// This ignores labelling – if there are no campaigns it must be because the options
// are set incorrectly, so it throws an error.
var campaignIds = getCampaignIds();
// Find or create the campaign label
var labels = AdWordsApp.labels().withCondition(Name = ‘ + labelName + ).get();
if (!labels.hasNext()) {
// If the label does not exist, we create it
labels = AdWordsApp.labels().withCondition(Name = ‘ + labelName + ).get();
if (AdWordsApp.getExecutionInfo().isPreview() && !labels.hasNext()) {
// We can’t create labels when previewing scripts, so if this is a preview run
// and the label still doesn’t exist we use a dummy value for the ID
// (as we know nothing can be labelled with the non-existent label anyway)
var labelId = 0;
} else {
var labelId =;
var addMethod = {};
addMethod[sitelinks] = addSitelink;
addMethod[callouts] = addCallout;
addMethod[reviews] = addReview;
addMethod[mobileApps] = addMobileApp;
addMethod[phoneNumbers] = addPhoneNumber;
addMethod[excludedPlacementLists] = addExcludedPlacementList;
addMethod[negativeKeywordLists] = addNegativeKeywordList;
// Make an iterator of the campaigns that match the settings and are not labelled
var campaigns = AdWordsApp.campaigns()
.withCondition(CampaignId IN [ + campaignIds.join(,) + ])
.withCondition(Labels CONTAINS_NONE [ + labelId + ])
var campaignCount = 0;
// Go through each campaign and apply the lists
while (campaigns.hasNext()) {
var campaign =;
for (var i=0; i<extensionsAndLists.length; i++) { // Loop over the types of list
for (var j=0; j< objectsToAdd[i].length; j++) { // Loop over the lists to add
campaign[addMethod[extensionsAndLists[i]]](objectsToAdd[i][j]); // Add the list
campaign.applyLabel(labelName); // Label the campaign now the lists have been applied
if (campaignCount%100 == 0) {
Logger.log(Applied lists to + campaignCount + campaigns so far);
if (campaignCount == 0) {
Logger.log(campaignIds.length + campaigns match the settings, but all were labelled with ‘ + labelName + ‘. This suggests the extensions/lists have been applied to everything.);
} else {
Logger.log(Finished. Extensions/lists applied to + campaignCount + campaigns.);
// Get the IDs of campaigns which match the given options
function getCampaignIds() {
var whereStatement = WHERE ;
var whereStatementsArray = [];
var campaignIds = [];
if (ignorePausedCampaigns) {
whereStatement += CampaignStatus = ENABLED ;
} else {
whereStatement += CampaignStatus IN [‘ENABLED’,’PAUSED’] ;
for (var i=0; i<campaignNameDoesNotContain.length; i++) {
whereStatement += AND CampaignName DOES_NOT_CONTAIN_IGNORE_CASE ‘ + campaignNameDoesNotContain[i].replace(//g,\\\”) + ;
if (campaignNameContains.length == 0) {
whereStatementsArray = [whereStatement];
} else {
for (var i=0; i<campaignNameContains.length; i++) {
whereStatementsArray.push(whereStatement + AND CampaignName CONTAINS_IGNORE_CASE “ + campaignNameContains[i].replace(//g,\\\”) + );
for (var i=0; i<whereStatementsArray.length; i++) {
var adTextReport =
SELECT CampaignId +
whereStatementsArray[i] +
var rows = adTextReport.rows();
while (rows.hasNext()) {
var row =;
if (campaignIds.length == 0) {
throw(No campaigns found with the given settings.);
return campaignIds;
// Verify that all field names are valid, and return a deduped list of them
// with the correct capitalisation
function checkListOfNames(allowedFields, givenFields,fieldName) {
var allowedFieldsLowerCase = (str){return str.toLowerCase()});
var wantedFields = {};
var unrecognisedFields = [];
for (var i=0; i<givenFields.length; i++) {
var fieldIndex = allowedFieldsLowerCase.indexOf(givenFields[i].toLowerCase().replace( ,).trim());
if (fieldIndex === 1) {
// Try with an ‘s’ on the end
fieldIndex = allowedFieldsLowerCase.indexOf(givenFields[i].toLowerCase().replace( ,).trim() + s);
if(fieldIndex === 1){
} else {
wantedFields[allowedFields[fieldIndex]] = true;
if (unrecognisedFields.length > 0) {
throw unrecognisedFields.length + + fieldName + not recognised: ‘ + unrecognisedFields.join(‘, ‘) +
‘. Please choose from ‘ + allowedFields.join(‘, ‘) + ‘.;

return Object.keys(wantedFields);