Skip to content

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.

ModelSource
sonnetpack: google-cli
Full Reference

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.

Three CLI tools for Google Ads — choose based on scope:

ToolBest ForAuth
gaql (gaql-cli)Single account queries, interactive REPL explorationOAuth2 config.toml
mcc-gaqlCross-account GAQL runs, MCC hierarchy queriesOAuth2 config.toml + login_customer_id
google-ads-mcpAgent-driven operations, natural language, MCP serverADC or google-ads.yaml

Decision rule:

  • Exploring one account? → gaql REPL
  • Report across all MCC child accounts? → mcc-gaql
  • AI agent needs Ads access? → google-ads-mcp server
  • Bulk status updates? → gaql with MUTATE or mcc-gaql with iteration

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 accounts
ACCOUNTS=$(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 indicators
echo "$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"
fi
done

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_value
FROM campaign
WHERE segments.date DURING LAST_7_DAYS
AND campaign.status != 'REMOVED'
ORDER BY metrics.cost_micros DESC"
echo "Generating weekly report..."
# Write CSV header
echo "Account,Customer ID,Campaign,Impressions,Clicks,Cost,Conversions,Conv Value" > "$OUTPUT_FILE"
# Run cross-account GAQL
mcc-gaql \
--mcc-id "$MCC_ID" \
--query "$QUERY" \
--output csv \
>> "$OUTPUT_FILE"
# Print summary stats
TOTAL_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"

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
done

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=2
MIN_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'
ErrorRecovery
PERMISSION_DENIEDVerify developer token tier (Basic vs Standard), check login_customer_id
INVALID_ARGUMENT: date rangeUse 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_ERRORRe-auth: gaql auth or regenerate google-ads.yaml
DEVELOPER_TOKEN_NOT_APPROVEDApply for Standard access at ads.google.com/nav/selectaccount
mcc-gaql: no accounts foundVerify MCC ID format: 123-456-7890 (with dashes)
Empty metricsCheck date range — campaigns with no activity return null metrics
Terminal window
# Iterate stored MCC child accounts
gaql \
--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
done
  1. Use mcc-gaql for cross-account queries — don’t loop gaql if mcc-gaql does it natively
  2. Always specify date ranges — no date filter = no data or very slow queries
  3. Filter REMOVED resources — add WHERE campaign.status != 'REMOVED' to avoid deleted campaign noise
  4. Micros → dollars — all cost_micros values divide by 1,000,000 for dollar amounts
  5. Rate limit iterationsleep 1 between per-account API calls in loops
  6. Pipe through jq — always extract only needed fields before display
  7. Test with single account first — validate GAQL query on one account before MCC run
NeedSkill
GAQL syntax referencegoogle-ads-clireference/gaql.md
MCC setup and authgoogle-ads-clireference/mcc.md
Campaign managementgoogle-ads-clireference/campaigns.md
Workspace result exportgoogle-workspace-workflow (Sheets logging)