plugins/maccing-growth/skills/google-ads/SKILL.md
Use when managing Google Ads campaigns, creating ads, optimizing campaigns, checking metrics, or automating Google Ads via Scripts. Triggers on "google ads", "google ads campaign", "create campaign", "ad campaign", "google ads scripts", "check google ads", "google ads metrics", "campaign performance", "AdsApp", "automation", "adspower".
npx skillsauth add andredezzy/maccing google-adsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scripts-first approach to Google Ads. Google Ads Scripts run inside the Google Ads UI with zero setup — no developer token, no OAuth, no approval. Use Scripts for everything. Browser automation (Playwriter) is only needed to apply Google's built-in recommendations.
Project data lives in .maccing/growth/google-ads/<account>/README.md. This file contains only generic, reusable platform knowledge.
BEFORE ANY ACTION, ALWAYS READ THE PROJECT README.
.maccing/growth/google-ads/<account>/README.md contains current state:
campaign IDs, budgets, negatives, performance, pending actions.
Without reading it, you WILL operate on stale data.
ALWAYS USE GOOGLE ADS SCRIPTS FOR WRITE AND READ OPERATIONS.
PLAYWRITER IS ONLY FOR APPLYING GOOGLE RECOMMENDATIONS.
Priority order:
AdsApp.mutate / AdsApp.search) — create ads, add keywords, read metrics, manage campaigns. Reliable, requires zero approval, works immediately.google-ads MCP server) — secondary option when developer token is approved. Same power as Scripts but callable directly from Claude.NEVER guide the user through manual Google Ads UI steps when Scripts can do it.
After every session: update patterns, note new script techniques, record gotchas.
WHEN GUIDING ANY MANUAL ACTION, PROVIDE THE FULL CLICK PATH.
NEVER say "go to Conversions". ALWAYS say the exact sequence.
Every manual instruction MUST include:
Format: Sidebar Item → Sub-menu → Page Element → Action → Confirm
Conversion goals UI structure (confirmed 2026-05-05):
metrics.conversions takes days to recalculate after changing Primary→Secondary| Entity | Format | Example |
|---|---|---|
| Campaign | Brand - Product - Channel - Geo | Acme Brand - Search - PH/PK/BD |
| Ad Group | [Theme] - [Match Type] | Earning Keywords - Broad |
| Script file | NN-kebab-description.js | 33-campaign-optimize.js |
| Negative keyword | Lowercase, broad match default | without investment |
Split ad groups by match type for granular control:
A single script can chain multiple operation types reliably:
1. campaignCriterionOperation.create (negatives) via mutateAll
2. GAQL query → adGroupCriterionOperation.update (pause keywords) via mutateAll
3. adGroupCriterionOperation.create (add keywords) via mutateAll
4. adGroupAdOperation.remove (delete ad) via mutate
5. adGroupAdOperation.create (new RSA) via mutate
6. adGroupOperation.update (rename) via mutate
7. adGroupOperation.create (new ad group) via mutate
Use AdsApp.mutateAll() for batch (arrays), AdsApp.mutate() for single operations. Chain sequentially. If step N fails, return to stop the script.
| Field | Issue | Fix |
|---|---|---|
| quality_info.qualityScore | TypeError if undefined | Don't include in SELECT, query separately |
| ad_relevance, landing_page_experience | Invalid in Scripts API | Remove from queries |
| metrics.conversions | Inflated after Primary→Secondary change | Wait 7+ days for recalculation |
| REGEXP_MATCH | Works for keyword text filtering | Use for pattern-based queries |
| metrics.conversions with conversion_action | PROHIBITED_METRIC error | Use all_conversions or query from campaign with segments.conversion_action_name |
Broad match negatives can silently block positive keywords. Example: negative "jobs from home" (BROAD) blocks positive "online jobs from home". Google shows this in Recommendations → "Remover palavras-chave negativas em conflito" but does NOT prevent serving — it just wastes budget on zero impressions for those keywords.
Always audit: after adding negatives, cross-check against positive keywords. A broad negative matches any query containing those words in any order.
When the business model has a free tier → paid conversion funnel:
Claude reads the template from the plugin's skills/google-ads/scripts/ directory
Claude fills in CONFIG variables (IDs, ad copy, keywords) from the project README
Claude gives the script to the user using this EXACT format:
Script: script-name
Arquivo: .maccing/growth/google-ads/<account>/scripts/NN-script-name.js
Copiar: cat .maccing/growth/google-ads/<account>/scripts/NN-script-name.js | pbcopy
ALWAYS provide all three lines. Never skip any.
User opens Google Ads → Ferramentas → Scripts → Novo script
User names the script, pastes content, clicks Executar (NOT Visualizar — Preview is read-only and blocks all mutations)
For read scripts: user copies the Logger output back to Claude
For write scripts: script logs success/failure per operation
| Task | Scripts | MCP (when approved) | Playwriter |
|---|---|---|---|
| Create RSA ads | ✅ AdsApp.mutate | ✅ | ❌ Angular overlay |
| Add keywords | ✅ AdsApp.mutateAll | ✅ | ⚠️ FAB works but fragile |
| Read metrics | ✅ AdsApp.search GAQL | ✅ GAQL | ✅ innerText |
| Apply recommendations | ❌ No API | ❌ | ✅ Only way |
| Create conversion actions | ✅ AdsApp.mutate | ✅ | ❌ Blocked |
| Pause/resume campaigns | ✅ AdsApp.mutate | ✅ | ✅ Status toggle |
| Remove negative keywords | ✅ AdsApp.mutate | ✅ | ❌ No bulk UI |
| Update ad URLs | ✅ AdsApp.mutate | ✅ | ❌ |
| Add negative keywords | ✅ AdsApp.mutateAll | ✅ | ⚠️ Campaign picker broken |
| Modify conversion goals | ❌ ALL approaches fail | ❌ Same API | ❌ |
| Remove campaigns | ❌ Campaigns can only be PAUSED | ✅ | ✅ |
| Create sitelinks | ✅ AdsApp.mutate | ✅ | ❌ 16-field form corrupts |
Works:
campaignOperation create/update (name, status, budget, bidding) — confirmedadGroupCriterionOperation update (pause/enable keywords) — confirmedcampaignCriterionOperation create (add negative keywords) — confirmedcampaignCriterionOperation remove (delete negative keywords) — confirmedadGroupAdOperation create (create RSA ads) — confirmedadGroupAdOperation remove (delete ads) — confirmedconversionActionOperation create (create new conversion action) — confirmedadGroupOperation create/update (create ad group, rename) — confirmedassetOperation create (sitelinks) — confirmedcampaignAssetOperation create/remove (link/unlink sitelinks to campaigns) — confirmedassetOperation remove → NOT supported. Assets can only be unlinked via campaignAssetOperation.removeAlso works (confirmed 2026-05-12):
conversionActionOperation update name — confirmed (renamed 4 conversion actions in single mutateAll)campaignCriterionOperation create with location (add geo targets) — confirmedcampaignBudgetOperation update amountMicros — confirmedcampaign.finalUrlSuffix for UTM parameters — confirmed (set on 3 campaigns, verified via GAQL query)GAQL gotcha (confirmed 2026-05-13):
geographic_view queries require campaign.status in SELECT clause when filtering by campaign.status in WHERE — otherwise EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE errorDoes NOT work:
conversionActionOperation update/remove on WEBPAGE_CODELESS type → MUTATE_NOT_ALLOWEDconversionActionOperation update status to DISABLED → generic errorcustomerConversionGoalOperation (update biddable) → generic errorcampaignConversionGoalOperation (update biddable) → generic errorcampaignOperation with conversionGoalCampaignConfig → generic errorcampaignOperation.remove → generic error. Campaigns can only be paused.Classic AdsApp API (always works as fallback):
var campaigns = AdsApp.campaigns().withCondition("campaign.id = 123").get();
campaigns.next().enable(); // or .pause()
// NOTE: .remove() does NOT exist on campaign objects in Google Ads Scripts
// Bidding strategy change (classic API only, mutate update fails):
campaigns.next().bidding().setStrategy("TARGET_SPEND", { cpcBidCeiling: 2.50 });
// TARGET_SPEND = Maximize Clicks. "MAXIMIZE_CLICKS" is NOT valid.
AdsApp.mutate() return values:
result.isSuccessful() — booleanresult.getErrorMessages() — array of stringsgetReturnValue() and getReturnedResourceName() both don't exist)mutateAll (see below)Temp resource names (confirmed working):
Chain dependent creates in a single mutateAll batch using negative IDs:
var ops = [
{ campaignBudgetOperation: { create: { resourceName: "customers/CID/campaignBudgets/-1", ... } } },
{ campaignOperation: { create: { resourceName: "customers/CID/campaigns/-2", campaignBudget: "customers/CID/campaignBudgets/-1", ... } } },
{ adGroupOperation: { create: { resourceName: "customers/CID/adGroups/-3", campaign: "customers/CID/campaigns/-2", ... } } },
{ adGroupCriterionOperation: { create: { adGroup: "customers/CID/adGroups/-3", keyword: {...} } } },
{ adGroupAdOperation: { create: { adGroup: "customers/CID/adGroups/-3", ad: {...} } } }
];
AdsApp.mutateAll(ops); // All resolved in one batch
Then query by name to get real IDs: SELECT campaign.id FROM campaign WHERE campaign.name = '...'
WEBPAGE_CODELESS conversion actions are completely immutable via any API. Cannot disable, remove, change status, or modify biddable/primaryForGoal. This is a hard Google platform restriction.
Solution: Add send_page_view: false to gtag('config', ...) in the website code. This stops the codeless conversion from firing at the source.
Changing primaryForGoal / otimização de ações CANNOT be done via Scripts or API. Must be done manually:
Metas (sidebar, ícone troféu) → Conversões → Resumo
→ clica no nome da conversão (texto azul)
→ página de detalhes abre com abas "Detalhes" e "Configurações"
→ clica "Editar configurações" (botão azul, canto inferior direito da seção Configurações)
→ seção "Otimização de ações" expande com 2 radio buttons:
○ "Ação primária utilizada para otimização de lances"
● "Ação secundária não utilizada para otimização de lances"
→ seleciona a opção desejada → Salvar
campaignOperation: {
create: {
name: "...",
status: "PAUSED",
advertisingChannelType: "SEARCH",
campaignBudget: budgetResourceName,
maximizeConversions: {}, // NOT biddingStrategyType
containsEuPoliticalAdvertising: "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING", // REQUIRED
networkSettings: {
targetGoogleSearch: true,
targetSearchNetwork: false,
targetContentNetwork: false
}
}
}
biddingStrategyType: "MAXIMIZE_CONVERSIONS" does NOT work → use maximizeConversions: {} objectmaximizeClicks does NOT exist in v23 → use targetSpend: { cpcBidCeilingMicros: "1500000" } for Maximize ClickscontainsEuPoliticalAdvertising is REQUIRED even for non-EU targeting"DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING" (not "ADS", not boolean)explicitlyShared: false for Maximize Conversions biddingcampaignOperation.create fails if a PAUSED campaign with same name existsfinalUrls belongs to the Asset object (sibling of sitelinkAsset), NOT inside sitelinkAsset.
// WRONG
{ sitelinkAsset: { linkText: "...", finalUrls: ["url"] } }
// RIGHT
{ sitelinkAsset: { linkText: "...", description1: "...", description2: "..." }, finalUrls: ["url"] }
| Trigger | Policy | Example | |---|---|---| | "win", "winning" in descriptions | Declarações não confiáveis | "Play and win daily" | | Financial terms in landing page | MISLEADING_CONTENT | /invest with "profit sharing" | | "profit", "returns", "guaranteed" | Unreliable claims | "Daily profit sharing" |
Fix pattern: sitelink assets are immutable — create a new compliant asset, unlink old from campaigns, link new. Old asset stays in account (cannot be deleted).
| Element | Max Length | |---|---| | Headline | 30 chars | | Description | 90 chars | | Path1 | 15 chars | | Path2 | 15 chars |
ALWAYS count characters before creating RSA ads. The API gives generic "Too long" error.
analytics.google.com (NOT ads.google.com)
→ Create property with correct timezone + currency
→ Configure web data stream
→ Copy Measurement ID (format: G-XXXXXXXXXX)
analytics.google.com → Admin → Property → Data Streams → click stream
→ Events section → Measurement Protocol API secrets → Create
analytics.google.com → Admin → Product links → Google Ads → Link
→ Select Google Ads account → Confirm
Frontend (GA4 tag) ──cross-domain linker──▶ App (GA4 tag)
│
captures gaClientId + gaSessionId
│
▼
Backend: UserAttribution record
│
ConversionTrackingListener
USER_REGISTERED → GA4 MP "sign_up"
CONTRACT_ACTIVATED → GA4 MP "purchase" + value
│
▼
GA4 ──linked──▶ Google Ads
engagement_time_msec: 100 required in every MP event or GA4 may ignore ittransaction_id in purchase events = deduplication keyhttps://www.google-analytics.com/debug/mp/collectgtag('get', 'G-XXX', 'client_id', cb) API instead of parsing _ga cookieNote: the enforcement patterns below are practitioner-observed, not official Google policy. Verify against ads.google.com policy before acting.
Dangerous terms on landing page: "trading", "investment", "deposit", "portfolio", "returns", "profit", "earnings", "withdrawal"
Safe terms: "community", "membership", "operations", "progress tracking", "participants"
Ad copy triggers: "earn", "income", "get paid", "profit sharing", "daily returns", "trusted", "proven track record", "no hidden fees", "grow your money", "secure your future"
Critical: each step only saves on "Avançar". Clicking sidebar steps loses data.
Server: grantweston/google-ads-mcp-complete v2.0.0
Required credentials:
await state.page.goto("https://ads.google.com/aw/recommendations?ocid=<ACCOUNT_ID>");
await state.page.waitForLoadState("networkidle");
await state.page.evaluate(() => {
const options = document.querySelectorAll("[role=option]");
for (const opt of options) {
if (opt.textContent?.includes("TARGET_RECOMMENDATION_TEXT")) {
const btns = opt.querySelectorAll("[role=button], button");
for (const btn of btns) {
if (btn.textContent?.trim() === "Aplicar") {
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
break;
}
}
break;
}
}
});
await state.page.locator('role=button[name="Aplicar"] >> nth=1').click({ force: true });
Google Ads wraps everything in acx-overlay-container. Always use page.evaluate(() => element.click()) or dispatchEvent.
Base: https://ads.google.com — replace <ocid> with the account's Customer ID.
| Destination | Path |
|---|---|
| Overview | /aw/overview?ocid=<ocid> |
| Campaigns list | /aw/campaigns?ocid=<ocid> |
| Ad Groups | /aw/adgroups?ocid=<ocid> |
| Ads list | /aw/ads?ocid=<ocid> |
| Keywords | /aw/keywords?ocid=<ocid> |
| Campaign Settings | /aw/campaignsettings?ocid=<ocid> |
| Conversions | /aw/conversions?ocid=<ocid> |
| Account (Verification) | /aw/policy/account?ocid=<ocid> |
| Sitelink Extension | /aw/adextensions/new?ocid=<ocid>&placeholderType=1&assetFieldType=31 |
| Callout Extension | /aw/adextensions/new?ocid=<ocid>&placeholderType=17&assetFieldType=32 |
| Search Terms Report | /aw/keywords/searchterms?ocid=<ocid> |
| Negative Keywords | /aw/keywords/negative?ocid=<ocid> |
| Recommendations | /aw/recommendations?ocid=<ocid> |
| API Center (MCC only) | /aw/apicenter |
| Manager Accounts | https://ads.google.com/home/tools/manager-accounts/ |
| Country | ID | |---|---| | Bangladesh | 2050 | | India | 2356 | | Indonesia | 2360 | | Malaysia | 2458 | | Pakistan | 2586 | | Philippines | 2608 | | Thailand | 2764 |
| Intent | Reference | Use for |
|---|---|---|
| Automating Google Ads: Scripts/API-first, the narrow UI carve-out, official-surface decision tree | reference/automation.md | Choosing API vs browser vs operator for Google Ads tasks; staying undetectable when browser is needed |
All templates in .maccing/growth/google-ads/<account>/scripts/ (copy from plugin skills/google-ads/scripts/).
| Script | Description |
|---|---|
| read-full-audit.js | Full account audit: campaigns, ad groups, ads, keywords, search terms, conversions |
| read-campaign-performance.js | Campaign metrics + budget + bidding strategy |
| read-keyword-performance.js | Keywords with Quality Score breakdown |
| read-search-terms.js | Search queries with wasted spend and converting term filters |
| read-conversion-actions.js | All conversion actions with attribution settings |
| read-ad-details.js | RSA headlines/descriptions with pin positions, ad strength |
| write-create-rsa.js | Create RSA in existing ad group (PAUSED state) |
| write-add-keywords.js | Add keywords via AdsApp.mutateAll |
| write-add-negatives.js | Campaign-level negative keywords |
| write-pause-campaign.js | Pause or resume campaign |
| write-update-ad-url.js | Update Final URL of an existing ad |
| write-create-conversion.js | Create new conversion action |
tools
Use when working with André's self-hosted Google Workspace MCP (the `google-workspace` plugin) — driving Calendar, Gmail, Drive, Docs, Sheets, Slides, Forms, Tasks, Chat, or Contacts via the `mcp__plugin_google-workspace_workspace__*` tools, OR setting up / troubleshooting its OAuth (first-run consent, 7-day test-mode re-auth, credential storage). Covers the account-isolation rule (never use the `mcp__claude_ai_*` Google connectors — different account).
tools
Use when working with the Notion API or MCP — creating, editing, querying, or moving databases, data sources, pages, views (table/board/gallery/chart), formulas, rollups, relations, blocks, icons, or covers; or hitting Notion API/MCP errors (validation_error, pagination, permission, 400/409).
tools
YCloud — a multi-channel communications provider (CPaaS: WhatsApp, SMS, Voice, Email), not a Meta-only BSP. This skill covers its WhatsApp Business operations: console navigation, account creation/onboarding, Embedded Signup, campaigns/inbox/journeys, auto-unsubscribe chatbot, the public-API-vs-dashboard-backend distinction, BSP migration, and read-only CDP automation. Use when operating YCloud for WhatsApp dispatch: embedded signup, campaign sends, campaign analytics, inbox, auto-unsubscribe chatbot, opt-out attribution, dashboard automation. Triggers on: 'ycloud', 'CPaaS', 'BSP', 'bulk campaign', 'whatsapp dashboard', 'embedded signup', 'auto-unsubscribe', 'opt-out chatbot', 'campaign analytics', 'dispatch automation', 'ycloud free plan', 'zero markup', 'ycloud account creation', 'ycloud onboarding', 'ycloud signup code'.
development
YCloud v2 REST API reference for WhatsApp messaging via the BSP layer. Covers every callable endpoint: sending and listing messages (async and sendDirectly), template CRUD, phone number and WABA metadata, wallet balance, webhook management, contacts, unsubscribers/opt-outs, and media upload. Includes live-verified behavior deviations, pagination gotchas, and filter limitations. Use when calling the YCloud v2 REST API for WhatsApp — sending/listing messages, templates, phone numbers/WABA, wallet/balance, webhooks, contacts, unsubscribers, media, pagination gotchas. Triggers: 'ycloud api', 'X-API-Key', '/v2/whatsapp/messages', 'ycloud webhook', 'ycloud pagination', 'ycloud balance', 'sendDirectly', 'unsubscribers endpoint'.