From maccing-growth
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".
How this skill is triggered — by the user, by Claude, or both
Slash command
/maccing-growth:google-adsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
reference/automation.mdscripts/read-ad-details.jsscripts/read-campaign-performance.jsscripts/read-conversion-actions.jsscripts/read-full-audit.jsscripts/read-keyword-performance.jsscripts/read-search-terms.jsscripts/write-add-keywords.jsscripts/write-add-negatives.jsscripts/write-create-conversion.jsscripts/write-create-rsa.jsscripts/write-pause-campaign.jsscripts/write-update-ad-url.jsScripts-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 |
npx claudepluginhub andredezzy/maccing --plugin maccing-growthProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.