AI Agents Marketing

La Skill de Claude Que Escribe Mis Anuncios de Google Ads

Alejandro Rioja
Alejandro Rioja
9 min de lectura
TL;DR

Construí una skill de Claude que consulta la API de Google Ads directamente con GAQL, detecta anuncios de búsqueda adaptables con CTR débil o CPA alto, y reescribe titulares y descripciones dentro de los límites exactos de caracteres de Google (30 y 90). Crea el nuevo anuncio en estado PAUSADO — yo reviso y lo activo. La parte que realmente importa: los límites de caracteres de Google son la restricción real, no el copy. Un modelo que no cuenta caracteres correctamente produce anuncios que Google recorta o rechaza en silencio.

Newsletter gratuita

Cada miércoles. 28.400+ operadores. Sin relleno.

[Lectura de operador] Ya tengo una skill de Claude que gestiona mis anuncios de Facebook — esa construcción está aquí. Google Ads fue la secuela más difícil, no porque la API sea peor, sino porque los anuncios de búsqueda adaptables tienen límites de caracteres estrictos que Facebook no tiene: 30 caracteres por titular, 90 por descripción. Una reescritura que se lee bien pero es rechazada o recortada al momento de servirse no es una reescritura, es un bug. La mayor parte del trabajo en esta skill fue forzar al modelo a respetar esa restricción en lugar de confiar en que lo hiciera.

Table of contents

Open Table of contents

Por qué Google Ads necesitaba una construcción distinta a Facebook

La skill de Facebook lee datos de rendimiento, reescribe el copy y publica un nuevo conjunto de anuncios. Google Ads hace los mismos tres trabajos, pero dos cosas cambian:

  1. El lenguaje de consulta es GAQL, no filtros REST. La API de Google Ads v18 funciona con Google Ads Query Language — con forma de SQL, de solo lectura, construido exactamente para este tipo de extracción de reportes.
  2. Los anuncios de búsqueda adaptables tienen límites estrictos. Hasta 15 titulares (30 caracteres cada uno) y 4 descripciones (90 caracteres cada una) por anuncio. Google no avisa con delicadeza — o rechaza el elemento al enviarlo o, peor, sirve una versión recortada que se lee mal a un cliente real.

El punto 2 es el que rompe las implementaciones ingenuas. Pídele a un modelo que “escriba un titular más contundente” y con gusto te devolverá 34 caracteres. Google Ads rechaza ese elemento o lo renderiza cortado a mitad de palabra. Yo no me entero hasta que estoy viendo un anuncio en vivo que termina en “Reserva tu cit—”.

Conseguir acceso a la API (la parte que todos subestiman)

Tres cosas, y la tercera es la que te cuesta una semana si no la planificas:

  1. Un proyecto de Google Cloud con credenciales OAuth2 — client ID, client secret y un refresh token con el permiso https://www.googleapis.com/auth/adwords. Genera el refresh token una vez, vía el OAuth playground o un script local corto; después de eso es una credencial guardada, no un flujo de inicio de sesión.
  2. Un developer token, solicitado desde tu cuenta de administrador (MCC) de Google Ads, en Administración → Centro de API.
  3. Nivel de acceso. Todo developer token nuevo empieza con acceso de Cuenta de prueba — solo funciona contra cuentas de prueba de Google Ads, no contra tu cuenta real. Solicitas Basic Access (hasta 15.000 operaciones/día, suficiente para esto) mediante un formulario que Google revisa manualmente. Reserva unos días hábiles para la aprobación antes de planear activar esto en una cuenta real.

Guarda GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN y GOOGLE_ADS_CUSTOMER_ID (el ID de cuenta de 10 dígitos, sin guiones) en tu .env. Si gestionas la cuenta a través de un MCC, también necesitas GOOGLE_ADS_LOGIN_CUSTOMER_ID con el ID de la cuenta administradora — las consultas se rechazan con un error de permisos sin esto, y el mensaje de error no dice por qué.

La estructura de archivos de la skill

code
.claude/skills/google-ads/
  SKILL.md          ← instrucciones que lee Claude
  index.ts          ← la implementación real de la herramienta
  limits.ts         ← validación de límites de caracteres

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

La última línea de “What it will NOT do” es la que agregué después de que la skill publicara un titular de 32 caracteres en la primera semana. El conteo del propio modelo estaba mal por dos caracteres — lo suficientemente cerca para pasar una mirada rápida, lo suficientemente lejos para que el elemento fuera rechazado. Desde entonces todo pasa por un validador estricto, no por el autorreporte del modelo.

El código TypeScript central

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 };
}

Cada titular y descripción se valida contra el límite en código, después de generarse y antes de tocar la API. La reescritura del modelo es un borrador. El validador es el filtro.

