// tools

Free Google Ads Scripts

Niklas BuschnerFounder & CEO
Browse the scriptsAll free, paste-and-go

Three free Google Ads scripts we use in client work — auto-add converting search terms as keywords, track MCC monthly spend, and exclude non-brand queries from branded Shopping campaigns.

Use these free Google Ads scripts to automate the repetitive work — adding converting search terms as exact-match keywords, reporting MCC-level monthly spend, and keeping branded Shopping campaigns clean. Same scripts we use in client work, copy-paste-ready.

How to install any of them

The setup pattern is the same for all three scripts:

  1. In Google Ads, go to Tools → Bulk Actions → Scripts.
  2. Click the blue + button to create a new script.
  3. Give it a descriptive name.
  4. Copy the entire script code below into the editor.
  5. Update the placeholder values at the top (spreadsheet URL, campaign ID, brand keywords — depending on the script).
  6. Click Save, then Authorize and follow the Google authorization prompts.
  7. Run a preview to check for issues.
  8. Either run manually when needed, or click Frequency to schedule (suggested cadence noted per script).

Search Terms → Keywords automation

Reads rules from a Google Sheet and automatically adds high-performing search terms as exact-match keywords to the campaigns you specify. Pairs perfectly with dynamic keyword insertion.

Use cases

  • Automatically convert converting search terms into keywords without manual review.
  • Pair with DKI ads so new winning queries instantly become explicit keywords.
  • Only promote terms that meet your CPA / CVR / ROAS / conversions thresholds.
  • Manage all controls in a single Google Sheet — one row per campaign rule, multiple metrics treated as AND.

Setup

  1. Make a copy of the Google Sheet template.
  2. In the Campaign Rules tab, enter one row per campaign with the metrics you care about. Empty cells are ignored.
  3. In the script below, replace YOUR_SPREADSHEET_URL near the top with your sheet URL.
  4. Recommended frequency: daily.

Script

/*
  Built by Niklas Buschner @ Radyant (https://radyant.io)
*/

function main() {
  const SPREADSHEET_URL = 'YOUR_SPREADSHEET_URL'; // CREATE A COPY OF THE GOOGLE SHEET TEMPLATE
  const SHEET_NAME = 'Campaign Rules';

  Logger.log('=== Script Start ===');

  try {
    const sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME);
    const rules = getRulesFromSheet(sheet);

    Logger.log('Found ' + rules.length + ' rules to process');

    rules.forEach((rule, index) => {
      Logger.log('\n=== Processing Rule ' + (index + 1) + ' ===');
      Logger.log('Campaign ID: ' + rule.campaignId);
      Logger.log('Timeframe: ' + rule.timeframe + ' days');
      Logger.log('Metrics:');
      Object.keys(rule.metrics).forEach(metric => {
        Logger.log('- ' + metric + ': ' + rule.metrics[metric]);
      });

      processRule(rule);
    });
  } catch (error) {
    Logger.log('Error in main execution: ' + error);
  }

  Logger.log('=== Script End ===');
}

function getRulesFromSheet(sheet) {
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  const rules = [];

  const columnIndexes = {
    campaignId: headers.indexOf('Campaign ID'),
    timeframe: headers.indexOf('Time Frame'),
    conversions: headers.indexOf('Conversions'),
    costPerConversion: headers.indexOf('CPA'),
    roas: headers.indexOf('ROAS'),
    conversionRate: headers.indexOf('CVR')
  };

  if (columnIndexes.campaignId === -1) {
    throw new Error('Campaign ID column not found in sheet');
  }

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const rule = {
      campaignId: row[columnIndexes.campaignId],
      timeframe: row[columnIndexes.timeframe] || 30,
      metrics: {}
    };

    if (columnIndexes.conversions !== -1 && row[columnIndexes.conversions]) {
      rule.metrics.conversions = row[columnIndexes.conversions];
    }
    if (columnIndexes.costPerConversion !== -1 && row[columnIndexes.costPerConversion]) {
      rule.metrics.costPerConversion = row[columnIndexes.costPerConversion];
    }
    if (columnIndexes.roas !== -1 && row[columnIndexes.roas]) {
      rule.metrics.roas = row[columnIndexes.roas];
    }
    if (columnIndexes.conversionRate !== -1 && row[columnIndexes.conversionRate]) {
      rule.metrics.conversionRate = row[columnIndexes.conversionRate];
    }

    if (Object.keys(rule.metrics).length > 0) {
      rules.push(rule);
    }
  }

  return rules;
}

