Ja, ChatGPT, Claude oder Gemini schreiben dir brauchbare Google Ads Skripte. Gefährlich wird es erst, wenn du den Code ungelesen ausführst. Sprachmodelle kennen das Grundgerüst gut, verwechseln aber Feldnamen, greifen zu veralteten Methoden und vergessen, dass Kosten in Mikros geliefert werden. Mit einem festen Ablauf fängst du das ab: Aufgabe genau beschreiben, Doku-Auszug mitgeben, Code lesen, in der Vorschau testen und Änderungen erst nach einem Review freigeben.
Hier bekommst du diesen Ablauf mit Prompt-Vorlagen, vier vollständige und kommentierte Beispielskripte (nur lesend oder mit DRY_RUN = true) und die Fehler, die ich in KI-Code immer wieder finde, jeweils mit falscher und richtiger Fassung. Wie die KI in Google Ads selbst arbeitet, also Smart Bidding, Performance Max und AI Max, steht in KI und Automatisierung in Google Ads. Hier geht es um Code, den du selbst ins Konto bringst.
Stand: 08.10.2026. Grundlage sind die Entwicklerdokumentation zu Google Ads Scripts (Seite zu den Limits zuletzt aktualisiert am 30.09.2026) und die Feldreferenz der Google Ads API in Version v25. Alle Skripte sind konstruierte Beispiele ohne Kontodaten. Die Quellen stehen am Ende des Artikels.
Was Google Ads Skripte sind und was sie können
Google Ads Skripte sind JavaScript-Programme, die direkt in deinem Google-Ads-Konto laufen. Google beschreibt sie als Weg, Google-Ads-Daten programmatisch zu verwalten und abzufragen, in einer Entwicklungsumgebung im Browser, für ein einzelnes Konto oder für viele Konten über ein Verwaltungskonto (Google, Scripts-Übersicht). Einen eigenen Server brauchst du nicht.
Typische Aufgaben:
- Berichte automatisch in Google Tabellen schreiben
- per E-Mail warnen, wenn Kosten oder Klicks aus dem Rahmen fallen
- Landingpage-URLs auf Fehler prüfen
- Gebote, Budgets oder Labels nach festen Regeln ändern
Geplante Skripte laufen laut Google auch dann, wenn niemand angemeldet ist (Autorisierung). Seit März 2022 gibt es die neue Skript-Umgebung in voller Version, mit modernem JavaScript, also let, Klassen und for...of-Schleifen (Google Ads Developer Blog, März 2022). Alte Skripte wollte Google laut Ankündigung ab September 2022 automatisch migrieren und im Oktober 2022 komplett umstellen (Developer Blog, August 2022). Für KI-Code ist das wichtig: Im Netz liegt viel Beispielcode aus der Zeit davor, und in meiner Praxis mischen Modelle diese alten Muster gern mit den neuen.
Die Limits aus der Doku
| Grenze | Wert laut Google |
|---|---|
| Laufzeit pro Ausführung im Werbekonto | 30 Minuten, danach Abbruch |
| Laufzeit im Verwaltungskonto mit executeInParallel und Callback | bis 60 Minuten, bis zu 50 Konten |
| Ergebnisse pro Iterator | standardmäßig höchstens 50.000 |
| IDs in withIds() | höchstens 10.000 |
| Log-Ausgabe | wird bei 100 KB abgeschnitten |
| Autorisierte Skripte pro Konto | 250, darüber verliert ein älteres Skript vorübergehend die Autorisierung |
| E-Mail-Empfänger pro Tag (MailApp) | 100 mit privatem Google-Konto, 1.500 mit Google Workspace |
| URL-Abrufe pro Tag (UrlFetchApp) | 20.000 mit privatem Google-Konto, 100.000 mit Google Workspace |
Die ersten sechs Zeilen stammen von der Seite Limits der Google Ads Scripts. Die letzten beiden kommen aus den Kontingenten von Google Apps Script, denn Mail, URL-Abrufe und Tabellen laufen über Apps-Script-Dienste. Diese Kontingente gelten pro Nutzer und setzen sich 24 Stunden nach der ersten Anfrage zurück. Ein Detail, das KI-Code fast nie berücksichtigt: Bricht ein Skript nach 30 Minuten ab, bleiben laut Google alle bis dahin gemachten Änderungen bestehen. Ein Skript, das Gebote ändert und mittendrin abbricht, hinterlässt also ein halb umgebautes Konto.
So legst du ein Skript an
- Öffne in Google Ads links im Menü Tools > Bulk-Aktionen > Scripts. Alternativ suchst du in der Suchleiste von Google Ads nach „Scripts“.
- Klick auf das Pluszeichen und wähle „Neues Skript“. Im selben Menü findest du auch Skriptvorlagen von Google.
- Gib dem Skript einen Namen und füg den Code in den Editor ein.
- Klick bei der ersten Vorschau auf „Autorisieren“. Das ist einmal pro Skript nötig, danach greift das Skript in deinem Namen auf das Konto zu.
- Klick auf „Vorschau“. Ergebnisse und Logs erscheinen im Bereich „Änderungen / Logs“.
- Erst wenn die Vorschau passt, klickst du auf „Ausführen“. Danach öffnet sich die „Scripthistorie“ mit Status und Ergebnissen.
Die Bezeichnungen stammen aus der deutschen Fassung von Googles Einstiegsanleitung. In deinem Konto können sie leicht abweichen, die englische Oberfläche sagt „Tools > Bulk actions > Scripts“.
Vorschau und Ausführen: der wichtigste Unterschied
Im Vorschaumodus nehmen Skripte laut Google keine Änderungen an den tatsächlichen Kampagnendaten vor (Vorschau-Doku). Der Haken steht direkt darunter: Die Vorschau betrifft nur Aufrufe über AdsApp. Andere Dienste laufen normal. Eine E-Mail über MailApp geht raus, eine Google Tabelle wird beschrieben, egal ob Vorschau oder echter Lauf. Außerdem legt die Vorschau nichts an. Code, der ein neues Objekt erzeugt und sofort weiterverwendet, verhält sich dort anders als später im echten Lauf.
Daraus folgen zwei Regeln für alle Skripte in diesem Artikel:
- E-Mails gehen nur raus, wenn
AdsApp.getExecutionInfo().isPreview()false liefert. Genau dieses Muster zeigt Google in der Referenz zu ExecutionInfo. - Änderungen am Konto stehen hinter einem Schalter
DRY_RUN. Solange er auf true steht, loggt das Skript nur, was es tun würde.
Zeitplan und Logs
Für jedes Skript kannst du einen Zeitplan festlegen. Googles eigene Beispiellösungen zeigen die Spannweite: Den Link Checker soll man stündlich laufen lassen, die Kontoübersicht täglich um 3 Uhr oder später, weil Statistiken in Google Ads laut Google bis zu drei Stunden verzögert sein können. Diese Verzögerung betrifft jedes Skript, das mit Vortageswerten rechnet.
Geplante Skripte laufen im Namen der Person, die sie autorisiert hat. Verliert diese Person den Zugriff auf das Konto, hört das Skript auf zu laufen. Es bleibt erhalten, bis jemand mit Zugriff es neu autorisiert. Wenn die Kollegin geht, die alle Skripte eingerichtet hat, merkst du das sonst erst, wenn die Warnungen ausbleiben.
Logs schreibst du mit console.log(). Sie erscheinen in der Vorschau unter „Änderungen / Logs“ und nach einem echten Lauf in der Scripthistorie. Alles über 100 KB schneidet Google ab, logge also lieber Zusammenfassungen als jede einzelne Zeile.
Der Arbeitsablauf mit KI
So gehe ich vor, egal ob mit ChatGPT, Claude oder Gemini. Aus meiner Erfahrung entscheidet der Kontext, den du mitgibst, mehr über die Qualität als die Wahl des Modells. Prompt-Vorlagen für Aufgaben ohne Code, von Keyword-Clustern bis zum Monatsbericht, findest du in ChatGPT-Prompts für Google Ads.
1. Aufgabe präzise beschreiben
„Mach mir ein Skript für Budget-Warnungen“ erzeugt Code, der irgendetwas tut. Beschreib stattdessen:
- welche Daten das Skript liest (Ressource, Zeitraum, Kampagnentypen)
- welche Schwelle in welcher Einheit gilt (Kontowährung oder Mikros)
- was am Ende passiert (Log, E-Mail, Tabelle, Label)
- was das Skript ausdrücklich nicht tun darf, zum Beispiel Gebote, Budgets oder Status ändern
2. Kontext mitgeben
Kopier die Abschnitte der Methoden, die das Skript nutzen soll, aus der AdsApp-Referenz in den Prompt, dazu die GAQL-Felder aus der Feldreferenz der Google Ads API. In meiner Praxis sinkt damit die Zahl erfundener Methoden spürbar. Den Kontoaufbau beschreibst du abstrakt: „Suchkampagnen und eine Performance-Max-Kampagne, Währung EUR, Zeitzone Europe/Berlin“. Kampagnennamen, Kunden-IDs und Suchbegriffe braucht das Modell nicht.
3. Code anfordern, mit klaren Regeln
Diese Vorlage nutze ich als Startpunkt. Die Teile in eckigen Klammern ersetzt du:
Du bist erfahrener Entwickler für Google Ads Scripts.
Aufgabe: [GENAUE BESCHREIBUNG, z. B. "Kosten des laufenden Monats mit einem
Monatsbudget vergleichen und per E-Mail warnen"]
Kontext:
- Umgebung: Google Ads Scripts (aktuelle Skript-Umgebung, JavaScript mit
let/const und for...of). Abfragen mit AdsApp.search() und GAQL,
keine AWQL-Spalten.
- Konto: [ABSTRAKTE STRUKTUR, z. B. "nur Suchkampagnen, Währung EUR,
Zeitzone Europe/Berlin"]. Keine echten Namen oder IDs.
- Referenz-Auszug: [ABSCHNITTE AUS DER OFFIZIELLEN DOKU EINFÜGEN]
Regeln:
1. Nutze nur Methoden und Felder, die im Referenz-Auszug oder in der
offiziellen Google-Ads-Scripts-Doku stehen. Erfinde nichts. Wenn du
unsicher bist, sag es ausdrücklich.
2. Das Skript ändert nichts am Konto. [ODER: Änderungen nur, wenn
DRY_RUN = false. Standard ist DRY_RUN = true, dann nur loggen.]
3. Kosten kommen in GAQL als Mikros: durch 1.000.000 teilen.
4. Datumswerte in der Zeitzone des Kontos bilden
(AdsApp.currentAccount().getTimeZone()).
5. E-Mails nur senden, wenn AdsApp.getExecutionInfo().isPreview() false ist.
6. Datenmengen begrenzen (LIMIT bzw. withLimit) und bei langen Läufen die
Restlaufzeit mit getRemainingTime() prüfen.
7. Jeden Abschnitt auf Deutsch kommentieren und am Ende alle genutzten
Methoden mit Link zur jeweiligen Referenzseite auflisten.
4. Code lesen
Bevor du irgendetwas startest, liest du jede Zeile. Meine Checkliste:
- Jede Methode steht in der Referenz von Google Ads Scripts oder Apps Script. Links, die das Modell angibt, öffnest du selbst.
- Jedes GAQL-Feld existiert in der Feldreferenz der abgefragten Ressource.
- Im Ergebnis heißen die Felder in lowerCamelCase: aus
metrics.cost_microsin der Abfrage wirdrow.metrics.costMicros(Google, Berichte in Skripten). - Geldwerte sind durchgehend entweder Mikros oder Kontowährung.
- Methoden, die Kampagnen, Gebote, Budgets oder Status ändern, stehen nur in einem
DRY_RUN-Zweig. - Du weißt, was
MailApp,UrlFetchAppundSpreadsheetAppschon in der Vorschau tun. - Es gibt Grenzen: LIMIT oder withLimit, einen Blick auf die Restlaufzeit, eine überschaubare Zahl an E-Mails.
Ein zweites Modell als Reviewer findet zusätzliche Fehler. Ich gebe den Code dafür in einen frischen Chat:
Prüfe dieses Google-Ads-Skript wie ein strenger Code-Reviewer.
1. Liste jede aufgerufene Methode und jedes GAQL-Feld auf und sag zu jedem,
ob es in der offiziellen Google-Ads-Scripts- bzw. Google-Ads-API-Referenz
existiert. Markiere alles, was du nicht sicher belegen kannst.
2. Nenne alle Stellen, an denen das Skript Daten im Konto ändert,
E-Mails verschickt oder Tabellen beschreibt.
3. Prüfe Einheiten (Mikros oder Kontowährung), Zeitzonen und Datumsgrenzen.
4. Prüfe, ob das Skript bei vielen Daten an Limits stößt
(30 Minuten Laufzeit, 50.000 Ergebnisse pro Iterator).
Ändere den Code nicht. Gib nur eine nummerierte Mängelliste aus.
[CODE EINFÜGEN]
5. In der Vorschau testen
Starte die Vorschau und lies die Logs. Meldet das Skript, dass ein Feld undefined ist, logge eine einzige Ergebniszeile mit console.log(JSON.stringify(row)). Dann siehst du die echten Feldnamen. Blind kopieren hilft übrigens auch bei Google nicht: Auf der Best-Practice-Seite fragt ein Beispiel metrics.cost_micros ab und loggt danach row.metrics.cost, ein Feld, das gar nicht abgefragt wurde.
Bricht das Skript ab, hilft diese Vorlage:
Das Skript bricht in der Vorschau mit dieser Meldung ab:
[FEHLERMELDUNG WÖRTLICH]
Log-Ausgabe einer Ergebniszeile, Werte durch Platzhalter ersetzt:
[AUSGABE VON JSON.stringify(row), ANONYMISIERT]
Finde die Ursache. Nutze nur Methoden aus der offiziellen Referenz und
erkläre die Änderung in zwei Sätzen, bevor du den korrigierten Code zeigst.
6. Absichern
Teste ein neues Skript zuerst in einem Konto, in dem wenig auf dem Spiel steht, oder grenz es auf eine einzelne Kampagne ein, etwa mit campaign.name = 'Testkampagne' in der GAQL-Abfrage oder über ein Label. Im Verwaltungskonto startest du mit einem einzigen Kundenkonto.
7. Schrittweise freigeben
DRY_RUN = true, Vorschau, Logs lesen.DRY_RUN = true, echter Lauf über „Ausführen“, Scripthistorie prüfen.DRY_RUN = false, aber nur für eine Kampagne oder ein Label.- Ausweiten und erst dann einen Zeitplan setzen.
Vier Beispielskripte zum Anpassen
Alle vier Skripte lesen Daten mit AdsApp.search() und GAQL, so wie Google es auf der Best-Practice-Seite empfiehlt. Die Werte in Großbuchstaben am Anfang sind Beispielwerte, die du an dein Konto anpasst. Keines der Skripte ändert Gebote, Budgets oder Status. Skript 3 kann Anzeigen mit einem Label markieren, aber nur, wenn du DRY_RUN bewusst auf false setzt.
Skript 1: Budget-Pacing-Warnung per E-Mail
Das Skript vergleicht die Kosten vom Monatsersten bis gestern mit dem anteiligen Monatsbudget und rechnet hoch, wo du am Monatsende landest. Liegt das Konto außerhalb der Toleranz, kommt eine E-Mail. Den heutigen Tag lasse ich bewusst weg, weil er noch läuft und die Statistiken verzögert sein können.
/**
* Budget-Pacing-Warnung (nur lesend)
* Vergleicht die Kosten vom Monatsersten bis gestern mit dem anteiligen
* Monatsbudget und warnt per E-Mail, wenn das Konto zu schnell oder zu
* langsam ausgibt. Ändert nichts am Konto.
*/
const MONTHLY_BUDGET = 3000; // Monatsbudget in Kontowährung (Beispielwert)
const UPPER_LIMIT = 1.15; // Warnung ab 15 % über Plan (Beispielwert)
const LOWER_LIMIT = 0.85; // Warnung ab 15 % unter Plan (Beispielwert)
const EMAIL = 'du@example.com'; // Empfänger der Warnung
function main() {
const account = AdsApp.currentAccount();
const timeZone = account.getTimeZone();
const now = new Date();
// Datum immer in der Zeitzone des Kontos bilden
const day = Number(Utilities.formatDate(now, timeZone, 'd'));
const month = Number(Utilities.formatDate(now, timeZone, 'M'));
const year = Number(Utilities.formatDate(now, timeZone, 'yyyy'));
const daysInMonth = new Date(year, month, 0).getDate();
if (day === 1) {
console.log('Monatserster: Es gibt noch keinen vollen Tag zum Vergleichen.');
return;
}
const firstDay = Utilities.formatDate(now, timeZone, 'yyyy-MM') + '-01';
// 24 Stunden zurück ergibt gestern in Kontozeit (nicht kurz nach Mitternacht planen)
const yesterday = Utilities.formatDate(
new Date(now.getTime() - 24 * 60 * 60 * 1000), timeZone, 'yyyy-MM-dd');
const query =
'SELECT metrics.cost_micros FROM customer ' +
"WHERE segments.date BETWEEN '" + firstDay + "' AND '" + yesterday + "'";
let costMicros = 0;
for (const row of AdsApp.search(query)) {
costMicros += Number(row.metrics.costMicros); // Feldname in lowerCamelCase
}
const cost = costMicros / 1000000; // Mikros in Kontowährung
const daysDone = day - 1;
const plan = MONTHLY_BUDGET * daysDone / daysInMonth;
const forecast = cost / daysDone * daysInMonth;
const ratio = cost / plan;
const currency = account.getCurrencyCode();
const summary =
'Kosten bis gestern: ' + cost.toFixed(2) + ' ' + currency + '\n' +
'Plan bis gestern: ' + plan.toFixed(2) + ' ' + currency + '\n' +
'Hochrechnung Monatsende: ' + forecast.toFixed(2) + ' ' + currency;
console.log(summary);
if (ratio > UPPER_LIMIT || ratio < LOWER_LIMIT) {
const subject = 'Budget-Pacing ' + account.getName() + ': ' +
Math.round(ratio * 100) + ' % des Plans';
if (AdsApp.getExecutionInfo().isPreview()) {
console.log('Vorschau, keine E-Mail. Betreff wäre: ' + subject);
} else {
MailApp.sendEmail(EMAIL, subject, summary);
}
}
}
Genutzte Methoden: AdsApp.currentAccount() mit getTimeZone(), getCurrencyCode() und getName() aus der Account-Referenz, Utilities.formatDate(), AdsApp.search(), AdsApp.getExecutionInfo().isPreview() und MailApp.sendEmail(). GAQL: Ressource customer, Felder metrics.cost_micros und segments.date.
Die Ressource customer umfasst das ganze Konto. Hast du mehrere Budgets für verschiedene Kampagnengruppen, fragst du stattdessen campaign ab und filterst mit einer Bedingung auf campaign.name. Plane das Skript täglich am Vormittag ein.
Skript 2: Suchbegriffe mit Kosten ohne Conversion in eine Tabelle
Das Skript schreibt die teuersten Suchbegriffe der letzten 30 Tage, die keine Conversion gebracht haben, in ein Google Sheet. Es ändert nichts im Konto. Die Entscheidung, welche Begriffe du ausschließt, triffst du selbst. Hilfe dabei gibt dir meine Liste negativer Keywords.
/**
* Suchbegriffe mit Kosten, aber ohne Conversion (nur lesend im Konto)
* Schreibt die teuersten Suchbegriffe der letzten 30 Tage ohne Conversion
* in ein Google Sheet. Achtung: Die Tabelle wird auch in der Vorschau
* beschrieben.
*/
const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/DEINE-TABELLEN-ID/edit';
const SHEET_NAME = 'Suchbegriffe ohne Conversion';
const MIN_COST = 20; // Mindestkosten in Kontowährung (Beispielwert)
const MAX_ROWS = 500; // Obergrenze für die Abfrage
function main() {
const minCostMicros = Math.round(MIN_COST * 1000000); // Schwelle in Mikros
// search_term_view enthält keine Performance-Max-Daten
const query =
'SELECT search_term_view.search_term, campaign.name, ad_group.name, ' +
'metrics.clicks, metrics.cost_micros, metrics.conversions ' +
'FROM search_term_view ' +
'WHERE segments.date DURING LAST_30_DAYS ' +
'AND metrics.cost_micros > ' + minCostMicros + ' ' +
'ORDER BY metrics.cost_micros DESC ' +
'LIMIT ' + MAX_ROWS;
const output = [];
for (const row of AdsApp.search(query)) {
// Conversions sind Kommazahlen, Bruchteile sind möglich
if (Number(row.metrics.conversions) > 0) {
continue;
}
output.push([
row.searchTermView.searchTerm,
row.campaign.name,
row.adGroup.name,
Number(row.metrics.clicks),
Number(row.metrics.costMicros) / 1000000,
]);
}
const header = ['Suchbegriff', 'Kampagne', 'Anzeigengruppe', 'Klicks', 'Kosten'];
const spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
const sheet = spreadsheet.getSheetByName(SHEET_NAME) ||
spreadsheet.insertSheet(SHEET_NAME);
sheet.clearContents();
sheet.getRange(1, 1, 1, header.length).setValues([header]);
if (output.length > 0) {
sheet.getRange(2, 1, output.length, header.length).setValues(output);
}
console.log(output.length + ' Suchbegriffe ohne Conversion geschrieben.');
}
Genutzte Methoden: AdsApp.search(), SpreadsheetApp.openByUrl(), getSheetByName(), insertSheet(), clearContents(), getRange() und setValues() aus der Apps-Script-Referenz. GAQL: Ressource search_term_view mit search_term_view.search_term, metrics.clicks, metrics.cost_micros und metrics.conversions.
Performance Max fehlt in diesem Bericht: search_term_view enthält laut Feldreferenz keine PMax-Daten, dafür verweist Google auf die Ressource campaign_search_term_view. LIMIT greift vor dem Conversion-Filter im Code, es können also weniger als 500 Zeilen ankommen. Die Tabelle wird schon in der Vorschau beschrieben, weil Tabellen kein AdsApp-Aufruf sind, nimm zum Testen also eine eigene Testtabelle. Geschrieben wird mit einem einzigen setValues() statt Zeile für Zeile, so wie Google es auf der Best-Practice-Seite empfiehlt. Welche Kennzahlen du für solche Auswertungen sonst noch brauchst, steht in Google Ads Reports und Analyse.
Skript 3: Link-Checker für finale URLs mit DRY_RUN
Das Skript ruft jede finale URL aktiver Anzeigen einmal auf und meldet alles, was nicht mit dem Statuscode 200 antwortet. Mit DRY_RUN = true loggt es nur und schickt nach einem echten Lauf eine E-Mail. Mit DRY_RUN = false bekommen die betroffenen Anzeigen zusätzlich ein Label, das du vorher im Konto angelegt hast. Ein Label ändert selbst keine Gebote, Budgets oder Status, deshalb eignet es sich gut als erste Änderung zum Üben. Prüfe aber, ob automatisierte Regeln oder andere Skripte in deinem Konto auf Labels reagieren.
/**
* Link-Checker für finale URLs aktiver Anzeigen
* Ruft jede finale URL einmal auf und meldet alles, was nicht mit 200
* antwortet. Mit DRY_RUN = true ändert das Skript nichts am Konto.
* Mit DRY_RUN = false bekommen betroffene Anzeigen ein Label, das du
* vorher im Konto angelegt hast.
*/
const DRY_RUN = true; // true: nur prüfen und loggen
const LABEL_NAME = 'Link-Fehler'; // muss im Konto schon existieren
const EMAIL = 'du@example.com';
const MAX_URLS = 300; // Obergrenze pro Lauf (Beispielwert)
const TIME_BUFFER_SECONDS = 120; // Reserve vor dem 30-Minuten-Limit
function main() {
const query =
'SELECT ad_group.id, ad_group_ad.ad.id, ad_group_ad.ad.final_urls ' +
'FROM ad_group_ad ' +
"WHERE ad_group_ad.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND campaign.status = 'ENABLED'";
// Jede URL nur einmal prüfen, aber merken, welche Anzeigen sie nutzen
const adsByUrl = new Map();
for (const row of AdsApp.search(query)) {
const urls = row.adGroupAd.ad.finalUrls || [];
for (const url of urls) {
if (!adsByUrl.has(url)) {
adsByUrl.set(url, []);
}
adsByUrl.get(url).push([String(row.adGroup.id), String(row.adGroupAd.ad.id)]);
}
}
const executionInfo = AdsApp.getExecutionInfo();
const broken = [];
let checked = 0;
for (const url of adsByUrl.keys()) {
if (checked >= MAX_URLS ||
executionInfo.getRemainingTime() < TIME_BUFFER_SECONDS) {
console.log('Grenze erreicht, den Rest beim nächsten Lauf prüfen.');
break;
}
checked++;
let status;
try {
// muteHttpExceptions: 404 oder 500 werfen keinen Fehler, sondern liefern den Code
status = UrlFetchApp.fetch(url, {muteHttpExceptions: true}).getResponseCode();
} catch (e) {
status = 'Fehler: ' + e.message; // z. B. Domain nicht erreichbar
}
if (status !== 200) {
broken.push({url: url, status: status, adIds: adsByUrl.get(url)});
}
}
console.log(checked + ' von ' + adsByUrl.size + ' URLs geprüft, ' +
broken.length + ' mit Problemen.');
if (broken.length === 0) {
return;
}
const report = broken.map(function(item) {
return item.status + ' ' + item.url + ' (' + item.adIds.length + ' Anzeigen)';
}).join('\n');
console.log(report);
if (DRY_RUN) {
console.log('DRY_RUN: keine Labels gesetzt.');
} else {
const labelExists = AdsApp.labels()
.withCondition("label.name = '" + LABEL_NAME + "'")
.get()
.hasNext();
if (!labelExists) {
console.log('Label "' + LABEL_NAME + '" fehlt. Bitte zuerst im Konto anlegen.');
} else {
for (const item of broken) {
const ads = AdsApp.ads().withIds(item.adIds).get();
while (ads.hasNext()) {
ads.next().applyLabel(LABEL_NAME);
}
}
}
}
if (!executionInfo.isPreview()) {
MailApp.sendEmail(EMAIL, 'Link-Checker: ' + broken.length + ' fehlerhafte URLs', report);
}
}
Genutzte Methoden: AdsApp.search(), AdsApp.getExecutionInfo() mit getRemainingTime() und isPreview(), UrlFetchApp.fetch() mit getResponseCode(), AdsApp.labels().withCondition(), AdsApp.ads().withIds() mit Paaren aus Anzeigengruppen-ID und Anzeigen-ID laut AdSelector-Referenz, applyLabel() und MailApp.sendEmail(). GAQL: Ressource ad_group_ad mit ad_group_ad.ad.final_urls, einem Listenfeld.
UrlFetchApp folgt Weiterleitungen standardmäßig, eine 301 auf eine funktionierende Seite zählt also als 200 (UrlFetchApp-Referenz). Jeder Abruf zählt gegen das Tageskontingent für URL-Abrufe, und die Abrufe passieren auch in der Vorschau, weil sie nicht über AdsApp laufen. Das ist hier unkritisch, weil nur Seiten gelesen werden. applyLabel() schlägt laut Referenz fehl, wenn das Label nicht existiert, deshalb prüft das Skript das vorher. Diese einfache Fassung prüft nur finale URLs von Anzeigen, ohne Tracking-Vorlagen, eigene Parameter oder mobile URLs. Googles vollständiger Link Checker prüft auch Keywords und Sitelinks und arbeitet große Konten über mehrere stündliche Läufe ab. Erfahrungsgemäß blocken manche Server automatisierte Abrufe und antworten mit 403, obwohl die Seite für Menschen funktioniert. Solche Treffer prüfst du von Hand.
Skript 4: Anomalie-Warnung für Klicks und Kosten
Das Skript vergleicht gestern mit dem Durchschnitt derselben Wochentage der letzten vier Wochen und warnt bei starken Abweichungen. Der Vergleich mit dem gleichen Wochentag ist robuster als der mit dem Vortag, weil die Wochentage in den meisten Konten, die ich sehe, sehr unterschiedlich laufen.
/**
* Anomalie-Warnung für Klicks und Kosten (nur lesend)
* Vergleicht gestern mit dem Durchschnitt derselben Wochentage der
* letzten vier Wochen und warnt per E-Mail bei starker Abweichung.
*/
const WEEKS_BACK = 4; // Anzahl Vergleichswochen
const THRESHOLD = 0.4; // Warnung ab 40 % Abweichung (Beispielwert)
const MIN_CLICKS = 30; // darunter ist der Vergleich zu wackelig (Beispielwert)
const EMAIL = 'du@example.com';
function main() {
const account = AdsApp.currentAccount();
const timeZone = account.getTimeZone();
const currency = account.getCurrencyCode();
const dayMs = 24 * 60 * 60 * 1000;
const now = Date.now();
// Datum vor n Tagen in der Zeitzone des Kontos
const dateDaysAgo = function(n) {
return Utilities.formatDate(new Date(now - n * dayMs), timeZone, 'yyyy-MM-dd');
};
const yesterday = dateDaysAgo(1);
const compareDays = [];
for (let week = 1; week <= WEEKS_BACK; week++) {
compareDays.push(dateDaysAgo(1 + 7 * week));
}
const firstDay = compareDays[compareDays.length - 1];
const query =
'SELECT segments.date, metrics.clicks, metrics.cost_micros FROM customer ' +
"WHERE segments.date BETWEEN '" + firstDay + "' AND '" + yesterday + "'";
const byDate = new Map();
for (const row of AdsApp.search(query)) {
byDate.set(row.segments.date, {
clicks: Number(row.metrics.clicks),
cost: Number(row.metrics.costMicros) / 1000000,
});
}
// Tage, an denen alle Werte 0 sind, liefert die API nicht: als 0 zählen
const valuesFor = function(date) {
return byDate.get(date) || {clicks: 0, cost: 0};
};
const last = valuesFor(yesterday);
let sumClicks = 0;
let sumCost = 0;
for (const date of compareDays) {
sumClicks += valuesFor(date).clicks;
sumCost += valuesFor(date).cost;
}
const avgClicks = sumClicks / WEEKS_BACK;
const avgCost = sumCost / WEEKS_BACK;
if (avgClicks < MIN_CLICKS) {
console.log('Zu wenig Klicks für einen sinnvollen Vergleich.');
return;
}
const clickChange = (last.clicks - avgClicks) / avgClicks;
const costChange = avgCost > 0 ? (last.cost - avgCost) / avgCost : 0;
const summary =
'Gestern (' + yesterday + '): ' + last.clicks + ' Klicks, ' +
last.cost.toFixed(2) + ' ' + currency + '\n' +
'Schnitt gleicher Wochentag: ' + avgClicks.toFixed(1) + ' Klicks, ' +
avgCost.toFixed(2) + ' ' + currency + '\n' +
'Abweichung: Klicks ' + Math.round(clickChange * 100) + ' %, Kosten ' +
Math.round(costChange * 100) + ' %';
console.log(summary);
if (Math.abs(clickChange) > THRESHOLD || Math.abs(costChange) > THRESHOLD) {
const subject = 'Auffälligkeit in ' + account.getName();
if (AdsApp.getExecutionInfo().isPreview()) {
console.log('Vorschau, keine E-Mail. Betreff wäre: ' + subject);
} else {
MailApp.sendEmail(EMAIL, subject, summary);
}
}
}
Genutzte Methoden: AdsApp.currentAccount() mit getTimeZone(), getCurrencyCode() und getName(), Utilities.formatDate(), AdsApp.search(), isPreview() und MailApp.sendEmail(). GAQL: Ressource customer mit segments.date, metrics.clicks und metrics.cost_micros.
GAQL-Zeiträume rechnet Google in der Zeitzone des Kontos (GAQL-Datumsbereiche), deshalb baut das Skript auch seine eigenen Datumswerte in dieser Zeitzone. Zeilen, in denen alle abgefragten Messwerte 0 sind, liefert die API bei segmentierten Abfragen gar nicht aus (Google, Zero metrics). Ein KI-Skript, das einfach über alle gelieferten Zeilen mittelt, rechnet den Durchschnitt dann zu hoch. Conversions lasse ich in dieser Warnung bewusst weg: In meiner Praxis laufen sie oft noch Tage nach dem Klick nach, und der Vortag sähe dann fast immer zu schlecht aus. Plane das Skript täglich um 3 Uhr oder später ein. Wer mehr will, findet in Googles Lösungsbibliothek den Account Anomaly Detector als ausgebaute Variante.
Typische Fehler in KI-Code
Diese Fehler finde ich in KI-generierten Skripten am häufigsten. Die Belege stehen in der jeweils verlinkten Referenz.
| Fehler | Woran du ihn erkennst | Richtig |
|---|---|---|
| AWQL-Spalten aus altem Code | Bedingungen wie 'Clicks > 10' oder 'Cost > 50' | GAQL-Felder wie 'metrics.clicks > 10', Google rät ausdrücklich zu GAQL statt AWQL |
| Feldnamen aus der Abfrage im Ergebnis | row.metrics.cost_micros ist undefined | row.metrics.costMicros, Ergebnisfelder kommen in lowerCamelCase |
| Mikros mit Währung verwechselt | Schwelle 50 gegen cost_micros, das Skript schlägt bei fast jedem Klick an | Schwelle mal 1.000.000 oder Ergebnis durch 1.000.000 teilen |
| Veraltete Methoden | getAveragePosition(), getRemainingGetQuota() | getAveragePosition() liefert laut Referenz seit Ende September 2019 nur noch null, die Kontingent-Methoden sind veraltet, weil es das Limit nicht mehr gibt |
| Erfundene Methoden | campaign.getCost() oder withStatus('ENABLED') am Selektor | Kosten über getStatsFor(...).getCost() oder GAQL, Status über withCondition() |
| Nur Such- und Displaykampagnen | AdsApp.campaigns() findet keine Performance-Max-, Shopping- oder Videokampagnen | performanceMaxCampaigns(), shoppingCampaigns(), videoCampaigns() oder GAQL auf campaign |
| Keine Grenzen | Schleife über alle Keywords ohne withLimit(), kein Blick auf die Restlaufzeit | LIMIT bzw. withLimit() und getRemainingTime() |
| Zeitzone ignoriert | Datum mit new Date() und toISOString() gebaut | Utilities.formatDate() mit der Zeitzone des Kontos |
| Vorschau missverstanden | E-Mail oder Tabelle „nur zum Testen“ in der Vorschau | isPreview() abfragen, Testtabelle nutzen |
| Fehlende Nulltage | Durchschnitt nur über die Tage, die die API geliefert hat | fehlende Tage als 0 zählen |
Tabelle seitlich scrollbar
Belege: CampaignSelector (GAQL-Bedingungen, Kosten in Mikros), Best Practices (GAQL statt AWQL, search statt report), Stats (getCost() in Kontowährung, getAveragePosition() veraltet), ExecutionInfo (Kontingent-Methoden veraltet), AdsApp (campaigns() liefert Such- und Displaykampagnen).
Drei der Fehler als Code, jeweils falsch und richtig:
// FALSCH: AWQL-Spalte aus altem Beispielcode, ohne Zeitraum
const oldSelector = AdsApp.campaigns()
.withCondition('Clicks > 10')
.get();
// RICHTIG: GAQL-Feld. Bei Bedingungen auf Statistikfelder ist ein Zeitraum Pflicht
const newSelector = AdsApp.campaigns()
.withCondition('metrics.clicks > 10')
.forDateRange('LAST_30_DAYS')
.get();
const rows = AdsApp.search(
'SELECT campaign.name, metrics.cost_micros FROM campaign ' +
'WHERE segments.date DURING LAST_7_DAYS');
for (const row of rows) {
// FALSCH: Feldname wie in der Abfrage, Mikros als Euro gelesen
// if (row.metrics.cost_micros > 50) ...
// RICHTIG: lowerCamelCase und Umrechnung in Kontowährung
const cost = Number(row.metrics.costMicros) / 1000000;
if (cost > 50) {
console.log(row.campaign.name + ': ' + cost.toFixed(2));
}
}
// FALSCH: toISOString() liefert das Datum in UTC, nicht in Kontozeit
const wrongDate = new Date().toISOString().slice(0, 10);
// RICHTIG: Datum in der Zeitzone des Kontos
const rightDate = Utilities.formatDate(
new Date(), AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
Gegen erfundene Methoden hilft nur eines: jede Methode in der Referenz nachschlagen. Steht sie dort nicht, gibt es sie nicht, egal wie plausibel der Name klingt.
Sicherheit und Datenschutz
Keine Kundendaten in KI-Chats. Für den Code braucht das Modell die Struktur deiner Daten, nicht die Daten selbst. Suchbegriffe können Namen, Adressen oder Telefonnummern enthalten, die Menschen bei Google eingetippt haben. Kundenlisten, CRM-Exporte und E-Mail-Adressen gehören erst recht nicht in einen Chat. Wenn du zur Fehlersuche eine Log-Zeile einfügst, ersetzt du die Werte vorher durch Platzhalter.
Tarif prüfen. Ob ein Anbieter deine Eingaben weiterverwendet, hängt vom Tarif ab. Bei der Gemini API schreibt Google zum Beispiel auf der Preisseite, dass Inhalte aus dem kostenlosen Kontingent zur Verbesserung der Produkte genutzt werden, im bezahlten Tarif nicht. Was die Modelle über die API kosten, habe ich im Preisvergleich der KI-APIs zusammengestellt.
Keine Zugangsdaten in Prompts. Passwörter, Bestätigungscodes, API-Schlüssel und Entwickler-Tokens gibst du nie in einen Chat. Für Google Ads Skripte brauchst du sie auch nicht: Das Skript bekommt seinen Zugriff über „Autorisieren“ mit deinem Google-Konto. Ruft ein Skript einen externen Dienst mit Schlüssel auf, trägst du den Schlüssel selbst im Editor ein. Er ist dann für alle lesbar, die das Skript öffnen können. Wie du den Zugang zum Konto selbst schützt, steht in Google-Ads-Konto absichern.
Änderungen nur nach Review. Jedes Skript, das etwas im Konto ändert, liest ein zweiter Mensch, bevor DRY_RUN auf false geht. Halte fest, wer welches Skript autorisiert hat, denn es läuft im Namen dieser Person. Und denk an den Abbruch nach 30 Minuten: Was bis dahin geändert wurde, bleibt geändert.
Grenzen: wann API, Editor oder Regeln besser passen
Google grenzt die Werkzeuge in der Scripts-Übersicht selbst ab: Die Google Ads API ist für eigene Software und Infrastruktur gedacht, für Massenverwaltung ohne Code nennt Google automatisierte Regeln, Bulk-Uploads und den Google Ads Editor.
| Aufgabe | Besser geeignet |
|---|---|
| einfache wiederkehrende Aktion ohne eigene Logik, etwa eine Kampagne zu festen Zeiten pausieren | automatisierte Regeln |
| große einmalige Änderungen, Umbau der Kontostruktur | Google Ads Editor oder Bulk-Uploads |
| eigenes Dashboard, Datenbank, Tool für viele Konten mit eigener Oberfläche | Google Ads API |
| Prüfungen, Warnungen, Berichte in Tabellen, Regeln mit eigener Logik | Skripte |
| Aufgaben, die länger als 30 Minuten rechnen | auf mehrere Läufe aufteilen oder API |
Skripte, MCP und KI-Agenten
Ein Skript ist fester Code. Es tut bei jedem Lauf dasselbe, und du kannst vorher lesen, was es tut. Ein KI-Agent, der über MCP (Model Context Protocol) an die Google Ads API angebunden ist, entscheidet dagegen erst zur Laufzeit, welche Abfragen er stellt. Das ist flexibel für Analysefragen, aber schwerer zu kontrollieren. Für wiederkehrende Prüfungen nehme ich lieber ein einmal geprüftes Skript als einen Agenten, der jedes Mal neu entscheidet. Wie Agenten mit Google Ads arbeiten und wo ihre Grenzen liegen, zeige ich im Beitrag Google Ads mit KI-Agenten steuern.
Fazit: KI schreibt, du verantwortest
KI nimmt dir beim Skripten das Tippen ab, nicht die Verantwortung. Gib dem Modell eine genaue Aufgabe und einen Auszug aus der Referenz, lies den Code mit der Checkliste, teste in der Vorschau und denk daran, dass E-Mails und Tabellen dort trotzdem laufen. Änderungen am Konto stehen hinter DRY_RUN und gehen erst nach einem Review live, Schritt für Schritt.
Wenn du Automatisierung im Marketing grundsätzlich aufsetzen willst oder einen zweiten Blick auf KI-Code brauchst: Im KI-Consulting starte ich mit einem KI-Audit für 700 €. Willst du die Kontoführung ganz abgeben, gibt es die Google-Ads-Betreuung ab 1.000 € pro Monat, 3 Monate Startphase, danach monatlich kündbar.
Quellen
- Google for Developers, Google Ads Scripts, Übersicht: developers.google.com/google-ads/scripts/docs/start
- Google for Developers, Google Ads Scripts, Einstieg (deutsche Fassung): developers.google.com/google-ads/scripts/docs/getting-started
- Google for Developers, Google Ads Scripts, Limits: developers.google.com/google-ads/scripts/docs/limits
- Google for Developers, Google Ads Scripts, Vorschaumodus: developers.google.com/google-ads/scripts/docs/preview
- Google for Developers, Google Ads Scripts, Autorisierung: developers.google.com/google-ads/scripts/docs/authorization
- Google for Developers, Google Ads Scripts, Best Practices: developers.google.com/google-ads/scripts/docs/best-practices
- Google for Developers, Google Ads Scripts, Berichte: developers.google.com/google-ads/scripts/docs/concepts/reports
- Google for Developers, Referenz AdsApp, Account, Ad, AdSelector, CampaignSelector, Stats, ExecutionInfo: developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp
- Google for Developers, Lösungen Link Checker, Account Summary, Account Anomaly Detector: developers.google.com/google-ads/scripts/docs/solutions/link-checker
- Google for Developers, Google Ads API, GAQL-Datumsbereiche: developers.google.com/google-ads/api/docs/query/date-ranges
- Google for Developers, Google Ads API, Zero metrics: developers.google.com/google-ads/api/docs/reporting/zero-metrics
- Google for Developers, Google Ads API v25, Felder search_term_view: developers.google.com/google-ads/api/fields/v25/search_term_view
- Google for Developers, Google Ads API v25, Felder ad_group_ad: developers.google.com/google-ads/api/fields/v25/ad_group_ad
- Google for Developers, Apps Script, Kontingente: developers.google.com/apps-script/guides/services/quotas
- Google for Developers, Apps Script, UrlFetchApp: developers.google.com/apps-script/reference/url-fetch/url-fetch-app
- Google Ads Developer Blog, New Google Ads scripts experience (14.03.2022): ads-developers.googleblog.com
- Google Ads Developer Blog, Google Ads Scripts migration office hours (02.08.2022): ads-developers.googleblog.com
- Google AI for Developers, Gemini API, Preise: ai.google.dev/gemini-api/docs/pricing
Stand: 08.10.2026




