AI Agents Marketing

Der Claude-Skill, Der Meine Google-Ads-Texte Schreibt

Alejandro Rioja
Alejandro Rioja
8 Min. Lesezeit
TL;DR

Ich habe einen Claude-Skill gebaut, der die Google Ads API direkt per GAQL abfragt, responsive Suchanzeigen mit schwacher CTR oder hohem CPA erkennt und Headlines und Beschreibungen innerhalb der exakten Google-Zeichenlimits (30 und 90) umschreibt. Die neue Anzeige wird als PAUSIERT angelegt — ich prüfe und aktiviere selbst. Der eigentlich wichtige Punkt: Googles Zeichenlimits sind die echte Einschränkung, nicht der Text. Ein Modell, das Zeichen nicht korrekt zählt, produziert Anzeigen, die Google stillschweigend kürzt oder ablehnt.

Kostenloser Newsletter

Jeden Mittwoch. 28.400+ Experten. Kein Füllstoff.

[Operator-Perspektive] Ich habe bereits einen Claude-Skill, der meine Facebook-Anzeigen verwaltet — dieser Build ist hier. Google Ads war die schwierigere Fortsetzung, nicht weil die API schlechter ist, sondern weil responsive Suchanzeigen harte Zeichenlimits haben, die Facebook nicht kennt: 30 Zeichen pro Headline, 90 pro Beschreibung. Eine Umschreibung, die sich gut liest, aber bei der Auslieferung abgelehnt oder gekürzt wird, ist keine Umschreibung — das ist ein Bug. Der größte Teil der Arbeit an diesem Skill bestand darin, das Modell zu zwingen, diese Einschränkung einzuhalten, statt ihm einfach zu vertrauen.

Table of contents

Open Table of contents

Warum Google Ads einen anderen Build brauchte als Facebook

Der Facebook-Skill liest Performance-Daten, schreibt Texte um und veröffentlicht ein neues Anzeigenset. Google Ads erledigt dieselben drei Aufgaben, aber zwei Dinge sind anders:

  1. Die Abfragesprache ist GAQL, keine REST-Filter. Die Google Ads API v18 läuft auf Google Ads Query Language — SQL-artig, nur lesend, genau für diese Art von Reporting-Abfrage gebaut.
  2. Responsive Suchanzeigen haben harte Limits. Bis zu 15 Headlines (je 30 Zeichen) und 4 Beschreibungen (je 90 Zeichen) pro Anzeige. Google warnt nicht sanft — es lehnt das Asset bei der Einreichung ab oder, schlimmer, liefert eine gekürzte Version aus, die sich für einen echten Kunden falsch liest.

Punkt 2 ist der, der naive Implementierungen zerstört. Bittet man ein Modell, “eine knackigere Headline zu schreiben”, liefert es bereitwillig 34 Zeichen zurück. Google Ads lehnt dieses Asset ab oder rendert es mitten im Wort abgeschnitten. Ich merke das erst, wenn ich eine Live-Anzeige sehe, die mit “Jetzt Termin buche—” endet.

API-Zugang bekommen (der Teil, den alle unterschätzen)

Drei Dinge, und das dritte kostet eine Woche, wenn man es nicht einplant:

  1. Ein Google-Cloud-Projekt mit OAuth2-Zugangsdaten — Client-ID, Client-Secret und ein Refresh-Token mit dem Scope https://www.googleapis.com/auth/adwords. Das Refresh-Token einmal generieren, über den OAuth-Playground oder ein kurzes lokales Skript; danach ist es eine gespeicherte Zugangsdaten, kein Login-Flow mehr.
  2. Ein Developer-Token, beantragt über dein Google-Ads-Verwaltungskonto (MCC) unter Verwaltung → API-Center.
  3. Die Zugriffsebene. Jeder neue Developer-Token startet mit Testkonto-Zugriff — er funktioniert nur gegen Google-Ads-Testkonten, nicht gegen dein echtes Konto. Du beantragst Basic Access (bis zu 15.000 Operationen/Tag, dafür ausreichend) über ein Formular, das Google manuell prüft. Plane ein paar Werktage für die Genehmigung ein, bevor du auf ein echtes Konto gehen willst.

