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:
- In Google Ads, go to Tools → Bulk Actions → Scripts.
- Click the blue + button to create a new script.
- Give it a descriptive name.
- Copy the entire script code below into the editor.
- Update the placeholder values at the top (spreadsheet URL, campaign ID, brand keywords — depending on the script).
- Click Save, then Authorize and follow the Google authorization prompts.
- Run a preview to check for issues.
- 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
- Make a copy of the Google Sheet template.
- In the Campaign Rules tab, enter one row per campaign with the metrics you care about. Empty cells are ignored.
- In the script below, replace
YOUR_SPREADSHEET_URLnear the top with your sheet URL. - 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
- Create an empty Google Sheet and copy its URL.
- In the script below, replace
ADD YOUR URL OF AN EMPTY GOOGLE SHEET HEREwith that URL. - 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
- In the script below, replace the
brandKeywordsarray with your brand terms (and common misspellings). - Replace
campaignIdwith your branded Standard Shopping campaign ID. - 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.