Claude Skill

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

LLM Mart · 0 points · 1 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download hyperfx-ai-marketing-skills-skills_google-ads-abd711a.zip · 34 KB
Part of hyperfx-ai/marketing-skills — 30 skills

Install

skills CLI npx skills add https://github.com/hyperfx-ai/marketing-skills/tree/main/skills/google-ads
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install hyperfx-ai-marketing-skills@llmmart
Git 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

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-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 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) — 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 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/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.

No comments yet.

Reviews (0)

No reviews yet.

Related