Speichere GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN und GOOGLE_ADS_CUSTOMER_ID (die 10-stellige Konto-ID, ohne Striche) in deiner .env. Verwaltest du das Konto über ein MCC, brauchst du zusätzlich GOOGLE_ADS_LOGIN_CUSTOMER_ID mit der ID des Verwaltungskontos — ohne das werden Abfragen mit einem Berechtigungsfehler abgelehnt, und die Fehlermeldung sagt nicht, warum.

Die Dateistruktur des Skills

code
.claude/skills/google-ads/
  SKILL.md          ← Anweisungen, die Claude liest
  index.ts          ← die eigentliche Tool-Implementierung
  limits.ts         ← Validierung der Zeichenlimits

SKILL.md:

markdown
# Google Ads Manager Skill

Use this skill when the user says "check my Google Ads", "run ads
report", or "rewrite underperforming RSAs". Never run this without
explicit user instruction — it touches live ad spend.

## What it can do
- Pull campaign and ad group performance via GAQL (last 7 or 30 days)
- Flag responsive search ads with CTR < 3% or CPA above the stored
  target as underperformers
- Rewrite headlines (≤30 chars) and descriptions (≤90 chars) for
  flagged ads in Ale's voice
- Create new ad variations as PAUSED — never enabled automatically

## What it will NOT do
- Change budgets or bids without explicit confirmation
- Enable a paused ad
- Submit any headline or description that fails the character check

Die letzte Zeile unter “What it will NOT do” habe ich hinzugefügt, nachdem der Skill in der ersten Woche eine 32-Zeichen-Headline veröffentlicht hat. Die eigene Zählung des Modells lag um zwei Zeichen daneben — nah genug, um bei einem flüchtigen Blick durchzugehen, weit genug, damit das Asset abgelehnt wurde. Seitdem läuft alles durch einen harten Validator, nicht durch die Selbstauskunft des Modells.

Der TypeScript-Kerncode

typescript
// .claude/skills/google-ads/index.ts
import { GoogleAdsApi } from "google-ads-api";
import Anthropic from "@anthropic-ai/sdk";

const client = new GoogleAdsApi({
  client_id: process.env.GOOGLE_ADS_CLIENT_ID!,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET!,
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
});

const customer = client.Customer({
  customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
  login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
});

interface AdPerformance {
  adGroupAdId: string;
  campaignName: string;
  headlines: string[];
  descriptions: string[];
  impressions: number;
  clicks: number;
  costMicros: number;
  conversions: number;
  ctr: number;
  cpa: number;
}

async function getAdPerformance(days = 7): Promise<AdPerformance[]> {
  const rows = await customer.query(`
    SELECT
      ad_group_ad.ad.id,
      campaign.name,
      ad_group_ad.ad.responsive_search_ad.headlines,
      ad_group_ad.ad.responsive_search_ad.descriptions,
      metrics.impressions,
      metrics.clicks,
      metrics.cost_micros,
      metrics.conversions
    FROM ad_group_ad
    WHERE segments.date DURING LAST_${days === 30 ? "30" : "7"}_DAYS
      AND ad_group_ad.status = 'ENABLED'
  `);

  return rows.map((r: any) => {
    const impressions = Number(r.metrics.impressions ?? 0);
    const clicks = Number(r.metrics.clicks ?? 0);
    const cost = Number(r.metrics.cost_micros ?? 0) / 1_000_000;
    const conversions = Number(r.metrics.conversions ?? 0);
    return {
      adGroupAdId: r.ad_group_ad.ad.id,
      campaignName: r.campaign.name,
      headlines: (r.ad_group_ad.ad.responsive_search_ad?.headlines ?? []).map((h: any) => h.text),
      descriptions: (r.ad_group_ad.ad.responsive_search_ad?.descriptions ?? []).map((d: any) => d.text),
      impressions,
      clicks,
      costMicros: Number(r.metrics.cost_micros ?? 0),
      conversions,
      ctr: impressions > 0 ? (clicks / impressions) * 100 : 0,
      cpa: conversions > 0 ? cost / conversions : Infinity,
    };
  });
}

