De omgeving
Google Ads Scripts draaien op Google's servers in een JavaScript-omgeving. Sinds de overstap op de nieuwe runtime (V8-gebaseerd, verplicht sinds 2024) kun je moderne JavaScript gebruiken: const/let, arrow functions, template literals, destructuring, Array.prototype.map, enzovoort. Oude scripts met var en ES5-patronen werken meestal nog, maar nieuwe scripts schrijf je modern.
Je maakt scripts onder Tools → Bulkacties → Scripts. Elk script heeft een main()-functie als startpunt. Twee knoppen zijn essentieel:
- Preview: voert het script uit zonder wijzigingen door te voeren; toont logs en "zou-wijzigen"-acties. Gebruik dit altijd eerst.
- Run: voert het script echt uit.
Bij de eerste keer moet je het script autoriseren voor de gevraagde services (Google Ads, Sheets, Gmail).
AdsApp: selectors en iterators
Het object AdsApp is de toegang tot het account. Het patroon is steeds: selector → filter → get → iterator.
function main() {
const campaigns = AdsApp.campaigns()
.withCondition("campaign.status = 'ENABLED'")
.withCondition("metrics.cost_micros > 100000000") // > € 100
.forDateRange("LAST_7_DAYS")
.orderBy("metrics.cost_micros DESC")
.withLimit(20)
.get();
while (campaigns.hasNext()) {
const campaign = campaigns.next();
const stats = campaign.getStatsFor("LAST_7_DAYS");
Logger.log(
campaign.getName() + " | kosten: € " + stats.getCost().toFixed(2) +
" | conversies: " + stats.getConversions()
);
}
}
Belangrijke selectors: AdsApp.campaigns(), .shoppingCampaigns(), .performanceMaxCampaigns(), .adGroups(), .keywords(), .ads(), .negativeKeywordLists(), .labels(), .budgets(). Filters gebruiken sinds de nieuwe runtime de GAQL-veldnamen (campaign.status, metrics.cost_micros).
Wijzigen doe je via methoden op het object: campaign.pause(), campaign.getBudget().setAmount(150), adGroup.applyLabel("Check").
GAQL: rapporten met AdsApp.search()
Voor rapportage is Google Ads Query Language flexibeler dan selectors. Je kiest velden en resources zoals in SQL:
function main() {
const query =
"SELECT campaign.name, segments.date, metrics.cost_micros, " +
"metrics.conversions, metrics.conversions_value " +
"FROM campaign " +
"WHERE segments.date DURING LAST_30_DAYS " +
"AND campaign.status = 'ENABLED' " +
"ORDER BY metrics.cost_micros DESC";
const rows = AdsApp.search(query);
while (rows.hasNext()) {
const row = rows.next();
const cost = Number(row.metrics.costMicros) / 1e6;
Logger.log(row.campaign.name + " " + row.segments.date + " € " + cost.toFixed(2));
}
}
Let op: veldnamen in de query zijn snake_case (cost_micros), in het resultaat camelCase (costMicros). Bedragen zijn in micros (delen door 1.000.000). Handige resources: search_term_view, shopping_performance_view, asset_group, ad_group_ad, keyword_view, geographic_view, change_event. De Google Ads Query Builder (developers.google.com) helpt bij het samenstellen.
Google Sheets koppelen
const SHEET_URL = "https://docs.google.com/spreadsheets/d/JOUW_SHEET_ID/edit";
function writeToSheet(rows) {
const ss = SpreadsheetApp.openByUrl(SHEET_URL);
let sheet = ss.getSheetByName("Data");
if (!sheet) sheet = ss.insertSheet("Data");
sheet.clearContents();
sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
Bouw eerst een array van arrays ([["Campagne", "Kosten"], ["A", 120.5], ...]) en schrijf die in één keer met setValues; cel-voor-cel schrijven is traag en loopt tegen de 30-minutengrens. Lezen werkt met sheet.getDataRange().getValues(); zo haal je bijvoorbeeld marges of budgetdoelen op.
Planning
Onder het script stel je een frequentie in: elk uur, dagelijks (met tijdstip), wekelijks, maandelijks. Google garandeert geen exact tijdstip (kan tot een uur afwijken). Voor alerts is dagelijks om 07:00 gebruikelijk; voor pacing kan elk uur nodig zijn. Mislukte runs sturen automatisch een e-mail naar de scripteigenaar.
MCC-scripts
In een manageraccount gebruik je MccApp (in de nieuwe runtime ook AdsManagerApp) om over meerdere accounts te itereren:
function main() {
const accounts = AdsManagerApp.accounts()
.withCondition("customer_client.descriptive_name CONTAINS 'Retail'")
.get();
while (accounts.hasNext()) {
const account = accounts.next();
AdsManagerApp.select(account);
Logger.log("Account: " + account.getName());
checkBudgets(); // dezelfde logica per account
}
}
Met executeInParallel("functieNaam", "callbackNaam") verwerk je tot 50 accounts parallel, elk met een eigen tijdslimiet, wat de 30-minutengrens omzeilt.
Foutafhandeling
Wat er mis kan gaan: een Sheet die niet bestaat, een lege campagne, een API-wijziging, een quota-limiet. Vang fouten op en zorg dat een deel-fout niet het hele script stopt:
function safeRun(label, fn) {
try {
fn();
} catch (e) {
Logger.log("FOUT in " + label + ": " + e.message);
MailApp.sendEmail("jij@bedrijf.nl", "Script-fout: " + label, e.message + "\n" + e.stack);
}
}
Gebruik AdsApp.getExecutionInfo().isPreview() om in preview geen e-mails te sturen of geen wijzigingen door te voeren, en AdsApp.getExecutionInfo().getRemainingTime() om tijdig te stoppen bij lange runs.
Voorbeeldscript: budget-alert met pacing
Dit script controleert dagelijks per campagne of de uitgaven van gisteren het dagbudget overschrijden en of de maanduitgaven op koers liggen ten opzichte van een maandbudget uit een Sheet. Bij afwijkingen stuurt het één samenvattende e-mail.
// Budget & pacing alert – dagelijks draaien om 07:00
const CONFIG = {
email: "ads-team@bedrijf.nl",
sheetUrl: "https://docs.google.com/spreadsheets/d/JOUW_SHEET_ID/edit",
sheetName: "Maandbudget", // kolommen: Campagne | Maandbudget
overspendThreshold: 1.3, // 130% van dagbudget
pacingTolerance: 0.15 // ±15% t.o.v. verwachte maanduitgave
};
function main() {
const monthlyBudgets = readMonthlyBudgets();
const issues = [];
const today = new Date();
const dayOfMonth = today.getDate();
const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
const expectedShare = (dayOfMonth - 1) / daysInMonth; // t/m gisteren
const campaigns = AdsApp.campaigns()
.withCondition("campaign.status = 'ENABLED'")
.get();
while (campaigns.hasNext()) {
const c = campaigns.next();
const name = c.getName();
const dailyBudget = c.getBudget().getAmount();
const yesterday = c.getStatsFor("YESTERDAY").getCost();
const mtd = c.getStatsFor("THIS_MONTH").getCost();
// 1. Overspend gisteren
if (dailyBudget > 0 && yesterday > dailyBudget * CONFIG.overspendThreshold) {
issues.push("OVERSPEND | " + name + " | gisteren € " + yesterday.toFixed(2) +
" bij dagbudget € " + dailyBudget.toFixed(2));
}
// 2. Maandpacing
const monthly = monthlyBudgets[name];
if (monthly && expectedShare > 0) {
const expected = monthly * expectedShare;
const deviation = (mtd - expected) / expected;
if (Math.abs(deviation) > CONFIG.pacingTolerance) {
const label = deviation > 0 ? "TE SNEL" : "TE LANGZAAM";
issues.push("PACING " + label + " | " + name + " | MTD € " + mtd.toFixed(0) +
" vs verwacht € " + expected.toFixed(0) + " (" + (deviation * 100).toFixed(0) + "%)");
}
}
}
reportIssues(issues);
}
function readMonthlyBudgets() {
const result = {};
try {
const sheet = SpreadsheetApp.openByUrl(CONFIG.sheetUrl).getSheetByName(CONFIG.sheetName);
const values = sheet.getDataRange().getValues();
for (let i = 1; i < values.length; i++) {
const campaignName = String(values[i][0]).trim();
const budget = Number(values[i][1]);
if (campaignName && budget > 0) result[campaignName] = budget;
}
} catch (e) {
Logger.log("Kon maandbudgetten niet lezen: " + e.message);
}
return result;
}
function reportIssues(issues) {
if (issues.length === 0) {
Logger.log("Geen budgetafwijkingen.");
return;
}
const body = "Budgetcontrole " + Utilities.formatDate(new Date(), "Europe/Amsterdam", "dd-MM-yyyy") +
"\n\n" + issues.join("\n");
Logger.log(body);
if (!AdsApp.getExecutionInfo().isPreview()) {
MailApp.sendEmail(CONFIG.email, "[Google Ads] " + issues.length + " budgetafwijking(en)", body);
}
}
Uitbreidingen: budget automatisch bijstellen binnen een maximum (bijvoorbeeld ±15%), meldingen naar Slack via UrlFetchApp.fetch(webhookUrl, {method: "post", payload: JSON.stringify({text: body})}), of loggen naar een Sheet-tabblad "Log".
Veelgemaakte fouten
- Direct op Run klikken zonder Preview.
- Micros vergeten te delen door een miljoen, waardoor alle bedragen een factor miljoen te hoog zijn.
- Cel-voor-cel schrijven naar Sheets en tegen de tijdslimiet lopen.
- Geen isPreview-check, waardoor testruns echte e-mails of wijzigingen veroorzaken.
- Oude ES5-scripts van internet plakken met verouderde selectors of veldnamen.
- Geen limieten op wijzigingen: één bug en alle budgetten staan op € 10.000.
- Hardcoded e-mailadressen en URL's verspreid door de code in plaats van één CONFIG-object.
Tip: bewaar al je scripts in een Git-repository of gedeelde map met versienummer en changelog. De editor in Google Ads heeft geen versiebeheer; één verkeerde opslag en de vorige versie is weg.
- Scripts draaien in de moderne V8-runtime; gebruik Preview altijd vóór Run en autoriseer alleen de services die je nodig hebt.
- Het basispatroon is selector → filter → get → iterator via AdsApp; voor flexibele rapportage gebruik je GAQL via AdsApp.search() met bedragen in micros.
- Schrijf naar Google Sheets in één setValues-aanroep en lees configuratie (budgetten, marges) uit Sheets in plaats van hardcoded waarden.
- MCC-scripts itereren over accounts met AdsManagerApp en kunnen met executeInParallel de 30-minutengrens omzeilen.
- Bouw foutafhandeling, een isPreview-check en impactgrenzen in, en beheer scripts met versiebeheer buiten de Google Ads-editor.
Toets je kennis
5 vragen · slaag met 70% of hoger om de les af te ronden
- 1.Wat is het verschil tussen Preview en Run in Google Ads Scripts?
- 2.Een script rapporteert kosten van 45.000.000 voor een campagne die € 45 uitgaf. Wat is er mis?
- 3.Waarom schrijf je data naar Google Sheets met één setValues-aanroep in plaats van per cel?
- 4.Waarvoor gebruik je AdsApp.getExecutionInfo().isPreview()?
- 5.Hoe verwerk je in een MCC-script veel accounts binnen de tijdslimiet?