google-ads
Plan and create new Google Ads campaigns and report on existing accounts via the Hyper MCP. Use when the user wants to launch Search, Display, Performance Max, Video, Demand Gen, Shopping, or App campaigns, build Google Ads reports or dashboards, diagnose conversion tracking, or
Install
npx skills add https://github.com/hyperfx-ai/marketing-skills/tree/main/skills/google-ads
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install hyperfx-ai-marketing-skills@llmmart
git clone https://github.com/hyperfx-ai/marketing-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole hyperfx-ai/marketing-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Google Ads
Strategic guide for building new Google Ads campaigns and reporting on existing accounts. Research first, consult intelligently, validate everything, and build with the granular resource tools in dependency order (budget → campaign → targeting → ad groups → ads / asset groups). The API has exactly one campaign create — google_ads_campaigns_create — and the campaign type is just advertising_channel_type; type-specific requirements are enforced by the tools' built-in validators, which return corrective errors. Reporting is GAQL-backed and evidence-first — dashboards are optional presentation surfaces.
Requirements
- Hyper MCP installed and connected. https://app.hyperfx.ai/mcp
- Google Ads integration connected at https://app.hyperfx.ai/apps.
If search("google_ads_accounts_list") does not find google_ads_accounts_list, stop and tell the user to enable Hyper MCP and connect Google Ads.
How to run the tools in this skill
Every tool in this skill is named by its canonical tool name. Run it with the call your surface gives you:
| Surface | Find a tool | Run it |
|---|---|---|
| MCP client (Claude, Cursor, Codex, ChatGPT) | search("<what you want to do>"), then describe("<name>") |
call("<name>", {...}) |
| Hyper CLI | hyperai search "<what you want to do>", then hyperai describe <name> |
hyperai call <name> --json '{...}' |
If a tool is not found, its integration is not connected or not enabled for the workspace: stop and tell the user which integration to connect.
Out of scope
- Live optimization mutations (bid adjustments, pausing keywords, restructuring existing campaigns). Reports may recommend these changes; applying them needs explicit per-change user approval.
- Creative generation (headlines, descriptions, images) →
ad-creative-generation. - Cross-platform campaign launches → use this skill for Google, then invoke
meta-ads/tiktok-adsseparately.
Tool surface
Reads are GAQL (google_ads_gaql_query covers every resource). Writes are one tool per resource operation, named google_ads_<resource>_<create|update|remove>; create/update take (customer_id, body) where body is the bare resource object in snake_case.
| Tool | Purpose |
|---|---|
google_ads_accounts_list |
Discover accessible accounts (and MCC sub-accounts). |
google_ads_gaql_query |
Run a GAQL query (conversion actions, search terms, any resource). |
google_ads_campaign_budgets_create |
Create the budget first (micros!). |
google_ads_campaigns_create / _update / _remove |
The ONE campaign create — every advertising_channel_type (SEARCH, DISPLAY, PERFORMANCE_MAX, VIDEO, DEMAND_GEN, SHOPPING, MULTI_CHANNEL for App). |
google_ads_ad_groups_create / _update, google_ads_ad_groups_delete |
Ad groups under a campaign. |
google_ads_ad_group_ads_create / _update / _remove |
Ads (typed: responsive_search_ad, responsive_display_ad, video / demand-gen ad types). |
google_ads_keywords_create / _update / _delete / _list |
Keywords (positive and negative). |
google_ads_locations_search, google_ads_location_targets_add |
Resolve location names, target them. |
google_ads_image_assets_upload |
Image assets — accepts file_id from the workspace file manager, image_url, or base64. |
google_ads_video_assets_link |
Register a YouTube video ID as a video asset (the Ads API cannot host video files — videos must be on YouTube). |
google_ads_asset_groups_create |
PMax asset group (atomic: text + image assets in one call). |
google_ads_conversion_actions_create, google_ads_user_lists_create, google_ads_bidding_strategies_create, google_ads_shared_sets_create, … |
Full per-resource CRUD surface — same naming pattern. |
google_ads_request |
Raw escape hatch for any uncovered endpoint. |
data_apps_build, data_apps_refresh |
Optional dashboards / data apps for reports. |
Rules that must never be forgotten
BUDGETS IN MICROS: $50/day = 50,000,000 micros. Never pass dollar amounts directly.
ALWAYS START PAUSED: Create campaigns with
status="PAUSED". Activate only after explicit user approval.
SUMMARIZE BEFORE CREATE: Present the full build plan (budget, campaign settings, targeting, ad groups, ads/assets) and get explicit user approval before the first mutate. Create in dependency order and reference each created resource by the
resource_namethe mutate returns. The tools validate bodies against the real API schema (writable fields, enum values) plus channel-type rules and return corrective errors — fix and retry rather than guessing.
NEVER INVENT: Don't assume URLs, budgets, or location IDs. Don't skip research. Don't invent keywords or copy — creative comes only from the actual site. Don't build without user buy-in.
GAQL GUARDRAILS: Never join
ad_group_criterionwithsearch_term_view. Never query more than 5 sub-accounts in a single batch.
See references/constraints.md for the full constraint set.
All reference files live in
references/. Read them atreferences/<file>(e.g.references/discovery.md). They are not in the same directory as this SKILL.md.
Core process
Every campaign build follows this sequence. Do not skip steps.
- Identify the goal — new campaign, reporting/analysis, or both?
- Check the routing table and read the referenced files before calling any tools
- Discovery is mandatory for creation (references/discovery.md) — account setup, site scan, conversion tracking check, market research, consultation
- Present the pre-creation summary and wait for explicit approval
- Build in dependency order (budget → campaign PAUSED → targeting → ad groups → ads/assets) after approval, re-checking references/constraints.md at each step
- Activate only when the user approves
Full workflow: Initial Setup → Research (site + GAQL + market) → Analyze (goals, audience, keywords) → Consult (options + trade-offs) → Recommend (structure + bids) → Confirm (summary approved) → Create in dependency order (PAUSED) → Verify by GAQL → Activate (post-approval).
Routing table
| The user wants to… | Read these files first |
|---|---|
| Create a Search or Display campaign | references/discovery.md → references/campaigns/search-display.md |
| Create a Performance Max campaign | references/discovery.md → references/campaigns/pmax.md |
| Add an asset group to an existing PMax campaign | references/campaigns/pmax.md |
| Create a Video, Demand Gen, Shopping, or App campaign | references/discovery.md → references/campaigns/other-types.md |
| Run a report / GAQL query / analyze performance (any kind) | references/reporting.md → the matching references/reports/*.md recipe |
| Account health snapshot / "how is the account doing?" | references/reporting.md → references/reports/account-overview.md |
| Diagnose conversion tracking | references/reporting.md → references/reports/conversion-tracking.md |
| Find wasteful search terms / negative keyword candidates | references/reporting.md → references/reports/search-terms-waste.md |
| Build a Google Ads dashboard or data app | references/reporting.md → matching report recipe's dashboard section |
| Work across an MCC / many sub-accounts | references/mcc.md → references/reporting.md |
| Goal not yet clear | references/discovery.md — discovery clarifies the goal |
Reporting responses follow references/report-template.md; keep claims tied to queried data per references/heuristics.md.
Files (marketing-skills)
-
references
-
campaigns
-
other-types.md 2.2 KB
# Video, Demand Gen, Shopping, and App campaigns Same one create tool — `google_ads_campaigns_create` — with the channel type and its required settings. The validators return corrective errors for missing channel requirements. Always: budget first, campaign **PAUSED**, targeting, then the type's ad/asset layer. Follow the approval discipline from [search-display.md](search-display.md). ## Video (YouTube) - Campaign: `"advertising_channel_type": "VIDEO"` + a bidding scheme (`target_cpm` / `maximize_conversions` per goal). - Videos must be on YouTube — the Ads API cannot host video files. Register each with `google_ads_video_assets_link(customer_id, youtube_video_id)`. - Ad group (`google_ads_ad_groups_create`), then a video ad via `google_ads_ad_group_ads_create` with the matching typed ad object (e.g. `video_responsive_ad` referencing the video asset). ## Demand Gen - Campaign: `"advertising_channel_type": "DEMAND_GEN"`, `maximize_conversions` or `target_cpa`. - Ad group, then `demand_gen_multi_asset_ad` / `demand_gen_carousel_ad` / `demand_gen_video_responsive_ad` on `google_ads_ad_group_ads_create` — images via `google_ads_image_assets_upload`, videos via YouTube link. ## Shopping - Requires a linked Merchant Center account: `"shopping_setting": {"merchant_id": ...}` on the campaign body — the create is rejected without it. Find linked accounts by GAQL on `product_link`. - `"advertising_channel_type": "SHOPPING"`. Standard Shopping: ad group + `shopping_product_ad` (no creative fields — products come from the feed) + listing-group criteria via `google_ads_ad_group_criteria_create` (`listing_group` field). ## App - Campaign: `"advertising_channel_type": "MULTI_CHANNEL"` with `"app_campaign_setting": {"app_id": "...", "app_store": "GOOGLE_APP_STORE"|"APPLE_APP_STORE", "bidding_strategy_goal_type": ...}` — required, enforced by the validator. - Assets (text/image/video) attach at campaign level; Google assembles the ads. ## Anything else Every remaining resource has the same `google_ads_<resource>_<create|update|remove>` tools (conversion actions, user lists, bidding strategies, shared sets, labels, experiments, …); reads are GAQL; `google_ads_request` covers any uncovered endpoint. -
pmax.md 2.7 KB
# Performance Max campaigns (granular build) > **CRITICAL**: Present the full plan and get explicit user approval before the first mutate ([discovery.md](../discovery.md) must be complete and the summary approved). PMax campaigns MUST be created **PAUSED** — they need at least one asset group before they can serve, and the create tool enforces this. ## Build order 1. **Budget** — `google_ads_campaign_budgets_create(customer_id, body={"name": "...", "amount_micros": 50000000})`. PMax budgets must not be shared. 2. **Campaign** — `google_ads_campaigns_create(customer_id, body={...})`: ```json { "name": "PMax Campaign", "advertising_channel_type": "PERFORMANCE_MAX", "status": "PAUSED", "campaign_budget": "customers/1234567890/campaignBudgets/111", "maximize_conversions": {} } ``` Do NOT set `network_settings` or manual bidding — PMax serves across all networks automatically and only accepts `maximize_conversions` / `maximize_conversion_value` (optionally with `target_cpa` / `target_roas` inside the scheme). The validator rejects anything else. 3. **Locations** — `google_ads_locations_search` → `google_ads_location_targets_add`. 4. **Image assets** — `google_ads_image_assets_upload` (prefer `file_id` from the workspace file manager). Required ratios: landscape 1.91:1, square 1:1, logo 1:1 (or 4:1). 5. **Asset group** — `google_ads_asset_groups_create` (atomic: creates the asset group, its text assets, and all links in ONE API call, which the API requires): Required: `customer_id`, `campaign_id`, `final_urls`, `headlines` (3–15, ≤30 chars), `long_headlines` (1+, ≤90), `descriptions` (2+, ≤90), `business_name`, and all three image asset ID lists (`marketing_image_asset_ids`, `square_marketing_image_asset_ids`, `logo_asset_ids`). 6. **Optional video** — register a YouTube video with `google_ads_video_assets_link(customer_id, youtube_video_id)`, then link it to the asset group with `google_ads_asset_group_assets_link(field_type="YOUTUBE_VIDEO")`. Without one, Google auto-generates video from your assets. ## Standalone PMax asset group (existing campaign) `google_ads_asset_groups_create` also adds asset groups to existing PMax campaigns — same required fields as step 5. ## Campaign dates (API v24) Optional `start_date` / `end_date` use `YYYY-MM-DD`. The backend encodes them to v24 wire fields `startDateTime` / `endDateTime`. Omit both to let Google set schedule defaults. ## Verify, then activate Confirm by GAQL (campaign + asset_group + asset_group_asset), present to the user, and only set `status: "ENABLED"` via `google_ads_campaigns_update` after explicit approval. Re-check [constraints.md](../constraints.md) at each creation step. -
search-display.md 3.8 KB
# Search & Display campaigns (granular build) > **CRITICAL**: Present the full plan and get explicit user approval before the first mutate ([discovery.md](../discovery.md) must be complete and the summary approved). Create everything **PAUSED**, in dependency order, carrying each returned `resource_name` into the next step. The tools validate bodies (writable fields, real enums, channel-type rules) and return corrective errors — fix and retry. ## Build order 1. **Budget** — `google_ads_campaign_budgets_create(customer_id, body={"name": "...", "amount_micros": 50000000})` ($50/day; micros!). Returns `customers/{cid}/campaignBudgets/{id}`. 2. **Campaign** — `google_ads_campaigns_create(customer_id, body={...})`: ```json { "name": "Campaign Name", "advertising_channel_type": "SEARCH", "status": "PAUSED", "campaign_budget": "customers/1234567890/campaignBudgets/111", "maximize_clicks": {}, "network_settings": {"target_google_search": true, "target_search_network": true} } ``` Bidding is ONE scheme field on the campaign: `maximize_clicks`, `maximize_conversions`, `maximize_conversion_value`, `target_cpa`, `target_roas`, or `manual_cpc`. For Display set `"advertising_channel_type": "DISPLAY"`. 3. **Locations** — resolve names with `google_ads_locations_search`, then `google_ads_location_targets_add(customer_id, campaign_id, location_ids=[...])`. 4. **Ad group** — `google_ads_ad_groups_create(customer_id, body={"name": "Ad Group 1", "campaign": "customers/{cid}/campaigns/{id}", "status": "PAUSED"})`. 5. **Keywords** (Search) — `google_ads_keywords_create(customer_id, ad_group_id, keywords=[{"text": "marketing software", "match_type": "PHRASE"}, {"text": "competitor brand", "match_type": "EXACT", "negative": true}])`. 6. **Ads** — `google_ads_ad_group_ads_create(customer_id, body={...})` with exactly ONE typed ad object: ```json { "ad_group": "customers/1234567890/adGroups/222", "status": "PAUSED", "ad": { "final_urls": ["https://example.com"], "responsive_search_ad": { "headlines": [{"text": "Headline 1"}, {"text": "Headline 2"}, {"text": "Headline 3"}], "descriptions": [{"text": "Description 1"}, {"text": "Description 2"}] } } } ``` RSA minimums: 3–15 headlines (≤30 chars), 2–4 descriptions (≤90 chars). ## Display ads Upload the three image asset types first with `google_ads_image_assets_upload` (prefer `file_id` from the workspace file manager), then create a `responsive_display_ad`: ```json "ad": { "final_urls": ["https://example.com"], "responsive_display_ad": { "headlines": [{"text": "Headline 1"}], "long_headline": {"text": "Longer headline up to 90 characters"}, "descriptions": [{"text": "Description 1"}], "business_name": "Business Name", "marketing_images": [{"asset": "customers/{cid}/assets/{landscape_id}"}], "square_marketing_images": [{"asset": "customers/{cid}/assets/{square_id}"}], "square_logo_images": [{"asset": "customers/{cid}/assets/{logo_id}"}] } } ``` > **CRITICAL (Display)**: All three image asset types are REQUIRED: > - marketing images — landscape 1.91:1 (e.g. 1200×628) > - square marketing images — square 1:1 (e.g. 1200×1200) > - logos — square 1:1 (or landscape 4:1) ## Extensions (sitelinks, callouts) Create assets with `google_ads_assets_create` (`sitelink_asset` / `callout_asset` field on the body), then link with `google_ads_campaign_assets_link(customer_id, campaign_id, asset_id, field_type="SITELINK"|"CALLOUT")`. ## Verify, then activate After building, confirm the tree by GAQL (campaign, ad groups, ads, keywords), present it to the user, and only set `status: "ENABLED"` via `google_ads_campaigns_update` after explicit approval. Re-check [constraints.md](../constraints.md) (bidding strategies, technical rules) at each creation step.
-
-
reports
-
account-overview.md 4.5 KB
# Account Overview Full-account health snapshot. Combines spend, conversions, conversion rate, and cost per conversion in a single dashboard so the agent can orient before drilling into a specific issue. ## When to use - "How is the account doing?" - First report to run for a new client engagement. - Periodic health check. - When routing to a more focused report (tracking / budget / search terms / structure) requires an aggregate baseline. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). Use the GAQL below through `google_ads_gaql_query` for both ad-hoc reporting and refreshable dashboards. ## GAQL ```sql SELECT segments.date, campaign.id, campaign.name, campaign.status, metrics.cost_micros, metrics.clicks, metrics.impressions, metrics.conversions, metrics.conversions_value FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` ## How to interpret - Treat any campaign with significant spend and zero conversions over a meaningful window as a tracking-or-budget issue, not a "campaign performance" issue. - ROAS and CPA are downstream signals — surface them only when conversion tracking is verified (see [conversion-tracking.md](conversion-tracking.md)). - Aggregate spend without segmenting by date hides pacing problems; always include a daily series. ## Building a dashboard from this report ```python result = data_apps_build( name="Account Overview", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "campaign.status, metrics.cost_micros, metrics.clicks, " "metrics.impressions, metrics.conversions, " "metrics.conversions_value " "FROM campaign " "WHERE segments.date DURING LAST_30_DAYS " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_account_overview", "mode": "replace", }, }, sql_data_sources={ "total_spend": { "shape": "scalar", "sql": ( "SELECT COALESCE(SUM(metrics_cost_micros), 0)/1e6 " "FROM gads_account_overview" ), }, "total_conversions": { "shape": "scalar", "sql": ( "SELECT COALESCE(SUM(metrics_conversions), 0) " "FROM gads_account_overview" ), }, "daily": { "shape": "rows", "sql": ( "SELECT segments_date AS date, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_conversions) AS conversions " "FROM gads_account_overview GROUP BY 1 ORDER BY 1" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import AreaChart, ChartSeries with PrefabApp(title="Account Overview") as app: with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Account Overview") Muted("Last 30 days") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Spend", value=f"${total_spend:,.0f}") with Card(): with CardContent(): Metric(label="Conversions", value=f"{total_conversions:,.0f}") with Card(): with CardContent(): Text("Daily spend and conversions", css_class="text-sm font-medium text-muted-foreground mb-2") AreaChart( data=daily, x_axis="date", series=[ ChartSeries(data_key="spend", label="Spend"), ChartSeries(data_key="conversions", label="Conversions"), ], show_legend=True, height=300, ) """, refresh={"mode": "scheduled", "cron": "0 * * * *"}, ) ``` ## Variants - Add `metrics.average_cpc`, `metrics.ctr`, `metrics.search_impression_share` to the GAQL when the user wants efficiency context. - Group by `campaign.advertising_channel_type` to split Search / PMax / Display in the same dashboard. -
ad-performance.md 6 KB
# Ad Performance Ad-level (RSA / asset) performance across campaigns. Drives creative optimization, ad rotation diagnosis, and asset-replacement decisions. ## When to use - "Which ads are working / failing?" - Before rotating creatives or pausing underperforming RSAs. - After uploading new asset variants to compare against the baseline. - Diagnosing low-CTR ad groups that may be a creative problem rather than a targeting one. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). - Optional: `campaign_ids` to scope. ## GAQL ```sql SELECT ad_group_ad.ad.id, ad_group_ad.ad.name, ad_group_ad.ad.type, ad_group_ad.status, ad_group.name, campaign.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr, metrics.average_cpc FROM ad_group_ad WHERE segments.date BETWEEN '{start}' AND '{end}' AND ad_group_ad.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.status != 'REMOVED' ``` ## How to interpret - High-impression / low-CTR ads = creative or relevance problem; candidates for headline / description rotation. - Low-impression ads (relative to peers in the same ad group) = ad_strength / serving issue, not necessarily a performance issue. - An RSA dominating impressions inside an ad group should be the benchmark when proposing variants. ## Building a dashboard from this report ```python result = data_apps_build( name="Ad Performance", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT ad_group_ad.ad.id, ad_group_ad.ad.name, " "ad_group_ad.ad.type, ad_group_ad.status, " "ad_group.name, campaign.name, " "metrics.impressions, metrics.clicks, metrics.cost_micros, " "metrics.conversions, metrics.ctr, metrics.average_cpc " "FROM ad_group_ad " "WHERE segments.date DURING LAST_30_DAYS " "AND ad_group_ad.status != 'REMOVED' " "AND ad_group.status != 'REMOVED' " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_ad_performance", "mode": "replace", }, }, sql_data_sources={ "top_ads": { "shape": "rows", "sql": ( "SELECT ad_group_ad_ad_name AS ad_name, " "campaign_name AS campaign, " "ad_group_name AS ad_group, " "SUM(metrics_impressions) AS impressions, " "SUM(metrics_clicks) AS clicks, " "SUM(metrics_conversions) AS conversions, " "SUM(metrics_cost_micros)/1e6 AS spend " "FROM gads_ad_performance " "GROUP BY 1, 2, 3 ORDER BY clicks DESC LIMIT 25" ), }, "by_type": { "shape": "rows", "sql": ( "SELECT ad_group_ad_ad_type AS ad_type, " "SUM(metrics_clicks) AS clicks, " "SUM(metrics_conversions) AS conversions " "FROM gads_ad_performance GROUP BY 1 ORDER BY clicks DESC" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import BarChart, ChartSeries with PrefabApp(title="Ad Performance") as app: total_clicks = sum(row.get("clicks", 0) for row in top_ads) total_conversions = sum(row.get("conversions", 0) for row in top_ads) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Ad Performance") Muted("Top ads and creative format mix") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Clicks", value=f"{total_clicks:,.0f}") with Card(): with CardContent(): Metric(label="Conversions", value=f"{total_conversions:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Performance by ad type", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=by_type, x_axis="ad_type", series=[ ChartSeries(data_key="clicks", label="Clicks"), ChartSeries(data_key="conversions", label="Conversions"), ], show_legend=True, height=300, ) with Card(): with CardContent(): Text("Top ads", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="ad_name", header="Ad"), DataTableColumn(key="campaign", header="Campaign"), DataTableColumn(key="ad_group", header="Ad Group"), DataTableColumn(key="impressions", header="Impressions"), DataTableColumn(key="clicks", header="Clicks"), DataTableColumn(key="conversions", header="Conversions"), DataTableColumn(key="spend", header="Spend"), ], rows=top_ads, ) """, refresh={"mode": "scheduled", "cron": "0 */6 * * *"}, ) ``` ## Variants - Filter to `ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD'` for search-only RSA reviews. - Add `ad_group_ad.ad_strength` to see Google's own quality signal. - Group by `segments.device` for device-level creative performance. -
budget-distribution.md 4.9 KB
# Budget Distribution Where spend is actually going across campaigns. ## When to use - "Where is our budget going?" - "Which campaigns are wasting money?" - Pacing review before adjusting daily budgets. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). Use the GAQL below through `google_ads_gaql_query` for both ad-hoc reporting and refreshable dashboards. ## GAQL ```sql SELECT campaign.id, campaign.name, campaign.status, campaign_budget.amount_micros, campaign_budget.delivery_method, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.clicks FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` ## How to interpret - High `cost_micros` + zero `conversions` over a meaningful window = immediate candidate for budget reduction OR for a tracking audit. Don't recommend cuts until tracking is ruled out. - Spend Herfindahl: if 80%+ of spend lives in 2–3 campaigns, the account is concentrated; restructure work has high leverage. - Compare `cost_micros` to `campaign_budget.amount_micros * days` to detect under-pacing (budget caps / bidding ceilings). ## Building a dashboard from this report ```python result = data_apps_build( name="Budget Distribution", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT campaign.id, campaign.name, campaign.status, " "campaign_budget.amount_micros, " "campaign_budget.delivery_method, " "metrics.cost_micros, metrics.conversions, " "metrics.conversions_value, metrics.clicks " "FROM campaign " "WHERE segments.date DURING LAST_30_DAYS " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_budget", "mode": "replace", }, }, sql_data_sources={ "by_campaign": { "shape": "rows", "sql": ( "SELECT campaign_name AS campaign, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_conversions) AS conversions " "FROM gads_budget GROUP BY 1 ORDER BY spend DESC LIMIT 25" ), }, "zero_conv_high_spend": { "shape": "rows", "sql": ( "SELECT campaign_name AS campaign, " "SUM(metrics_cost_micros)/1e6 AS spend " "FROM gads_budget GROUP BY 1 " "HAVING SUM(metrics_conversions) = 0 " "AND SUM(metrics_cost_micros) > 50000000 " "ORDER BY spend DESC" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import BarChart, ChartSeries with PrefabApp(title="Budget Distribution") as app: total_spend = sum(row.get("spend", 0) for row in by_campaign) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Budget Distribution") Muted("Spend concentration and zero-conversion risk") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Spend", value=f"${total_spend:,.0f}") with Card(): with CardContent(): Metric(label="Zero-conversion campaigns", value=f"{len(zero_conv_high_spend):,}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Spend by campaign", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=by_campaign, x_axis="campaign", series=[ChartSeries(data_key="spend", label="Spend")], height=300, ) with Card(): with CardContent(): Text("High spend without conversions", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="campaign", header="Campaign"), DataTableColumn(key="spend", header="Spend"), ], rows=zero_conv_high_spend, ) """, refresh={"mode": "scheduled", "cron": "0 */6 * * *"}, ) ``` ## Variants - Add `metrics.search_impression_share` to flag campaigns that are budget-constrained vs. competitor-pressured. - Group by `campaign.advertising_channel_type` to compare Search vs PMax vs Display efficiency separately. -
campaign-performance.md 5.8 KB
# Campaign Performance Time-series view of spend, clicks, conversions, and ROAS per campaign. The general-purpose performance dashboard — pair with the diagnosis reports when an issue surfaces. ## When to use - "How is each campaign performing over time?" - Weekly / monthly performance review with the client. - Identifying trend changes (sudden spend spike, conversion drop). - The default first dashboard for a new client engagement. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). - Optional: `campaign_ids` filter. Use the GAQL below through `google_ads_gaql_query` for both ad-hoc reporting and refreshable dashboards. ## GAQL ```sql SELECT segments.date, campaign.id, campaign.name, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` ## How to interpret - Daily ROAS = `conversions_value / cost`. Plot as time series; a step-down on a known date often correlates with an ad / landing-page change. - A sustained drop in conversions with steady spend = tracking issue or seasonality, not necessarily campaign decay. - Compare campaigns of the same `advertising_channel_type`; cross-type comparisons (Search vs PMax) are misleading. ## Building a dashboard from this report ```python result = data_apps_build( name="Campaign Performance", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "campaign.advertising_channel_type, " "metrics.impressions, metrics.clicks, metrics.cost_micros, " "metrics.conversions, metrics.conversions_value, " "metrics.ctr, metrics.average_cpc " "FROM campaign " "WHERE segments.date DURING LAST_30_DAYS " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_campaign_performance", "mode": "replace", }, }, sql_data_sources={ "daily_totals": { "shape": "rows", "sql": ( "SELECT segments_date AS date, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_conversions) AS conversions, " "SUM(metrics_conversions_value) AS revenue " "FROM gads_campaign_performance GROUP BY 1 ORDER BY 1" ), }, "by_campaign": { "shape": "rows", "sql": ( "SELECT campaign_name AS campaign, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_conversions) AS conversions, " "CASE WHEN SUM(metrics_cost_micros) > 0 " "THEN SUM(metrics_conversions_value)/(SUM(metrics_cost_micros)/1e6) " "ELSE 0 END AS roas " "FROM gads_campaign_performance GROUP BY 1 ORDER BY spend DESC LIMIT 25" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import AreaChart, ChartSeries with PrefabApp(title="Campaign Performance") as app: total_spend = sum(row.get("spend", 0) for row in by_campaign) total_conversions = sum(row.get("conversions", 0) for row in by_campaign) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Campaign Performance") Muted("Spend, conversions, revenue, and campaign-level efficiency") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Spend", value=f"${total_spend:,.0f}") with Card(): with CardContent(): Metric(label="Conversions", value=f"{total_conversions:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Daily performance", css_class="text-sm font-medium text-muted-foreground mb-2") AreaChart( data=daily_totals, x_axis="date", series=[ ChartSeries(data_key="spend", label="Spend"), ChartSeries(data_key="conversions", label="Conversions"), ChartSeries(data_key="revenue", label="Revenue"), ], show_legend=True, height=300, ) with Card(): with CardContent(): Text("Campaign detail", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="campaign", header="Campaign"), DataTableColumn(key="spend", header="Spend"), DataTableColumn(key="conversions", header="Conversions"), DataTableColumn(key="roas", header="ROAS"), ], rows=by_campaign, ) """, refresh={"mode": "scheduled", "cron": "0 * * * *"}, ) ``` ## Variants - Add `metrics.search_impression_share` and `metrics.search_top_impression_share` to spot lost-by-budget / lost-by-rank issues. - Group by `segments.device` for desktop vs mobile vs tablet splits. -
campaign-structure.md 5.4 KB
# Campaign Structure Campaign and ad-group structure view for understanding where spend, clicks, and conversions sit across account organization. ## When to use - "Show me campaign and ad group structure." - "Which ad groups are getting spend?" - "How are campaigns and ad groups performing together?" - Before launching a parallel campaign, inspect whether the existing account already covers the same theme. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). Use the GAQL below through `google_ads_gaql_query` for both ad-hoc reporting and refreshable dashboards. ## GAQL ```sql SELECT campaign.id, campaign.name, campaign.advertising_channel_type, ad_group.id, ad_group.name, metrics.cost_micros, metrics.conversions, metrics.clicks, metrics.impressions FROM ad_group WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' ``` ## How to interpret - One campaign carrying many ad groups with very different conversion rates → segmentation candidates. - Brand-matching ad groups inside the same campaign as generic non-brand ad groups → recommend a brand split (huge bidding leverage). - Ad groups with very low impression share inside high-spend campaigns → likely competing internally with another ad group; consolidation candidates. ## Building a dashboard from this report ```python result = data_apps_build( name="Campaign Structure", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT campaign.id, campaign.name, " "campaign.advertising_channel_type, " "ad_group.id, ad_group.name, " "metrics.cost_micros, metrics.conversions, " "metrics.clicks, metrics.impressions " "FROM ad_group " "WHERE segments.date DURING LAST_30_DAYS " "AND campaign.status != 'REMOVED' " "AND ad_group.status != 'REMOVED'" ), }, "cache_table": "gads_structure", "mode": "replace", }, }, sql_data_sources={ "ad_groups_per_campaign": { "shape": "rows", "sql": ( "SELECT campaign_name AS campaign, " "COUNT(DISTINCT ad_group_id) AS ad_groups, " "SUM(metrics_cost_micros)/1e6 AS spend " "FROM gads_structure GROUP BY 1 ORDER BY spend DESC LIMIT 25" ), }, "ad_group_efficiency": { "shape": "rows", "sql": ( "SELECT campaign_name AS campaign, ad_group_name AS ad_group, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_conversions) AS conversions " "FROM gads_structure GROUP BY 1, 2 " "ORDER BY spend DESC LIMIT 50" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import BarChart, ChartSeries with PrefabApp(title="Campaign Structure") as app: total_ad_groups = sum(row.get("ad_groups", 0) for row in ad_groups_per_campaign) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Campaign Structure") Muted("Ad group distribution and efficiency") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Campaigns", value=f"{len(ad_groups_per_campaign):,}") with Card(): with CardContent(): Metric(label="Ad Groups", value=f"{total_ad_groups:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Ad groups and spend", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=ad_groups_per_campaign, x_axis="campaign", series=[ ChartSeries(data_key="ad_groups", label="Ad Groups"), ChartSeries(data_key="spend", label="Spend"), ], show_legend=True, height=300, ) with Card(): with CardContent(): Text("Ad group efficiency", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="campaign", header="Campaign"), DataTableColumn(key="ad_group", header="Ad Group"), DataTableColumn(key="spend", header="Spend"), DataTableColumn(key="conversions", header="Conversions"), ], rows=ad_group_efficiency, ) """, refresh={"mode": "manual"}, ) ``` ## Variants - Filter to `campaign.advertising_channel_type = 'SEARCH'` for search-only restructure analysis. - Add `ad_group.type` to distinguish standard vs PMax asset groups. -
conversion-by-action.md 8.4 KB
# Conversions by Action Per–conversion-action breakdown across campaigns and dates. Use this when the user wants to see how each individual conversion event (like "sign up", "purchase", "qualified lead") performs over time and across campaigns — not just the aggregate `conversions` metric. ## When to use - "Show me conversions broken down by action / by event." - "Which campaigns drive each stage of our funnel?" (paired with [conversion-funnel.md](conversion-funnel.md)) - Diagnosing a single noisy conversion action (e.g. duplicate primary). - Building a stacked area / bar chart of conversion mix over time. ## Inputs - `customer_id` (required, format `XXX-XXX-XXXX`). - `date_range` (default: `LAST_30_DAYS`; or explicit `start_date` / `end_date`). - Optional: `campaign_ids` to scope to a subset. ## GAQL ```sql SELECT segments.date, campaign.id, campaign.name, segments.conversion_action, segments.conversion_action_name, segments.conversion_action_category, metrics.conversions, metrics.all_conversions, metrics.conversions_value FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` **Critical caveat — common failure mode:** Do **not** select `conversion_action.id` or `conversion_action.name` against `FROM campaign`. That triggers `PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE`. Always use the `segments.conversion_action*` fields instead. The metric name is `metrics.conversions` (already segmented by `segments.conversion_action`), **not** `metrics.conversions_by_conversion_action` (which does not exist). Do **not** select `metrics.cost_micros` in this query. Google Ads rejects `metrics.cost_micros` with `segments.conversion_action*` using `PROHIBITED_SEGMENT_WITH_METRIC_IN_SELECT_OR_WHERE_CLAUSE`. If cost is needed, run a second campaign/date query and join downstream: ```sql SELECT segments.date, campaign.id, campaign.name, metrics.cost_micros, metrics.clicks, metrics.impressions FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` Join conversion rows to cost rows on `(segments.date, campaign.id)`. For ad-hoc analysis, an in-memory pandas/dataframe merge in the sandbox is enough; no cache table is required. If the user wants to enumerate conversion actions independently of campaigns, query the `conversion_action` resource directly — see [conversion-tracking.md](conversion-tracking.md). ## How to interpret - Each row is `(date, campaign, conversion_action)`. A campaign that fires three actions yields three rows per day. - `metrics.conversions` is the segmented count for that action; sum across actions in a campaign to recover the campaign-level `metrics.conversions`. - `segments.conversion_action_category` (e.g. `PURCHASE`, `SIGNUP`, `LEAD`) lets you group ad-hoc events into business categories. - Joined cost is campaign/date context, not action-attributed spend. Do not sum joined cost across conversion actions unless you intentionally allocate campaign/date spend across actions first. ## Building a dashboard from this report The dashboard/data app tool reads tool-sourced data via `tool_data_sources`, re-aggregates it via `sql_data_sources`, and binds the variables in the interface source. Cache table convention: `gads_<report>_<scope>`. ```python result = data_apps_build( name="Conversions by Action", tool_data_sources={ "raw_conversions": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "segments.conversion_action, segments.conversion_action_name, " "segments.conversion_action_category, metrics.conversions, " "metrics.all_conversions, metrics.conversions_value " "FROM campaign " "WHERE segments.date BETWEEN '2026-03-01' AND '2026-05-12' " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_conv_by_action", "mode": "replace", }, "raw_cost": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "metrics.cost_micros, metrics.clicks, metrics.impressions " "FROM campaign " "WHERE segments.date BETWEEN '2026-03-01' AND '2026-05-12' " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_campaign_cost", "mode": "replace", }, }, sql_data_sources={ "totals_by_action": { "shape": "rows", "sql": ( "SELECT segments_conversion_action_name AS action, " "SUM(metrics_conversions) AS conversions " "FROM gads_conv_by_action " "GROUP BY 1 ORDER BY conversions DESC" ), }, "cost_totals": { "shape": "rows", "sql": ( "SELECT SUM(metrics_cost_micros)/1e6 AS cost " "FROM gads_campaign_cost" ), }, "trend": { "shape": "rows", "sql": ( "SELECT segments_date AS date, " "segments_conversion_action_name AS action, " "SUM(metrics_conversions) AS conversions " "FROM gads_conv_by_action GROUP BY 1, 2 ORDER BY 1" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import AreaChart, BarChart, ChartSeries with PrefabApp(title="Conversions by Action") as app: total_conversions = sum(row.get("conversions", 0) for row in totals_by_action) total_cost = (cost_totals[0].get("cost", 0) if cost_totals else 0) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Conversions by Action") Muted("Conversion mix, trend, and campaign spend context") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Conversions", value=f"{total_conversions:,.0f}") with Card(): with CardContent(): Metric(label="Cost", value=f"${total_cost:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Conversion trend", css_class="text-sm font-medium text-muted-foreground mb-2") AreaChart( data=trend, x_axis="date", series=[ChartSeries(data_key="conversions", label="Conversions")], height=300, ) with Card(): with CardContent(): Text("Conversions by action", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=totals_by_action, x_axis="action", series=[ChartSeries(data_key="conversions", label="Conversions")], height=300, ) with Card(): with CardContent(): Text("Action detail", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="action", header="Action"), DataTableColumn(key="conversions", header="Conversions"), ], rows=totals_by_action, ) """, refresh={"mode": "scheduled", "cron": "0 * * * *"}, ) ``` ## Variants - **Per ad group**: change `FROM campaign` to `FROM ad_group` and add `ad_group.id`, `ad_group.name`. Useful when one ad group fires the conversion but the campaign rolls up many. - **Per ad**: `FROM ad_group_ad` plus `ad_group_ad.ad.id`, `ad_group_ad.ad.name`. Heavier but useful for ad-level diagnosis. - **Filtered to one action**: add `AND segments.conversion_action = 'customers/.../conversionActions/12345'` to focus on a single event. -
conversion-funnel.md 10.9 KB
# Conversion Funnel Multi-stage conversion funnel for lead-gen / B2B accounts. Pivots per-action conversion data into ordered funnel stages and computes step-conversion rates between stages. ## When to use - Lead-gen / B2B accounts with multiple sequential stages (e.g. application_submitted → lead_reached → scheduled → showed → won). - "What's the drop-off between [stage A] and [stage B]?" - "Which stage is the bottleneck?" - Pricing or onboarding optimization rooted in funnel data. For a flat per-action breakdown without ordering, use [conversion-by-action.md](conversion-by-action.md) instead. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). - An ordered list of conversion-action names (or IDs) that defines the funnel. **The agent must discover these first** — no funnel ordering is hard-coded. ## GAQL ### Step 1 — discover the user's conversion actions ```sql SELECT conversion_action.id, conversion_action.name, conversion_action.category, conversion_action.status FROM conversion_action WHERE conversion_action.status != 'REMOVED' ``` Show the result to the user and ask them to map each action to a funnel stage in order. **Do not assume a six-stage funnel** (or any stage count) — funnels are workspace-specific. ### Step 2 — pull per-action data Same shape as [conversion-by-action.md](conversion-by-action.md): ```sql SELECT segments.date, campaign.id, campaign.name, segments.conversion_action, segments.conversion_action_name, segments.conversion_action_category, metrics.conversions, metrics.all_conversions, metrics.conversions_value FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` The same caveat applies: **only** segment via `segments.conversion_action*`, never `conversion_action.id` against `FROM campaign` (raises `PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE`). The metric is `metrics.conversions`, not the nonexistent `metrics.conversions_by_conversion_action`. Do **not** include `metrics.cost_micros` in the per-action query. Google Ads rejects cost with `segments.conversion_action*` using `PROHIBITED_SEGMENT_WITH_METRIC_IN_SELECT_OR_WHERE_CLAUSE`. If cost is needed, run a separate campaign/date query: ```sql SELECT segments.date, campaign.id, campaign.name, metrics.cost_micros, metrics.clicks, metrics.impressions FROM campaign WHERE segments.date BETWEEN '{start}' AND '{end}' AND campaign.status != 'REMOVED' ``` Join conversion rows to cost rows on `(segments.date, campaign.id)`. For one-off analysis, join in memory with pandas or an equivalent dataframe in the sandbox; cache tables are only needed for dashboard/data-app builds. ## How to interpret - Pivot the raw rows so each user-mapped stage becomes a column, values are stage totals. - Step conversion rate = `stage_n / stage_n-1`. - Joined cost is campaign/date context. Do not present it as action-attributed cost per stage unless the report explicitly defines an allocation rule. - State the user-provided stage mapping in the final report. Do not infer action order from names alone. ## Building a dashboard from this report ```python # After the agent has mapped action names to ordered stages, e.g. # stages = ["application_submitted", "lead_reached", # "scheduled_for_training", "showed_for_training", # "launched_from_training", "rep_that_sells"] result = data_apps_build( name="Lead-Gen Funnel", tool_data_sources={ "raw_conversions": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "segments.conversion_action, segments.conversion_action_name, " "segments.conversion_action_category, metrics.conversions, " "metrics.all_conversions, metrics.conversions_value " "FROM campaign " "WHERE segments.date BETWEEN '2026-03-01' AND '2026-05-12' " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_funnel_raw", "mode": "replace", }, "raw_cost": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT segments.date, campaign.id, campaign.name, " "metrics.cost_micros, metrics.clicks, metrics.impressions " "FROM campaign " "WHERE segments.date BETWEEN '2026-03-01' AND '2026-05-12' " "AND campaign.status != 'REMOVED'" ), }, "cache_table": "gads_funnel_cost", "mode": "replace", }, }, sql_data_sources={ "stage_totals": { "shape": "rows", "sql": ( "SELECT segments_conversion_action_name AS stage, " "CASE segments_conversion_action_name " "WHEN 'application_submitted' THEN 1 " "WHEN 'lead_reached' THEN 2 " "WHEN 'scheduled_for_training' THEN 3 " "WHEN 'showed_for_training' THEN 4 " "WHEN 'launched_from_training' THEN 5 " "WHEN 'rep_that_sells' THEN 6 END AS stage_order, " "SUM(metrics_conversions) AS conversions " "FROM gads_funnel_raw " "WHERE segments_conversion_action_name IN (" " 'application_submitted', 'lead_reached', " " 'scheduled_for_training', 'showed_for_training', " " 'launched_from_training', 'rep_that_sells'" ") GROUP BY 1, 2 ORDER BY 2" ), }, "step_rates": { "shape": "rows", "sql": ( "WITH ordered AS (" " SELECT segments_conversion_action_name AS stage, " " CASE segments_conversion_action_name " " WHEN 'application_submitted' THEN 1 " " WHEN 'lead_reached' THEN 2 " " WHEN 'scheduled_for_training' THEN 3 " " WHEN 'showed_for_training' THEN 4 " " WHEN 'launched_from_training' THEN 5 " " WHEN 'rep_that_sells' THEN 6 END AS stage_order, " " SUM(metrics_conversions) AS conversions " " FROM gads_funnel_raw " " WHERE segments_conversion_action_name IN (" " 'application_submitted', 'lead_reached', " " 'scheduled_for_training', 'showed_for_training', " " 'launched_from_training', 'rep_that_sells'" " ) GROUP BY 1, 2" ") SELECT stage, conversions, " "conversions / NULLIF(LAG(conversions) OVER (ORDER BY stage_order), 0) " "AS step_rate " "FROM ordered ORDER BY stage_order" ), }, "cost_totals": { "shape": "rows", "sql": ( "SELECT SUM(metrics_cost_micros)/1e6 AS cost " "FROM gads_funnel_cost" ), }, "stage_trend": { "shape": "rows", "sql": ( "SELECT segments_date AS date, " "segments_conversion_action_name AS stage, " "SUM(metrics_conversions) AS conversions " "FROM gads_funnel_raw GROUP BY 1, 2 ORDER BY 1" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import AreaChart, BarChart, ChartSeries with PrefabApp(title="Lead-Gen Funnel") as app: total_conversions = sum(row.get("conversions", 0) for row in stage_totals) total_cost = (cost_totals[0].get("cost", 0) if cost_totals else 0) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Lead-Gen Funnel") Muted("Stage volume, trend, and step-rate health") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Conversions", value=f"{total_conversions:,.0f}") with Card(): with CardContent(): Metric(label="Cost", value=f"${total_cost:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Stage trend", css_class="text-sm font-medium text-muted-foreground mb-2") AreaChart( data=stage_trend, x_axis="date", series=[ChartSeries(data_key="conversions", label="Conversions")], height=300, ) with Card(): with CardContent(): Text("Stage totals", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=stage_totals, x_axis="stage", series=[ChartSeries(data_key="conversions", label="Conversions")], height=300, ) with Grid(columns=[1, 1], gap=6): with Card(): with CardContent(): Text("Stage detail", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="stage", header="Stage"), DataTableColumn(key="conversions", header="Conversions"), ], rows=stage_totals, ) with Card(): with CardContent(): Text("Step rates", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="stage", header="Stage"), DataTableColumn(key="conversions", header="Conversions"), DataTableColumn(key="step_rate", header="Step Rate"), ], rows=step_rates, ) """, refresh={"mode": "scheduled", "cron": "0 * * * *"}, ) ``` ## Variants - **Step rates**: add an `sql_data_sources` entry that uses window functions (`LAG`) to compute `conversions / lag_conversions` between ordered stages and bind it to a separate KPI row. - **Per campaign**: filter `WHERE campaign.id = ...` to compare funnel shapes between campaigns. - **Allocated cost per stage**: join campaign/date cost to conversion rows, then apply an explicit allocation rule before presenting stage cost. -
conversion-tracking.md 5.5 KB
# Conversion Tracking Conversion-action inventory report. Use it to list configured conversion actions and their metadata before building action-specific dashboards. ## When to use - "Which conversion actions exist?" - "Which conversion actions are enabled?" - "What are the conversion action categories and settings?" - Before building a funnel dashboard that needs user-mapped stages. ## Inputs - `customer_id` (required). Use the GAQL below through `google_ads_gaql_query` when you need conversion-action metadata. This is separate from campaign-level conversion metrics, which use `segments.conversion_action*`. ## GAQL ```sql SELECT conversion_action.id, conversion_action.name, conversion_action.category, conversion_action.type, conversion_action.status, conversion_action.primary_for_goal, conversion_action.counting_type, conversion_action.click_through_lookback_window_days FROM conversion_action WHERE conversion_action.status != 'REMOVED' ``` ## How to interpret - **No enabled conversion actions** → optimization is blind; halt all Smart Bidding advice until at least one is configured. - **Multiple `PRIMARY_FOR_GOAL` actions in the same category** (e.g. two primary purchase actions) → Smart Bidding double-counts; recommend demoting one to secondary. - **`PURCHASE` category but `counting_type = MANY_PER_CLICK`** is rare and usually a misconfiguration; flag for review. - **Lookback window mismatches** between online + offline actions can produce inconsistent attribution. ## Building a dashboard from this report ```python result = data_apps_build( name="Conversion Tracking Audit", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT conversion_action.id, conversion_action.name, " "conversion_action.category, conversion_action.type, " "conversion_action.status, conversion_action.primary_for_goal, " "conversion_action.counting_type, " "conversion_action.click_through_lookback_window_days " "FROM conversion_action " "WHERE conversion_action.status != 'REMOVED'" ), }, "cache_table": "gads_conv_actions", "mode": "replace", }, }, sql_data_sources={ "by_category": { "shape": "rows", "sql": ( "SELECT conversion_action_category AS category, " "COUNT(*) AS total_actions, " "SUM(CASE WHEN conversion_action_primary_for_goal THEN 1 ELSE 0 END) AS primary_actions " "FROM gads_conv_actions GROUP BY 1 ORDER BY 1" ), }, "actions": { "shape": "rows", "sql": ( "SELECT conversion_action_name AS name, " "conversion_action_category AS category, " "conversion_action_status AS status, " "conversion_action_primary_for_goal AS primary " "FROM gads_conv_actions ORDER BY name" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text from prefab_ui.components.charts import BarChart, ChartSeries with PrefabApp(title="Conversion Tracking Audit") as app: primary_actions = sum(row.get("primary_actions", 0) for row in by_category) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Conversion Tracking Audit") Muted("Configured actions, primary actions, and status") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Actions", value=f"{len(actions):,}") with Card(): with CardContent(): Metric(label="Primary Actions", value=f"{primary_actions:,.0f}") with Grid(columns=[2, 1], gap=6): with Card(): with CardContent(): Text("Actions by category", css_class="text-sm font-medium text-muted-foreground mb-2") BarChart( data=by_category, x_axis="category", series=[ ChartSeries(data_key="total_actions", label="Total"), ChartSeries(data_key="primary_actions", label="Primary"), ], show_legend=True, height=300, ) with Card(): with CardContent(): Text("Action inventory", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="name", header="Action"), DataTableColumn(key="category", header="Category"), DataTableColumn(key="status", header="Status"), DataTableColumn(key="primary", header="Primary"), ], rows=actions, ) """, refresh={"mode": "manual"}, ) ``` ## Variants - Join with the `conversion-by-action.md` cache table to overlay actual conversion volume on each action's setup status. - Filter to `category = 'PURCHASE'` for ecommerce-only audits. -
search-terms-waste.md 4.3 KB
# Search Terms Waste Search terms report showing query-level cost, clicks, conversions, and conversion value. ## When to use - "What search terms are getting spend?" - "Which search terms converted?" - "Show search terms by campaign/ad group." - Before discussing negatives, inspect the actual search-term evidence. ## Inputs - `customer_id` (required). - `date_range` (default: `LAST_30_DAYS`). - Optional: `campaign_ids` to scope. - Threshold: cost-without-conversion floor (default: $10 / 10_000_000 micros over the window). Use the GAQL below through `google_ads_gaql_query` for both ad-hoc reporting and refreshable dashboards. ## GAQL ```sql SELECT search_term_view.search_term, search_term_view.status, campaign.name, ad_group.name, metrics.cost_micros, metrics.clicks, metrics.conversions, metrics.conversions_value FROM search_term_view WHERE segments.date BETWEEN '{start}' AND '{end}' ``` ## How to interpret - Highest spend with zero conversions over a meaningful window = top candidates for negatives. - Off-intent terms (job seekers when you sell software, "free" when you sell paid, competitor names you don't want to bid on) deserve campaign- or account-level negatives, not ad-group-level. - A single ad group surfacing many wasteful themes signals a structural problem; pair with [campaign-structure.md](campaign-structure.md). ## Building a dashboard from this report ```python result = data_apps_build( name="Search Terms Waste", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": { "customer_id": "123-456-7890", "query": ( "SELECT search_term_view.search_term, search_term_view.status, " "campaign.name, ad_group.name, " "metrics.cost_micros, metrics.clicks, " "metrics.conversions, metrics.conversions_value " "FROM search_term_view " "WHERE segments.date DURING LAST_30_DAYS" ), }, "cache_table": "gads_search_terms", "mode": "replace", }, }, sql_data_sources={ "top_waste": { "shape": "rows", "sql": ( "SELECT search_term_view_search_term AS term, " "campaign_name AS campaign, " "SUM(metrics_cost_micros)/1e6 AS spend, " "SUM(metrics_clicks) AS clicks " "FROM gads_search_terms " "GROUP BY 1, 2 " "HAVING SUM(metrics_conversions) = 0 " "AND SUM(metrics_cost_micros) > 10000000 " "ORDER BY spend DESC LIMIT 50" ), }, }, prefab_python=""" from prefab_ui import PrefabApp from prefab_ui.components import Card, CardContent, Column, DataTable, DataTableColumn, Grid, Heading, Metric, Muted, Text with PrefabApp(title="Search Terms Waste") as app: wasted_spend = sum(row.get("spend", 0) for row in top_waste) with Column(gap=6, css_class="p-6"): with Column(gap=1): Heading("Search Terms Waste") Muted("Highest-spend terms with weak or missing conversion value") with Grid(columns=4, gap=4): with Card(): with CardContent(): Metric(label="Flagged Terms", value=f"{len(top_waste):,}") with Card(): with CardContent(): Metric(label="Flagged Spend", value=f"${wasted_spend:,.0f}") with Card(): with CardContent(): Text("Waste candidates", css_class="text-sm font-medium text-muted-foreground mb-2") DataTable( columns=[ DataTableColumn(key="term", header="Search Term"), DataTableColumn(key="campaign", header="Campaign"), DataTableColumn(key="spend", header="Spend"), DataTableColumn(key="clicks", header="Clicks"), ], rows=top_waste, ) """, refresh={"mode": "scheduled", "cron": "0 */6 * * *"}, ) ``` ## Variants - Group by theme (regex patterns over `search_term`) to surface thematic waste rather than per-query waste. - Add `metrics.average_cpc` to spot expensive-per-click terms even when total spend is small.
-
-
constraints.md 2.3 KB
# Constraints and rules Re-check this file at each creation step. ## Blueprint Features | Feature | Details | | --- | --- | | **Smart defaults** | Budget name, start date, network settings, ad group type auto-filled. | | **Location resolution** | Pass `location_names` (human-readable) instead of IDs. | | **Client-side validation** | Char limits, match types, image ratios checked before any API call. | | **Batch operations** | Keywords, extensions, ad schedules batched for efficiency. | | **Cleanup on failure** | Entire campaign removed if any step fails after campaign creation. | | **Ad extensions** | Sitelinks, callouts, structured snippets — created and linked automatically. | | **Ad scheduling** | Day-parting at campaign level via `ad_schedules`. | | **Audiences** | Target or exclude audiences per ad group. | | **Conversion actions** | Link specific conversion actions via `conversion_action_ids`. | ## Bidding Strategies | Strategy | Fields | Notes | | --- | --- | --- | | `MANUAL_CPC` | — | Manual cost-per-click. | | `MAXIMIZE_CLICKS` | — | Default, good starting point. | | `MAXIMIZE_CONVERSIONS` | `target_cpa_micros` (optional) | Requires conversion tracking. | | `MAXIMIZE_CONVERSION_VALUE` | `target_roas` (optional) | Requires conversion values. | | `TARGET_CPA` | `target_cpa_micros` (required) | Sets a target cost per acquisition. | | `TARGET_ROAS` | `target_roas` (required) | Sets a target return on ad spend. | > For PMax: `TARGET_CPA` and `TARGET_ROAS` are translated to constraints within `MAXIMIZE_CONVERSIONS` / `MAXIMIZE_CONVERSION_VALUE`. ## Technical Rules - Budget in micros: $50 → 50,000,000. - Create PAUSED; activate only after approval. - Location resolution: `google_ads_locations_search` to resolve names, then `google_ads_location_targets_add`. - RSA limits: headlines ≤30 chars (min 3), descriptions ≤90 chars (min 2). - Creative source: only from the actual site; no generic claims. - Image assets must already exist in the account (use `google_ads_assets_list` to find them). ## Critical Safety Rules **Never:** - Assume URL/budget/location IDs. - Skip research. - Invent keywords/copy. - Apply rigid "best practices" without context. - Build without buy-in. - Skip the preview step before creation. - Join `ad_group_criterion` with `search_term_view` in GAQL. - Query more than 5 accounts in a single batch. -
discovery.md 2.6 KB
# Discovery, research, and consultation Every campaign build starts here. Do not skip any phase — the pre-creation summary at the end must be approved before anything is created. ## Phase 1: Initial Setup Call `google_ads_accounts_list()` to list accessible accounts. - If multiple: ask the user to select one. - If single: inform the user and proceed. **CRITICAL**: Verify account access before any operations. ## Phase 2: Discovery & Research (MANDATORY) ### Research Steps 1. Get the real domain (ask; don't infer). 2. Scan the site end-to-end: home, product/service, pricing, about, FAQs, locations, contact, landing pages. 3. Understand funnel & goals: primary conversions, CTAs, forms/checkout, thank-you pages. 4. Extract messaging: value props, differentiators, proof, offers. ### Conversion Tracking Check Inspect conversion actions on the selected account: ``` google_ads_gaql_query( customer_id="<from list_accounts>", query=""" SELECT conversion_action.id, conversion_action.name, conversion_action.status, conversion_action.type FROM conversion_action WHERE conversion_action.status = 'ENABLED' """ ) ``` ### Market & Keyword Research - Inspect SERPs, competitors, and themes. - Propose keyword candidates (intent-aligned). - Identify initial negatives. ### Confirm Criticals - Daily budget (+currency). - Served geos. - Constraints. - Tracking status. ## Phase 3: Consultation Act as a partner, not order-taker: - Present findings (site + GAQL + market). - Recommend bidding (default Smart Bidding when tracking exists) with trade-offs. - Propose structure (campaign → themed ad groups → keywords + match types). - Suggest locations via `google_ads_locations_search(customer_id="...", location_names="New York")`. Note: `location_names` is a single string, not an array — pass a city, state, country, or postal code and the tool returns matching geo target IDs. - Set budget expectations via benchmark ranges. - Show reasoning for each choice. ## Phase 4: Pre-Creation Summary (Must Be Approved) ``` Campaign Strategy for [Business Name] Sources: [URL], GAQL Conversion Setup: [GAQL findings] Primary Goal: [objective + why] Bidding: [strategy + why] Budget: $[X]/day (expectations) Locations: [targets + rationale] Keyword Themes: [themes + match types + 2-3 examples] Messaging: [angles pulled from site] Trade-offs/Risks: [bullets] Approve to proceed? ``` Wait for explicit approval. No emojis. No assumptions. Once approved, continue to the matching campaign file: [campaigns/search-display.md](campaigns/search-display.md) or [campaigns/pmax.md](campaigns/pmax.md). -
heuristics.md 1.2 KB
# Google Ads Reporting Notes ## When to use Read this when explaining a Google Ads report. These notes describe how to keep claims tied to the queried data; they are not optimization rules. ## Reporting rules - State the customer ID and date range used. - Name the GAQL resource queried, such as `campaign`, `ad_group`, or `conversion_action`. - Treat rows as evidence, not as automatic recommendations. - Do not infer account changes from a report unless the user explicitly asks for recommendations. - If a report depends on conversion actions, use `segments.conversion_action` and `segments.conversion_action_name` for campaign-level metrics. - If a report needs configured conversion-action metadata, query `FROM conversion_action` separately. ## Optional dashboard/data app rules - Use `google_ads_gaql_query` inside `tool_data_sources`. - Query the saved cache table with `sql_data_sources`. - Bind only SQL output variables inside the dashboard/data app code. - Return the persisted dashboard artifact as the final output only when the user asked for an interface or the task explicitly chose one. - Do not describe internal interface implementation details to the user unless they ask. -
mcc.md 1.8 KB
# Multi-Account Workflows (MCC) When a user provides a manager account (MCC) with multiple sub-accounts: ## Batching Rule **Never query more than 5 sub-accounts in a single run.** Each `google_ads_gaql_query` call goes through a proxy with a timeout. Querying 10+ accounts sequentially in one loop reliably causes 504 timeouts. Process in batches of 5 and aggregate results before proceeding: ``` Batch 1: accounts[0:5] → collect results Batch 2: accounts[5:10] → collect results Batch 3: accounts[10:15] → collect results ...merge all batches... ``` Announce progress to the user: `"Processing accounts 1-5 of 23..."`. ## 504 Timeout Recovery If a `google_ads_gaql_query` call times out (504 / "Google Ads API timed out after retries"): 1. The tool already retries 3× internally — do not retry immediately. 2. Reduce the scope: narrow the date range, add a `LIMIT`, or split the account list further. 3. Tell the user which accounts succeeded and which failed; do not silently drop accounts. ## Cross-Account Search Term Report (Most Common Use Case) For MCC search term reports across many sub-accounts: ``` 1. Get all accounts: google_ads_accounts_list() Filter to sub-accounts: [a for a in result.accounts if not a["manager"]] 2. For each batch of 5 accounts: - Run search_term_view GAQL (using segments.keyword.info.*, NOT ad_group_criterion). - Collect results tagged with customer.descriptive_name. 3. Merge all batches into a single dataset. 4. Write to Google Sheets or CSV file for the user. ``` Minimum required columns for a search term report: - Search Term, Account Name, Keyword, Match Type, Campaign, Ad Group. - Clicks, Impressions, Conversions, Cost. - Add Negative (Yes/No — your recommendation). GAQL resource-compatibility rules (which fields work with `search_term_view` vs `keyword_view`) are in [reporting.md](reporting.md). -
report-template.md 582 B
# Google Ads Report Template ## When to use Read this when writing the final response for a Google Ads report or dashboard. Keep the response evidence-backed and clear about whether a dashboard artifact was created. ```text Google Ads Report for [Account Name] Date range: [Range] Data source: [GAQL resource and key segments] Output - Dashboard ID: - Dashboard name: - Refreshed by: What was queried - Tool: - GAQL: - Cache table: Key results - [metric/result 1] - [metric/result 2] - [metric/result 3] Notes - [freshness caveat, missing rows, or user-provided mapping] ``` -
reporting.md 6.4 KB
# Reporting and GAQL Report on existing Google Ads accounts using GAQL-backed data. Dashboards and data apps are optional presentation surfaces, not mandatory output. Every reporting task starts here, then picks the report recipe with the closest data shape from [reports/](reports/). ## Core contract - Use `google_ads_gaql_query` as the canonical Google Ads data tool. - Written GAQL-backed reports are valid outputs. Build a dashboard or data app only when the user asks for one or when an interactive view materially improves the answer. - Read the `google_ads_gaql_query` descriptor and select the account whose data the report needs. - Treat rows as evidence, not as automatic recommendations ([heuristics.md](heuristics.md)). - Keep reporting separate from live account changes. - Do not mention internal dashboard implementation details to the user unless they explicitly ask how the interface is built. ## Phase 1: Access and scope 1. Call `google_ads_accounts_list()` before touching account data. 2. Confirm the customer ID if more than one account is available. 3. Confirm the reporting window if the user has not given one. 4. If the user did not ask for a dashboard/data app and the best output is unclear, ask whether they want a written report or an interactive view. ## Phase 2: Pick the report | The user wants... | Read | |---|---| | Full account health snapshot | [reports/account-overview.md](reports/account-overview.md) | | Why aren't conversions tracking right | [reports/conversion-tracking.md](reports/conversion-tracking.md) | | Conversions broken down by action / per stage | [reports/conversion-by-action.md](reports/conversion-by-action.md) | | Lead-gen / B2B multi-stage funnel | [reports/conversion-funnel.md](reports/conversion-funnel.md) | | Where is spend going | [reports/budget-distribution.md](reports/budget-distribution.md) | | Wasteful search terms | [reports/search-terms-waste.md](reports/search-terms-waste.md) | | Should we restructure | [reports/campaign-structure.md](reports/campaign-structure.md) | | Campaign performance over time | [reports/campaign-performance.md](reports/campaign-performance.md) | | Ad-level performance | [reports/ad-performance.md](reports/ad-performance.md) | When the user asks for a funnel dashboard, use the conversion funnel recipe. ## Phase 3: Query with GAQL Use GAQL fields that are selectable with the report resource. For campaign-level conversion action reporting, use campaign metrics segmented by `segments.conversion_action` and `segments.conversion_action_name`. Do not select `conversion_action.id` or `conversion_action.name` from `FROM campaign`; Google rejects that shape. Query the `conversion_action` resource separately only when you need the inventory of configured conversion actions. ### GAQL Resource Compatibility **Critical:** Not all Google Ads resources can be joined in a single query. **search_term_view** — use `segments.keyword.info.*` for keyword text and match type — **never** `ad_group_criterion.*`: ```sql SELECT search_term_view.search_term, customer.descriptive_name, segments.keyword.info.text, segments.keyword.info.match_type, campaign.name, ad_group.name, metrics.clicks, metrics.impressions, metrics.conversions, metrics.cost_micros FROM search_term_view WHERE segments.date DURING LAST_7_DAYS ORDER BY metrics.impressions DESC ``` `ad_group_criterion` is **incompatible** with `search_term_view` as the FROM resource. Selecting `ad_group_criterion.*` fields will always fail with `PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE`. **keyword_view** — for keyword performance (Quality Score, bid estimates): ```sql SELECT ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, metrics.clicks, metrics.impressions, metrics.historical_quality_score FROM keyword_view WHERE segments.date DURING LAST_30_DAYS ``` ## Phase 4: Optional dashboard/data app Skip this phase for plain written reports. When the user asks for a dashboard or data app, or an interactive view is clearly the best presentation, use the matching report's dashboard section as the implementation recipe. Before authoring a custom dashboard: - Inspect the live `data_apps_build` tool schema first — it documents the accepted data-source shapes and UI components. Do not invent dashboard patterns or component names. The implementation pattern is: 1. `tool_data_sources` — the report's GAQL runs through `google_ads_gaql_query` and the rows are saved to a cache table (convention: `gads_<report>_<scope>`). 2. `sql_data_sources` — re-aggregate the cache table into named variables (KPIs, time series, top lists). 3. Build the interface with cards, KPIs, charts, tables, and a clear visual hierarchy. Bind only SQL output variables inside the dashboard/data app code. 4. `refresh` — set `{"mode": "scheduled", "cron": "0 * * * *"}` to keep the dashboard live; default is manual. ```python data_apps_build( name="Google Ads Report", tool_data_sources={ "raw": { "tool_name": "google_ads_gaql_query", "tool_args": {"customer_id": "...", "query": "..."}, "cache_table": "gads_report_raw", "mode": "replace", } }, sql_data_sources={...}, prefab_python="...", ) ``` The tool source name is not a Python variable. It populates the cache table. SQL sources create the variables used by the interface code. Re-run the same call (or call `data_apps_refresh`) to refresh without re-invoking the agent. Do not inject Google Ads credentials into the dashboard/data app runtime. ## Phase 5: Response format Use the template in [report-template.md](report-template.md). State what data was queried, which date range was used, and whether the final output is a written report, dashboard, data app, or published interface. ## Warehouse data When the workspace has a Google Ads warehouse connection, inspect its available tables and columns and query the selected connection through `database_query`. Existing warehouse data remains available independently of native Google Ads API calls. Check Warehouse Data Status for that connection and report its last successful refresh. Name the customer IDs, date boundaries, currency units and metric definitions. Do not assume an hourly cache, fixed table name or fresh rows merely because the connection is healthy. Use `google_ads_gaql_query` when the requested data is not in the configured warehouse.
-
-
SKILL.md 8.9 KB
--- name: google-ads description: Plan and create new Google Ads campaigns and report on existing accounts via the Hyper MCP. Use when the user wants to launch Search, Display, Performance Max, Video, Demand Gen, Shopping, or App campaigns, build Google Ads reports or dashboards, diagnose conversion tracking, or mentions Google Ads, AdWords, search ads, display ads, Performance Max, PMax, PPC, Google campaigns, search term reports, budget analysis, conversion funnels, negative keywords, or manager accounts (MCC). requires_toolkits: - google_ads icon: google_ads short_description: Plan and create Google Ads campaigns and build GAQL-backed reports and dashboards. --- # Google Ads Strategic guide for building new Google Ads campaigns and reporting on existing accounts. Research first, consult intelligently, validate everything, and build with the granular resource tools in dependency order (budget → campaign → targeting → ad groups → ads / asset groups). The API has exactly one campaign create — `google_ads_campaigns_create` — and the campaign type is just `advertising_channel_type`; type-specific requirements are enforced by the tools' built-in validators, which return corrective errors. Reporting is GAQL-backed and evidence-first — dashboards are optional presentation surfaces. ## Requirements - **Hyper MCP installed and connected.** [https://app.hyperfx.ai/mcp](https://app.hyperfx.ai/mcp) - **Google Ads integration connected** at [https://app.hyperfx.ai/apps](https://app.hyperfx.ai/apps). If `search("google_ads_accounts_list")` does not find `google_ads_accounts_list`, stop and tell the user to enable Hyper MCP and connect Google Ads. ### How to run the tools in this skill Every tool in this skill is named by its canonical tool name. Run it with the call your surface gives you: | Surface | Find a tool | Run it | | --- | --- | --- | | MCP client (Claude, Cursor, Codex, ChatGPT) | `search("<what you want to do>")`, then `describe("<name>")` | `call("<name>", {...})` | | Hyper CLI | `hyperai search "<what you want to do>"`, then `hyperai describe <name>` | `hyperai call <name> --json '{...}'` | If a tool is not found, its integration is not connected or not enabled for the workspace: stop and tell the user which integration to connect. ## Out of scope - **Live optimization mutations** (bid adjustments, pausing keywords, restructuring existing campaigns). Reports may *recommend* these changes; applying them needs explicit per-change user approval. - **Creative generation** (headlines, descriptions, images) → [`ad-creative-generation`](../ad-creative-generation). - **Cross-platform campaign launches** → use this skill for Google, then invoke `meta-ads` / `tiktok-ads` separately. ## Tool surface Reads are GAQL (`google_ads_gaql_query` covers every resource). Writes are one tool per resource operation, named `google_ads_<resource>_<create|update|remove>`; create/update take `(customer_id, body)` where `body` is the bare resource object in snake_case. | Tool | Purpose | | --- | --- | | `google_ads_accounts_list` | Discover accessible accounts (and MCC sub-accounts). | | `google_ads_gaql_query` | Run a GAQL query (conversion actions, search terms, any resource). | | `google_ads_campaign_budgets_create` | Create the budget first (micros!). | | `google_ads_campaigns_create` / `_update` / `_remove` | The ONE campaign create — every `advertising_channel_type` (SEARCH, DISPLAY, PERFORMANCE_MAX, VIDEO, DEMAND_GEN, SHOPPING, MULTI_CHANNEL for App). | | `google_ads_ad_groups_create` / `_update`, `google_ads_ad_groups_delete` | Ad groups under a campaign. | | `google_ads_ad_group_ads_create` / `_update` / `_remove` | Ads (typed: `responsive_search_ad`, `responsive_display_ad`, video / demand-gen ad types). | | `google_ads_keywords_create` / `_update` / `_delete` / `_list` | Keywords (positive and negative). | | `google_ads_locations_search`, `google_ads_location_targets_add` | Resolve location names, target them. | | `google_ads_image_assets_upload` | Image assets — accepts `file_id` from the workspace file manager, `image_url`, or base64. | | `google_ads_video_assets_link` | Register a YouTube video ID as a video asset (the Ads API cannot host video files — videos must be on YouTube). | | `google_ads_asset_groups_create` | PMax asset group (atomic: text + image assets in one call). | | `google_ads_conversion_actions_create`, `google_ads_user_lists_create`, `google_ads_bidding_strategies_create`, `google_ads_shared_sets_create`, … | Full per-resource CRUD surface — same naming pattern. | | `google_ads_request` | Raw escape hatch for any uncovered endpoint. | | `data_apps_build`, `data_apps_refresh` | Optional dashboards / data apps for reports. | ## Rules that must never be forgotten > **BUDGETS IN MICROS**: $50/day = 50,000,000 micros. Never pass dollar amounts directly. > **ALWAYS START PAUSED**: Create campaigns with `status="PAUSED"`. Activate only after explicit user approval. > **SUMMARIZE BEFORE CREATE**: Present the full build plan (budget, campaign settings, targeting, ad groups, ads/assets) and get explicit user approval before the first mutate. Create in dependency order and reference each created resource by the `resource_name` the mutate returns. The tools validate bodies against the real API schema (writable fields, enum values) plus channel-type rules and return corrective errors — fix and retry rather than guessing. > **NEVER INVENT**: Don't assume URLs, budgets, or location IDs. Don't skip research. Don't invent keywords or copy — creative comes only from the actual site. Don't build without user buy-in. > **GAQL GUARDRAILS**: Never join `ad_group_criterion` with `search_term_view`. Never query more than 5 sub-accounts in a single batch. See [references/constraints.md](references/constraints.md) for the full constraint set. > **All reference files live in `references/`.** Read them at `references/<file>` (e.g. `references/discovery.md`). They are not in the same directory as this SKILL.md. ## Core process Every campaign build follows this sequence. Do not skip steps. 1. **Identify the goal** — new campaign, reporting/analysis, or both? 2. **Check the routing table** and read the referenced files before calling any tools 3. **Discovery is mandatory for creation** ([references/discovery.md](references/discovery.md)) — account setup, site scan, conversion tracking check, market research, consultation 4. **Present the pre-creation summary** and wait for explicit approval 5. **Build in dependency order (budget → campaign PAUSED → targeting → ad groups → ads/assets)** after approval, re-checking [references/constraints.md](references/constraints.md) at each step 6. **Activate only when the user approves** Full workflow: Initial Setup → Research (site + GAQL + market) → Analyze (goals, audience, keywords) → Consult (options + trade-offs) → Recommend (structure + bids) → Confirm (summary approved) → Create in dependency order (PAUSED) → Verify by GAQL → Activate (post-approval). ## Routing table | The user wants to… | Read these files first | |---|---| | Create a Search or Display campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/search-display.md](references/campaigns/search-display.md) | | Create a Performance Max campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/pmax.md](references/campaigns/pmax.md) | | Add an asset group to an existing PMax campaign | [references/campaigns/pmax.md](references/campaigns/pmax.md) | | Create a Video, Demand Gen, Shopping, or App campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/other-types.md](references/campaigns/other-types.md) | | Run a report / GAQL query / analyze performance (any kind) | [references/reporting.md](references/reporting.md) → the matching `references/reports/*.md` recipe | | Account health snapshot / "how is the account doing?" | [references/reporting.md](references/reporting.md) → [references/reports/account-overview.md](references/reports/account-overview.md) | | Diagnose conversion tracking | [references/reporting.md](references/reporting.md) → [references/reports/conversion-tracking.md](references/reports/conversion-tracking.md) | | Find wasteful search terms / negative keyword candidates | [references/reporting.md](references/reporting.md) → [references/reports/search-terms-waste.md](references/reports/search-terms-waste.md) | | Build a Google Ads dashboard or data app | [references/reporting.md](references/reporting.md) → matching report recipe's dashboard section | | Work across an MCC / many sub-accounts | [references/mcc.md](references/mcc.md) → [references/reporting.md](references/reporting.md) | | Goal not yet clear | [references/discovery.md](references/discovery.md) — discovery clarifies the goal | Reporting responses follow [references/report-template.md](references/report-template.md); keep claims tied to queried data per [references/heuristics.md](references/heuristics.md).
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.