const HEADLINE_MAX = 30;
const DESCRIPTION_MAX = 90;

function validateAsset(text: string, max: number): boolean {
  return text.length <= max;
}

async function rewriteRsa(
  ad: AdPerformance,
  targetCpa: number,
): Promise<{ headlines: string[]; descriptions: string[] }> {
  const anthropic = new Anthropic();
  const msg = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: `Rewrite this Google Ads responsive search ad in Alejandro Rioja's voice: direct, operator-focused, specific. It's underperforming — CTR ${ad.ctr.toFixed(2)}%, CPA $${ad.cpa.toFixed(2)} vs a $${targetCpa} target.

Current headlines: ${JSON.stringify(ad.headlines)}
Current descriptions: ${JSON.stringify(ad.descriptions)}

Rules, non-negotiable:
- Each headline: 30 characters MAXIMUM, including spaces and punctuation
- Each description: 90 characters MAXIMUM, including spaces and punctuation
- Return 5 headlines and 4 descriptions
- Return JSON only: {"headlines": ["...", ...], "descriptions": ["...", ...]}`,
    }],
  });
  const text = (msg.content[0] as any).text.replace(/```json\n?/, "").replace(/```/, "").trim();
  const parsed = JSON.parse(text);

  const headlines = parsed.headlines.filter((h: string) => validateAsset(h, HEADLINE_MAX));
  const descriptions = parsed.descriptions.filter((d: string) => validateAsset(d, DESCRIPTION_MAX));

  if (headlines.length < 3 || descriptions.length < 2) {
    throw new Error("Model returned too many over-limit assets — rerun or revise manually.");
  }

  return { headlines, descriptions };
}

Jede Headline und Beschreibung wird im Code gegen das Limit geprüft, nach der Generierung, bevor irgendetwas die API berührt. Die Umschreibung des Modells ist ein Entwurf. Der Validator ist das Tor.

Wie ich ihn im Alltag nutze

code
> prüfe mein Google Ads der letzten 7 Tage

Claude führt die GAQL-Abfrage aus, markiert alles unter 3 % CTR oder über meinem gespeicherten CPA-Ziel und fragt vor dem Umschreiben nach. Ich bestätige. Es generiert neue Headlines und Beschreibungen, lässt sie durch den Zeichenvalidator laufen, verwirft, was durchfällt, und legt über die Mutate-Operation der Google-Ads-API eine neue Anzeige im Status PAUSED an. Ich vergleiche den neuen Text mit dem alten direkt in Google Ads, aktiviere die, die ich wirklich schalten würde, und lasse den Rest pausiert.

Gleiche Obergrenze wie bei der Facebook-Version: etwa 20 Minuten statt eines ganzen Nachmittags in der Oberfläche, weil der mechanische Teil — Daten ziehen, innerhalb einer harten Einschränkung formulieren, die Änderung vorbereiten — genau das ist, was ein Modell statt mir tun sollte.

Was der Validator nicht erkennt

Zeichenzählung ist mechanisch. Relevanz ist es nicht. Eine Headline kann 28 Zeichen haben, jede Prüfung bestehen und trotzdem die falsche Botschaft für den Suchbegriff sein, dem sie zugeordnet ist. Der Skill kann nicht erkennen, ob eine schwach performende Anzeigengruppe ein schlechtes Angebot, eine Diskrepanz zur Landingpage oder wirklich schwachen Text hat — er kann mir nur sagen, dass der Text schwach ist im Vergleich zu den Anzeigen um ihn herum. Wenn alle Anzeigen einer Kampagne eine niedrige CTR haben, ist das ein Targeting- oder Landingpage-Problem, und dem Skill mehr Umschreibungszyklen für eine falsch zugeordnete Suchintention zu geben, produziert nur konformeren, aber genauso ignorierten Text.

