google-ads-workflow
Use when building Google Ads MCC operation pipelines — cross-account GAQL reporting, campaign health checks, budget monitoring, and bulk status operations across multiple client accounts.
| Model | Source |
|---|---|
| sonnet | pack: google-cli |
Full Reference
Google Ads Workflow
Section titled “Google Ads Workflow”Pipeline skill for composing multi-step Google Ads operations using gaql-cli, mcc-gaql-rs, and google-ads-mcp. Core pattern: Query→Aggregate→Act across MCC child accounts.
Mandatory Announcement — FIRST OUTPUT before anything else:
┏━ ⚡ google-ads-workflow ━━━━━━━━━━━━━━━━━━━━━━━━━┓┃ [one-line description of workflow being built] ┃┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛No exceptions. Box frame first, then work.
Tool Selection Matrix
Section titled “Tool Selection Matrix”Three CLI tools for Google Ads — choose based on scope:
| Tool | Best For | Auth |
|---|---|---|
gaql (gaql-cli) | Single account queries, interactive REPL exploration | OAuth2 config.toml |
mcc-gaql | Cross-account GAQL runs, MCC hierarchy queries | OAuth2 config.toml + login_customer_id |
google-ads-mcp | Agent-driven operations, natural language, MCP server | ADC or google-ads.yaml |
Decision rule:
- Exploring one account? →
gaqlREPL - Report across all MCC child accounts? →
mcc-gaql - AI agent needs Ads access? →
google-ads-mcpserver - Bulk status updates? →
gaqlwithMUTATEormcc-gaqlwith iteration
Pipeline Patterns
Section titled “Pipeline Patterns”1. MCC Health Check
Section titled “1. MCC Health Check”Query all child accounts, flag anomalies (zero spend, disapproved ads, low impressions).
#!/usr/bin/env bash# MCC health check pipeline
MCC_ID="123-456-7890"DATE_RANGE="LAST_7_DAYS"
echo "Running MCC health check for ${MCC_ID}..."
# Step 1: Get all active child accountsACCOUNTS=$(mcc-gaql \ --mcc-id "$MCC_ID" \ --query "SELECT customer.id, customer.descriptive_name FROM customer WHERE customer.status = 'ENABLED'" \ --output json \ | jq -r '.[] | "\(.customer_id),\(.customer_descriptive_name)"')
# Step 2: Check each account for health indicatorsecho "$ACCOUNTS" | while IFS=, read -r account_id account_name; do # Get spend + impression metrics METRICS=$(gaql \ --customer-id "$account_id" \ --query "SELECT metrics.cost_micros, metrics.impressions, metrics.clicks FROM campaign WHERE segments.date DURING ${DATE_RANGE} AND campaign.status = 'ENABLED'" \ --output json \ | jq '{ total_spend: ([.[].metrics_cost_micros] | add // 0) / 1000000, total_impressions: ([.[].metrics_impressions] | add // 0), total_clicks: ([.[].metrics_clicks] | add // 0) }')
SPEND=$(echo "$METRICS" | jq '.total_spend') IMPRESSIONS=$(echo "$METRICS" | jq '.total_impressions')
# Flag anomalies if [ "$(echo "$SPEND < 1" | bc -l)" = "1" ]; then echo "⚠ ZERO_SPEND: $account_name ($account_id) — \$${SPEND} last 7 days" elif [ "$IMPRESSIONS" -lt 100 ] 2>/dev/null; then echo "⚠ LOW_IMPRESSIONS: $account_name ($account_id) — ${IMPRESSIONS} impressions" else echo "✓ $account_name ($account_id) — \$${SPEND} spend, ${IMPRESSIONS} impressions" fidone2. Weekly Performance Report
Section titled “2. Weekly Performance Report”Cross-account GAQL report → CSV → summary.
#!/usr/bin/env bash# Weekly performance report — all MCC child accounts
MCC_ID="123-456-7890"OUTPUT_FILE="weekly-report-$(date +%Y-%m-%d).csv"
QUERY="SELECT customer.descriptive_name, customer.id, campaign.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_valueFROM campaignWHERE segments.date DURING LAST_7_DAYS AND campaign.status != 'REMOVED'ORDER BY metrics.cost_micros DESC"
echo "Generating weekly report..."
# Write CSV headerecho "Account,Customer ID,Campaign,Impressions,Clicks,Cost,Conversions,Conv Value" > "$OUTPUT_FILE"
# Run cross-account GAQLmcc-gaql \ --mcc-id "$MCC_ID" \ --query "$QUERY" \ --output csv \ >> "$OUTPUT_FILE"
# Print summary statsTOTAL_SPEND=$(tail -n +2 "$OUTPUT_FILE" | awk -F, '{sum += $7} END {printf "%.2f", sum/1000000}')TOTAL_CONV=$(tail -n +2 "$OUTPUT_FILE" | awk -F, '{sum += $8} END {printf "%.0f", sum}')ACCOUNTS=$(tail -n +2 "$OUTPUT_FILE" | awk -F, '{print $2}' | sort -u | wc -l | tr -d ' ')
echo ""echo "Weekly Report Summary"echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"echo "Accounts: $ACCOUNTS"echo "Total Spend: \$${TOTAL_SPEND}"echo "Total Conversions: ${TOTAL_CONV}"echo "Output: $OUTPUT_FILE"3. Budget Pacing Monitor
Section titled “3. Budget Pacing Monitor”Daily spend vs monthly budget — flag over/under-pacing.
#!/usr/bin/env bash# Budget pacing monitor
CUSTOMER_ID="123-456-7890"DAY_OF_MONTH=$(date +%-d)DAYS_IN_MONTH=$(cal | awk 'NF{f=$NF}; END{print f}')EXPECTED_PACING=$(echo "scale=4; $DAY_OF_MONTH / $DAYS_IN_MONTH" | bc)
echo "Budget pacing check — Day ${DAY_OF_MONTH}/${DAYS_IN_MONTH} (expected: $(echo "$EXPECTED_PACING * 100" | bc | cut -d. -f1)%)"echo ""
gaql \ --customer-id "$CUSTOMER_ID" \ --query "SELECT campaign.name, campaign_budget.amount_micros, metrics.cost_micros FROM campaign WHERE campaign.status = 'ENABLED' AND segments.date DURING THIS_MONTH" \ --output json \ | jq -r '.[] | { name: .campaign_name, budget: (.campaign_budget_amount_micros / 1000000), spent: (.metrics_cost_micros / 1000000), pacing: (if .campaign_budget_amount_micros > 0 then (.metrics_cost_micros / .campaign_budget_amount_micros) else 0 end) }' \ | jq -r '"| \(.name) | $\(.spent | tostring) / $\(.budget | tostring) | \((.pacing * 100) | tostring)% |"' \ | while read -r line; do PACING_VAL=$(echo "$line" | grep -oP '\d+(?=% \|)') if [ -n "$PACING_VAL" ] && [ "$PACING_VAL" -lt "$(echo "$EXPECTED_PACING * 100 * 0.8" | bc | cut -d. -f1)" ]; then echo "⚠ UNDER $line" elif [ -n "$PACING_VAL" ] && [ "$PACING_VAL" -gt "$(echo "$EXPECTED_PACING * 100 * 1.2" | bc | cut -d. -f1)" ]; then echo "⚡ OVER $line" else echo "✓ OK $line" fi done4. Campaign Status Audit (Across MCC)
Section titled “4. Campaign Status Audit (Across MCC)”Find all paused/removed campaigns across all child accounts — surface ones that might need re-enabling.
#!/usr/bin/env bash# Campaign status audit across MCC
MCC_ID="123-456-7890"
echo "Campaign status audit — MCC ${MCC_ID}"echo ""
mcc-gaql \ --mcc-id "$MCC_ID" \ --query "SELECT customer.descriptive_name, customer.id, campaign.name, campaign.status, campaign.end_date, metrics.cost_micros FROM campaign WHERE campaign.status IN ('PAUSED', 'REMOVED') ORDER BY customer.descriptive_name, campaign.status" \ --output json \ | jq -r '.[] | "\(.customer_descriptive_name) | \(.customer_id) | \(.campaign_name) | \(.campaign_status) | \(.campaign_end_date // "no end date") | $\(.metrics_cost_micros / 1000000 | tostring)"' \ | column -t -s '|'5. Keyword Opportunity (Search Terms → New Keywords)
Section titled “5. Keyword Opportunity (Search Terms → New Keywords)”Pull search terms report, identify high-performing terms not yet in keyword list.
#!/usr/bin/env bash# Keyword opportunity finder
CUSTOMER_ID="123-456-7890"MIN_CONVERSIONS=2MIN_CLICKS=10
echo "Finding keyword opportunities (min ${MIN_CONVERSIONS} conv, ${MIN_CLICKS} clicks)..."
gaql \ --customer-id "$CUSTOMER_ID" \ --query "SELECT search_term_view.search_term, search_term_view.status, metrics.clicks, metrics.impressions, metrics.conversions, metrics.cost_micros FROM search_term_view WHERE metrics.conversions >= ${MIN_CONVERSIONS} AND metrics.clicks >= ${MIN_CLICKS} AND search_term_view.status = 'NONE' AND segments.date DURING LAST_30_DAYS ORDER BY metrics.conversions DESC LIMIT 50" \ --output json \ | jq -r '.[] | [ .search_term_view_search_term, .metrics_clicks, .metrics_conversions, (.metrics_cost_micros / 1000000 | tostring), "ADD_AS_KEYWORD" ] | @csv'Error Handling
Section titled “Error Handling”| Error | Recovery |
|---|---|
PERMISSION_DENIED | Verify developer token tier (Basic vs Standard), check login_customer_id |
INVALID_ARGUMENT: date range | Use GAQL date range enum: LAST_7_DAYS, THIS_MONTH, not raw dates |
RESOURCE_EXHAUSTED (quota) | Add sleep 2 between account iterations, check daily ops quota |
AUTHENTICATION_ERROR | Re-auth: gaql auth or regenerate google-ads.yaml |
DEVELOPER_TOKEN_NOT_APPROVED | Apply for Standard access at ads.google.com/nav/selectaccount |
mcc-gaql: no accounts found | Verify MCC ID format: 123-456-7890 (with dashes) |
| Empty metrics | Check date range — campaigns with no activity return null metrics |
Multi-Account Iteration Patterns
Section titled “Multi-Account Iteration Patterns”# Iterate stored MCC child accountsgaql \ --customer-id "$MCC_ID" \ --login-customer-id "$MCC_ID" \ --query "SELECT customer_client.id FROM customer_client WHERE customer_client.level = 1" \ --output json \ | jq -r '.[].customer_client_id' \ | while read -r child_id; do echo "Processing: $child_id" gaql --customer-id "$child_id" --login-customer-id "$MCC_ID" \ --query "SELECT campaign.name, metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS" \ --output json doneWorkflow Design Rules
Section titled “Workflow Design Rules”- Use mcc-gaql for cross-account queries — don’t loop gaql if mcc-gaql does it natively
- Always specify date ranges — no date filter = no data or very slow queries
- Filter REMOVED resources — add
WHERE campaign.status != 'REMOVED'to avoid deleted campaign noise - Micros → dollars — all
cost_microsvalues divide by 1,000,000 for dollar amounts - Rate limit iteration —
sleep 1between per-account API calls in loops - Pipe through jq — always extract only needed fields before display
- Test with single account first — validate GAQL query on one account before MCC run
Integration
Section titled “Integration”| Need | Skill |
|---|---|
| GAQL syntax reference | google-ads-cli → reference/gaql.md |
| MCC setup and auth | google-ads-cli → reference/mcc.md |
| Campaign management | google-ads-cli → reference/campaigns.md |
| Workspace result export | google-workspace-workflow (Sheets logging) |