Cómo la uso en el día a día

code
> revisa mi Google Ads de los últimos 7 días

Claude ejecuta la consulta GAQL, marca cualquier cosa por debajo de 3% de CTR o por encima de mi CPA objetivo guardado, y pregunta antes de reescribir. Confirmo. Genera nuevos titulares y descripciones, los pasa por el validador de caracteres, descarta lo que falla y crea un nuevo anuncio en estado PAUSED mediante la operación mutate de la API de Google Ads. Reviso el copy nuevo contra el anterior dentro de Google Ads, activo los que de verdad usaría, y dejo el resto pausado.

Mismo techo que la versión de Facebook: unos 20 minutos en vez de una tarde entera en la interfaz, porque la parte mecánica — extraer los datos, redactar dentro de una restricción estricta, dejar el cambio listo — es exactamente lo que un modelo debería estar haciendo en lugar de yo.

Lo que el validador no detecta

El conteo de caracteres es mecánico. La relevancia no lo es. Un titular puede tener 28 caracteres, pasar todas las validaciones, y seguir siendo el mensaje equivocado para el término de búsqueda al que está vinculado. La skill no puede saber si un grupo de anuncios con bajo rendimiento tiene una mala oferta, un desajuste con la landing page o un copy genuinamente débil — solo puede decirme que el copy es débil en relación con los anuncios que lo rodean. Si todos los anuncios de una campaña tienen un CTR bajo, eso es un problema de segmentación o de landing page, y darle a la skill más ciclos de reescritura sobre una intención de búsqueda mal emparejada solo produce copy más obediente e igual de ignorado.

Reviso la salida de GAQL yo mismo antes de aprobar un lote de reescrituras, igual que hago del lado de Facebook. La skill elimina la escritura y el conteo de caracteres. No elimina el criterio sobre qué está realmente roto.

Las herramientas que uso para esto

Claude es sobre lo que corren ambas skills de anuncios — la de Facebook y esta — comparten el mismo patrón de reescribir-y-validar, solo contra una API distinta y un presupuesto de caracteres más ajustado.

Google Ads API (paquete npm google-ads-api) — el wrapper en TypeScript sobre la API cruda gRPC/REST. Maneja el refresh de OAuth y la construcción de consultas GAQL para que no tengas que lidiar con protobuf a mano.

Preguntas frecuentes

¿Necesito Basic Access, o funciona el acceso de Cuenta de prueba?

El acceso de Cuenta de prueba solo funciona contra cuentas de prueba de Google Ads — útil para desarrollo, inútil para una cuenta real con gasto real. Necesitas Basic Access (solicítalo en el Centro de API de tu cuenta administradora) antes de que esta skill pueda tocar una campaña en vivo. La aprobación es revisión manual, no instantánea, así que solicítala antes de necesitarla, no la semana que quieres lanzar.

¿Por qué 5 titulares y 4 descripciones en vez de los máximos de 15 y 4?

Google permite hasta 15 titulares, pero tanto el Ad Strength como las pruebas reales sugieren rendimientos decrecientes pasados los 8–10 ángulos genuinamente distintos — la mayoría de las cuentas terminan con varios titulares casi duplicados rellenando el conteo. Limito la salida del modelo a 5 titulares nuevos por lote de reescritura para revisar opciones distintas, no filtrar nueve variaciones de la misma frase.

¿Qué pasa si la reescritura del modelo falla la validación de caracteres en todos los intentos?

El validador lanza un error en vez de recortar o enviar en silencio un elemento fuera de límite. En la práctica esto significa que vuelvo a correr el prompt una vez, y si falla dos veces escribo ese titular específico a mano. Pasa más seguido con las descripciones — 90 caracteres es un espacio más ajustado para una oración completa de lo que parece.

Relacionado: para reportes de solo lectura en GA4, Search Console, Bing, Ahrefs y Core Web Vitals en un solo conector de Claude en vez de construir una skill a medida por plataforma, uso SiteWide MCP — la misma idea de conectar Claude a datos de marketing reales, pero con una superficie más amplia que una sola plataforma de anuncios.

El veredicto del operador

Si estás escribiendo a mano variaciones de anuncios de búsqueda adaptables y contando caracteres tú mismo, eso es exactamente el tipo de tarea mecánica y basada en reglas que un modelo debería asumir. La parte que requiere criterio real — si es un problema de copy o de segmentación — sigue siendo tuya. Construye la skill, déjala redactar y validar contra los límites de Google, y guárdate el diagnóstico para ti.

Publicidad
Seguir leyendo

Artículos relacionados

Seguir leyendo

Recibe el manual de IA en tu buzón

Cada miércoles. 28.400+ operadores. Sin relleno.

↵ ↵ para ver todos los resultados esc esc para cerrar