Ich lese die GAQL-Ausgabe selbst, bevor ich eine Umschreibungs-Charge freigebe, genau wie auf der Facebook-Seite. Der Skill nimmt mir das Tippen und das Zeichenzählen ab. Er nimmt mir nicht die Beurteilung ab, was tatsächlich kaputt ist.

Die Tools, die ich dafür nutze

Claude ist die Basis für beide Anzeigen-Skills — den für Facebook und diesen hier — sie teilen sich dasselbe Muster aus Umschreiben und Validieren, nur gegen eine andere API und ein engeres Zeichenbudget.

Google Ads API (npm-Paket google-ads-api) — der TypeScript-Wrapper um die rohe gRPC/REST-API. Er übernimmt den OAuth-Refresh und den Aufbau der GAQL-Abfragen, damit man kein Protobuf von Hand zusammenbauen muss.

FAQ

Brauche ich Basic Access, oder reicht der Testkonto-Zugriff?

Testkonto-Zugriff funktioniert nur gegen Google-Ads-Testkonten — nützlich für die Entwicklung, nutzlos für ein echtes Konto mit echtem Budget. Du brauchst Basic Access (im API-Center deines Verwaltungskontos beantragen), bevor dieser Skill eine Live-Kampagne berühren kann. Die Genehmigung ist manuelle Prüfung, nicht sofort — also beantrage sie, bevor du sie brauchst, nicht in der Woche, in der du starten willst.

Warum 5 Headlines und 4 Beschreibungen statt der Maximalwerte 15 und 4?

Google erlaubt bis zu 15 Headlines, aber sowohl Ad Strength als auch reale Tests legen nahe, dass der Ertrag ab 8–10 wirklich unterschiedlichen Ansätzen sinkt — die meisten Konten landen bei mehreren fast identischen Headlines, die nur die Zahl auffüllen. Ich begrenze die Modellausgabe auf 5 neue Headlines pro Umschreibungs-Charge, damit ich wirklich unterschiedliche Optionen prüfe, statt neun Varianten desselben Satzes zu filtern.

Was passiert, wenn die Umschreibung des Modells bei jedem Versuch die Zeichenprüfung nicht besteht?

Der Validator wirft einen Fehler, statt ein zu langes Asset stillschweigend zu kürzen oder einzureichen. In der Praxis bedeutet das: Ich lasse den Prompt einmal neu laufen, und wenn er zweimal scheitert, schreibe ich genau diese Headline von Hand. Das passiert häufiger bei Beschreibungen — 90 Zeichen sind für einen vollständigen Satz enger, als es wirkt.

Außerdem relevant: für reines Reporting über GA4, Search Console, Bing, Ahrefs und Core Web Vitals in einem einzigen Claude-Connector, statt für jede Plattform einen eigenen Skill zu bauen, nutze ich SiteWide MCP — dieselbe Idee, Claude mit echten Marketingdaten zu verbinden, nur mit breiterer Abdeckung als eine einzelne Anzeigenplattform.

Das Fazit des Operators

Wer responsive Suchanzeigen-Varianten von Hand schreibt und selbst Zeichen zählt, erledigt genau die Art von mechanischer, regelgebundener Aufgabe, die ein Modell übernehmen sollte. Der Teil, der echtes Urteilsvermögen braucht — ob es ein Text- oder ein Targeting-Problem ist — bleibt bei dir. Baue den Skill, lass ihn gegen Googles Limits formulieren und validieren, und behalte die Diagnose für dich.

Werbung
Weiterlesen

Ähnliche Beiträge

Weiterlesen

Holen Sie sich das KI-Playbook in Ihr Postfach

Jeden Mittwoch. 28.400+ Experten. Kein Füllstoff.

↵ ↵ alle Ergebnisse anzeigen esc esc zum Schließen