function getStartDate(timeframe) {
  const date = new Date();
  date.setDate(date.getDate() - timeframe);
  return Utilities.formatDate(date, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');
}

function processRule(rule) {
  Logger.log('\n=== Processing Campaign ===');

  const campaign = AdsApp.campaigns()
    .withIds([rule.campaignId])
    .get()
    .next();

  Logger.log('Campaign Name: ' + campaign.getName());

  const startDate = getStartDate(rule.timeframe);
  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);
  const endDate = Utilities.formatDate(yesterday, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');

  Logger.log('Date Range: ' + startDate + ' to ' + endDate);

  const report = AdsApp.report(
    `SELECT
      Query,
      KeywordTextMatchingQuery,
      AdGroupId,
      AdGroupName,
      Conversions,
      CostPerConversion,
      ConversionRate,
      ValuePerConversion
    FROM SEARCH_QUERY_PERFORMANCE_REPORT
    WHERE
      CampaignId = ${campaign.getId()}
      AND Conversions > 0
      AND Date >= '${startDate}'
      AND Date <= '${endDate}'`
  );

  Logger.log('\n=== Analyzing Search Terms ===');
  const rows = report.rows();
  const processedQueries = new Set();
  const qualifyingTerms = [];

  while (rows.hasNext()) {
    const row = rows.next();
    const searchTerm = row['Query'];
    const triggeringKeyword = row['KeywordTextMatchingQuery'];

    Logger.log('\nAnalyzing search term:');
    Logger.log('- Search Term: ' + searchTerm);
    Logger.log('- Triggered by keyword: ' + triggeringKeyword);
    Logger.log('- Ad Group: ' + row['AdGroupName']);

    if (processedQueries.has(searchTerm)) {
      Logger.log('↪ Skipping duplicate search term');
      continue;
    }

    if (meetsAllCriteria(row, rule.metrics)) {
      Logger.log('✓ Search term meets criteria');
      qualifyingTerms.push({
        query: searchTerm,
        adGroupId: row['AdGroupId'],
        adGroupName: row['AdGroupName']
      });
      processedQueries.add(searchTerm);
    } else {
      Logger.log('✗ Search term does not meet criteria');
    }
  }

  Logger.log('\n=== Processing Qualifying Terms ===');
  Logger.log('Found ' + qualifyingTerms.length + ' qualifying terms');

  const existingKeywords = getCampaignExactMatchKeywords(campaign);

  qualifyingTerms.forEach((termData, index) => {
    Logger.log('\n--- Term ' + (index + 1) + ' of ' + qualifyingTerms.length + ' ---');
    addExactMatchKeyword(campaign, termData, existingKeywords);
  });
}

function getCampaignExactMatchKeywords(campaign) {
  const existingKeywords = new Set();
  const keywordIterator = campaign.keywords()
    .withCondition('KeywordMatchType = EXACT')
    .get();

  while (keywordIterator.hasNext()) {
    const keyword = keywordIterator.next();
    const keywordText = keyword.getText().replace(/^\[|\]$/g, '').toLowerCase();
    existingKeywords.add(keywordText);
  }

  Logger.log(`Found ${existingKeywords.size} existing exact match keywords in campaign`);
  return existingKeywords;
}

function meetsAllCriteria(row, metrics) {
  if (metrics.conversions && parseFloat(row['Conversions']) < metrics.conversions) {
    return false;
  }
  if (metrics.costPerConversion && parseFloat(row['CostPerConversion']) > metrics.costPerConversion) {
    return false;
  }
  if (metrics.roas) {
    const roas = parseFloat(row['ValuePerConversion']) / parseFloat(row['CostPerConversion']);
    if (roas < metrics.roas) return false;
  }
  if (metrics.conversionRate && parseFloat(row['ConversionRate']) < metrics.conversionRate) {
    return false;
  }
  return true;
}

function addExactMatchKeyword(campaign, searchTermData, existingKeywords) {
  Logger.log('Checking keyword for search term: ' + searchTermData.query);
  Logger.log('Target Ad Group: ' + searchTermData.adGroupName);

  const adGroupIterator = campaign.adGroups()
    .withIds([searchTermData.adGroupId])
    .get();

  if (!adGroupIterator.hasNext()) {
    Logger.log('Ad group not found with ID: ' + searchTermData.adGroupId);
    return;
  }

  const adGroup = adGroupIterator.next();
  const proposedKeyword = '[' + searchTermData.query + ']';

  const searchTermWithoutBrackets = searchTermData.query.toLowerCase();
  if (existingKeywords.has(searchTermWithoutBrackets)) {
    Logger.log('✗ Exact match keyword already exists in campaign - skipping');
    Logger.log('Found existing keyword: ' + proposedKeyword);
    return;
  }

  try {
    Logger.log('✓ Adding new keyword: ' + proposedKeyword);
    adGroup.newKeywordBuilder()
      .withText(proposedKeyword)
      .build();
    Logger.log(`Added new exact match keyword [${searchTermData.query}] to ad group ${adGroup.getName()} in campaign ${campaign.getName()}`);
  } catch (error) {
    Logger.log('✗ Error adding keyword: ' + error);
  }
}

MCC monthly spend report

Runs at MCC level and writes the previous month's spend per account into a Google Sheet — account ID, account name, and total spend.

Use cases

  • Track spending across all client accounts in one place.
  • Spot accounts with unusual spend patterns at a glance.
  • Automate monthly billing + budget reporting across the portfolio.

Setup

  1. Create an empty Google Sheet and copy its URL.
  2. In the script below, replace ADD YOUR URL OF AN EMPTY GOOGLE SHEET HERE with that URL.
  3. Recommended frequency: monthly, on the 1st.

Script

/*
  Built by Niklas Buschner @ Radyant (https://radyant.io)
*/

function main() {
  var SPREADSHEET_URL = "ADD YOUR URL OF AN EMPTY GOOGLE SHEET HERE";
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet = spreadsheet.getActiveSheet();

  var headers = sheet.getRange(1, 1, 1, 4).getValues();
  if (
    headers[0][0] !== 'Month' ||
    headers[0][1] !== 'Account ID' ||
    headers[0][2] !== 'Account Name' ||
    headers[0][3] !== 'Spend'
  ) {
    sheet.appendRow(['Month', 'Account ID', 'Account Name', 'Spend']);
  }

  var date = new Date();
  var currentMonth = date.getMonth();
  var lastMonthDate = new Date(date);
  lastMonthDate.setMonth(currentMonth - 1);
  var sheetMonth = Utilities.formatDate(lastMonthDate, "GMT", "MMMM-yyyy");

  var accountIterator = AdsManagerApp.accounts().get();
  while (accountIterator.hasNext()) {
    var account = accountIterator.next();
    AdsManagerApp.select(account);

    var report = AdsApp.report(
      'SELECT ExternalCustomerId, Cost, AccountDescriptiveName ' +
      'FROM ACCOUNT_PERFORMANCE_REPORT ' +
      'DURING LAST_MONTH'
    );

    var rows = report.rows();
    while (rows.hasNext()) {
      var row = rows.next();
      var customerID = row['ExternalCustomerId'];
      var accountName = row['AccountDescriptiveName'];
      var cost = parseFloat(row['Cost'].replace(/,/g, ''));

      Logger.log('Processing account: ' + customerID + ' (' + accountName + '), Cost: ' + cost);

      if (cost > 0) {
        sheet.appendRow([sheetMonth, customerID, accountName, cost]);
      }
    }
  }
}

Exclude non-brand queries from a branded Shopping campaign

Negatives non-brand search terms in a Standard Shopping campaign so it only "targets" branded queries — perfect for the PMax brand / non-brand split.

Use cases

  • Clean targeting for a brand-only Standard Shopping campaign in a PMax brand/non-brand split.
  • Automate manual exclusion of non-brand search terms.
  • Inverse the logic to only target a defined keyword list, if needed.

Notes

The script only excludes search terms that have at least one click — Google Ads caps negative keywords at 5,000 per campaign, so we have to prioritise. The list improves over time as more queries hit the threshold.

Tip: when you first install the script, add a batch of non-brand keywords you already know — the script will catch the rest from there.

Setup

  1. In the script below, replace the brandKeywords array with your brand terms (and common misspellings).
  2. Replace campaignId with your branded Standard Shopping campaign ID.
  3. Recommended frequency: hourly.

Script

/*
  Built by Niklas Buschner @ Radyant (https://radyant.io)
*/

// ----- Settings -----
var SETTINGS = {
  brandKeywords: ['brand', 'braand', 'brend'], // your brand terms + misspellings
  campaignId: '123456789', // your standard shopping campaign ID
};

function main() {
  var regexPattern = SETTINGS.brandKeywords.map(function(keyword) {
    return "(?i).*" + keyword.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1") + ".*";
  }).join("|");

  var now = new Date();
  var from = new Date(now.getTime() - 24 * 60 * 60 * 1000);
  var timeZone = AdsApp.currentAccount().getTimeZone();
  var formattedFrom = Utilities.formatDate(from, timeZone, 'yyyy-MM-dd');
  var formattedTo = Utilities.formatDate(now, timeZone, 'yyyy-MM-dd');

  var query = [
    "SELECT ad_group.id, search_term_view.search_term",
    "FROM search_term_view",
    "WHERE search_term_view.search_term NOT REGEXP_MATCH '" + regexPattern + "'",
    "AND campaign.id = '" + SETTINGS.campaignId + "'",
    "AND ad_group.status = 'ENABLED'",
    "AND search_term_view.status = 'NONE'",
    "AND metrics.clicks >= 1",
    "AND segments.date BETWEEN '" + formattedFrom + "' AND '" + formattedTo + "'"
  ].join(' ');

  try {
    var report = AdsApp.report(query);
    var rows = report.rows();
    while (rows.hasNext()) {
      var row = rows.next();
      var searchQuery = trimQuery(row["search_term_view.search_term"]);
      addToCampaignNegativeKeywords(SETTINGS.campaignId, searchQuery);
    }
  } catch (e) {
    Logger.log("Error executing query: " + e.message);
  }
}

function trimQuery(query) {
  if (query.length > 80) {
    query = query.substring(0, 80);
  }
  var words = query.split(" ");
  return words.length < 10 ? '[' + query + ']' : '"' + words.slice(0, 10).join(" ") + '"';
}

function addToCampaignNegativeKeywords(campaignId, query) {
  var campaigns = AdsApp.shoppingCampaigns()
    .withIds([campaignId])
    .get();

  if (!campaigns.hasNext()) {
    Logger.log("No campaign found with ID: " + campaignId);
    return;
  }
  var campaign = campaigns.next();
  campaign.createNegativeKeyword(query);
}

Need a custom one?

These are the three we publish openly. We build a lot more for client work — bid scripts, alerting, anomaly detection, cross-account reports. If you want a custom Google Ads script built for your stack, book a call.

Want this kind of measurement infrastructure done-for-you?

We instrument organic for B2B SaaS at the dashboard level — citations, attribution, pipeline. Book a 30-minute call to see how it'd work for your category.

// keep going

More from Radyant