From e959028259acd2d9091a68861f5c12dfeca508b2 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:32:43 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 61 + skills/google-ads-scripts/SKILL.md | 369 +++++ .../assets/bid-manager-template.js | 391 +++++ .../assets/campaign-optimizer-template.js | 303 ++++ .../references/ads-api-reference.md | 1379 +++++++++++++++++ .../google-ads-scripts/scripts/validators.py | 407 +++++ 8 files changed, 2925 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/google-ads-scripts/SKILL.md create mode 100644 skills/google-ads-scripts/assets/bid-manager-template.js create mode 100644 skills/google-ads-scripts/assets/campaign-optimizer-template.js create mode 100644 skills/google-ads-scripts/references/ads-api-reference.md create mode 100644 skills/google-ads-scripts/scripts/validators.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..7c19c9a --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "google-apps-ads-script", + "description": "Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers.", + "version": "1.0.0", + "author": { + "name": "Henrik Soederlund", + "email": "whom-wealthy.2z@icloud.com" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d1c2e5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# google-apps-ads-script + +Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..9c342d0 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,61 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:henkisdabro/wookstar-claude-code-plugins:google-apps-ads-script", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "e189ade6df7f385263e08753fbbc125cb678fce0", + "treeHash": "2ff9d94b8b0423f501ed58a2308236632803ab539e1c71da7d1a0d1315e88d5a", + "generatedAt": "2025-11-28T10:17:25.728053Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "google-apps-ads-script", + "description": "Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "ee8bbe41c532b64ffc5ebbdb8be5185a94487cf7354e8b54d505a568fc9b0829" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "99e2427c474a49c4fca60a1373d2bb356959a15dae3d38af61158b1799dcdb14" + }, + { + "path": "skills/google-ads-scripts/SKILL.md", + "sha256": "14fd5fa210bfb5e4f35d23c815f3728db03b1174e3f0f78263da15f5a01c95ac" + }, + { + "path": "skills/google-ads-scripts/references/ads-api-reference.md", + "sha256": "6682095fefb4215365e6110ec8221aa4f3c48ee684737e57c71712d9d0cd70e1" + }, + { + "path": "skills/google-ads-scripts/scripts/validators.py", + "sha256": "495f7cc3ebd7946060b532567b79d221788e3b8ac3348cde543c3f5003a6ba0b" + }, + { + "path": "skills/google-ads-scripts/assets/campaign-optimizer-template.js", + "sha256": "4ab86b2c666c3a673daca4d582fb46bea953b8b7b0329a45e138e6e5e2131b48" + }, + { + "path": "skills/google-ads-scripts/assets/bid-manager-template.js", + "sha256": "0cf960d1ece0441f09c6071a236003adaeb278c5bc9fc1fdab08d90d63020227" + } + ], + "dirSha256": "2ff9d94b8b0423f501ed58a2308236632803ab539e1c71da7d1a0d1315e88d5a" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/google-ads-scripts/SKILL.md b/skills/google-ads-scripts/SKILL.md new file mode 100644 index 0000000..253cc4f --- /dev/null +++ b/skills/google-ads-scripts/SKILL.md @@ -0,0 +1,369 @@ +--- +name: google-ads-scripts +description: Expert guidance for Google Ads Script development including AdsApp API, campaign management, ad groups, keywords, bidding strategies, performance reporting, budget management, automated rules, and optimization patterns. Use when automating Google Ads campaigns, managing keywords and bids, creating performance reports, implementing automated rules, optimizing ad spend, working with campaign budgets, monitoring quality scores, tracking conversions, pausing low-performing keywords, adjusting bids based on ROAS, or building Google Ads automation scripts. Covers campaign operations, keyword targeting, bid optimization, conversion tracking, error handling, and JavaScript-based automation in Google Ads editor. +--- + +# Google Ads Scripts + +## Overview + +This skill provides comprehensive guidance for developing Google Ads Scripts using the AdsApp API. Google Ads Scripts enable automation of campaign management, bid optimization, performance reporting, and bulk operations through JavaScript code that runs directly in the Google Ads editor. + +## When to Use This Skill + +Invoke this skill when: + +- Automating Google Ads campaign management operations +- Managing keywords and adjusting bids programmatically +- Creating performance reports and dashboards +- Implementing automated rules for campaign optimization +- Optimizing ad spend based on ROAS or CPA targets +- Working with campaign budgets and spend limits +- Monitoring quality scores and pausing low-performers +- Tracking conversions and adjusting strategies +- Building bulk operations for large-scale account management +- Implementing time-based or performance-based automation +- Debugging Google Ads Script code or API issues + +## Core Capabilities + +### 1. Campaign Operations + +Manage campaigns programmatically including creation, modification, status changes, and bulk updates. The AdsApp.campaigns() selector provides filtering and iteration patterns. + +**Common operations:** +- Get campaigns with conditions (status, budget, name patterns) +- Modify campaign properties (name, budget, dates, status) +- Apply labels for organization +- Pause/enable campaigns based on performance criteria + +### 2. Keyword & Bid Management + +Automate keyword targeting and bid adjustments based on performance metrics. + +**Common operations:** +- Get keywords with quality score filtering +- Adjust max CPC bids based on ROAS/CPA targets +- Add/remove negative keywords +- Monitor keyword-level conversions +- Implement bid optimization algorithms + +### 3. Performance Reporting + +Generate custom reports using campaign, ad group, keyword, and ad statistics. + +**Common operations:** +- Retrieve metrics for custom date ranges +- Calculate derived metrics (CTR, CPC, conversion rate) +- Export data to Google Sheets +- Create automated dashboards +- Monitor KPIs and trigger alerts + +### 4. Budget Management + +Control spending and allocate budgets across campaigns. + +**Common operations:** +- Get/set daily campaign budgets +- Monitor spend against thresholds +- Pause campaigns when budget limits reached +- Distribute budgets based on performance + +### 5. Automated Rules & Optimization + +Build intelligence into campaign management with automated decision-making. + +**Common operations:** +- Pause low-performing keywords (low QS, high CPC, no conversions) +- Increase bids for high-performers +- Adjust budgets based on day-of-week patterns +- Implement custom bidding strategies + +### 6. Error Handling & Resilience + +Implement robust error handling to manage API limits, quota issues, and runtime errors. + +**Key patterns:** +- Try-catch blocks for error handling +- Null checks for optional properties +- Logging to sheets for audit trails +- Rate limiting awareness (30-minute execution limit) + +## Quick Start Examples + +### Example 1: Pause Low-Quality Keywords + +```javascript +function pauseLowQualityKeywords() { + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.quality_info.quality_score < 4') + .withCondition('keyword.metrics.cost > 100000000') // >100 spend + .get(); + + let count = 0; + while (keywords.hasNext()) { + const keyword = keywords.next(); + keyword.pause(); + count++; + } + + Logger.log(`Paused ${count} low-quality keywords`); +} +``` + +### Example 2: Optimize Bids Based on ROAS + +```javascript +function optimizeBidsByROAS() { + const TARGET_ROAS = 3.0; // 300% + + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.metrics.conversions > 5') // Min conversions + .get(); + + while (keywords.hasNext()) { + const keyword = keywords.next(); + const stats = keyword.getStatsFor('LAST_30_DAYS'); + const roas = stats.getReturnOnAdSpend(); + const currentBid = keyword.getMaxCpc(); + + if (roas > TARGET_ROAS) { + // Increase bid by 10% + keyword.setMaxCpc(Math.floor(currentBid * 1.1)); + } else if (roas < TARGET_ROAS * 0.7) { + // Decrease bid by 5% + keyword.setMaxCpc(Math.floor(currentBid * 0.95)); + } + } +} +``` + +### Example 3: Export Campaign Performance to Sheets + +```javascript +function exportCampaignPerformance() { + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .orderBy('campaign.metrics.cost DESC') + .get(); + + const report = [['Campaign', 'Clicks', 'Cost', 'Conversions', 'CPC', 'ROAS']]; + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + const stats = campaign.getStatsFor('LAST_30_DAYS'); + + report.push([ + campaign.getName(), + stats.getClicks(), + stats.getCost() / 1000000, // Convert from micros + stats.getConversions(), + stats.getAverageCpc() / 1000000, + stats.getReturnOnAdSpend() + ]); + } + + // Write to Google Sheets + const ss = SpreadsheetApp.openById('YOUR_SHEET_ID'); + const sheet = ss.getSheetByName('Campaign Report') || ss.insertSheet('Campaign Report'); + sheet.clear(); + sheet.getRange(1, 1, report.length, report[0].length).setValues(report); +} +``` + +## Working with References + +For comprehensive API documentation, code patterns, and detailed examples, see: + +- **references/ads-api-reference.md** - Complete AdsApp API reference including all selectors, methods, conditions, statistics, and enterprise patterns + +The reference file contains: +- Complete API hierarchy and object model +- All selector patterns and conditions +- Statistics methods and date ranges +- Campaign, ad group, keyword, and ad operations +- Bidding strategy implementation details +- Performance reporting patterns +- Budget management techniques +- Advanced targeting options +- Error handling patterns +- Performance optimization strategies +- Quotas and limits reference + +## Best Practices + +### 1. Use Batch Operations + +Avoid individual API calls in loops. Instead, collect entities and perform batch operations: + +```javascript +// ✅ Good - Batch collection +const keywords = AdsApp.keywords() + .withCondition('keyword.quality_info.quality_score < 5') + .get(); + +const toUpdate = []; +while (keywords.hasNext()) { + toUpdate.push(keywords.next()); +} + +toUpdate.forEach(keyword => keyword.setMaxCpc(50000)); +``` + +### 2. Filter with Conditions + +Use `.withCondition()` to filter at the API level rather than in JavaScript: + +```javascript +// ✅ Good - API-level filtering +const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .withCondition('campaign.budget_information.budget_amount > 100000000') + .get(); +``` + +### 3. Handle Errors Gracefully + +Always wrap operations in try-catch blocks and log errors: + +```javascript +function safeOperation() { + try { + // Operation code + } catch (error) { + Logger.log('Error: ' + error.message); + Logger.log('Stack: ' + error.stack); + + // Optionally email alert + MailApp.sendEmail( + Session.getEffectiveUser().getEmail(), + 'Ads Script Error', + error.message + ); + } +} +``` + +### 4. Respect Execution Limits + +Scripts have a 30-minute execution timeout. For large accounts: +- Limit result sets with `.withLimit()` +- Process in batches +- Use early termination when needed + +### 5. Convert Micros Correctly + +Google Ads API uses micros (1/1,000,000) for currency values: + +```javascript +const costMicros = stats.getCost(); +const costCurrency = costMicros / 1000000; // Convert to dollars/local currency + +const bidCurrency = 5.00; // $5.00 +const bidMicros = bidCurrency * 1000000; // 5000000 micros +``` + +### 6. Log Operations for Auditing + +Maintain audit trails by logging changes to Google Sheets: + +```javascript +function logChange(operation, entity, details) { + const ss = SpreadsheetApp.openById('LOG_SHEET_ID'); + const sheet = ss.getSheetByName('Audit Log'); + sheet.appendRow([ + new Date(), + operation, + entity, + JSON.stringify(details) + ]); +} +``` + +## Integration with Other Skills + +- **google-apps-script** - Use for Google Sheets reporting, Gmail notifications, Drive file management, and trigger setup +- **ga4-measurement-protocol** - Combine with GA4 for tracking script-triggered events +- **gtm-api** - Coordinate with GTM configurations for holistic tracking + +## Common Patterns + +### Pattern: Conditional Bid Adjustment + +```javascript +function adjustBidsBasedOnDayOfWeek() { + const today = new Date().getDay(); // 0 = Sunday, 6 = Saturday + const isWeekend = today === 0 || today === 6; + + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .get(); + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + const budget = campaign.getBudget().getAmount(); + + if (isWeekend) { + campaign.getBudget().setAmount(budget * 1.2); // +20% on weekends + } + } +} +``` + +### Pattern: Quality Score Monitoring + +```javascript +function monitorQualityScores() { + const threshold = 5; + + const lowQualityKeywords = AdsApp.keywords() + .withCondition(`keyword.quality_info.quality_score < ${threshold}`) + .withCondition('keyword.status = ENABLED') + .orderBy('keyword.metrics.cost DESC') // Most expensive first + .withLimit(100) + .get(); + + const alerts = []; + while (lowQualityKeywords.hasNext()) { + const keyword = lowQualityKeywords.next(); + alerts.push({ + keyword: keyword.getText(), + qualityScore: keyword.getQualityScore(), + cost: keyword.getStatsFor('LAST_7_DAYS').getCost() / 1000000 + }); + } + + if (alerts.length > 0) { + // Send email or log to sheet + Logger.log(`${alerts.length} keywords with QS < ${threshold}`); + } +} +``` + +## Validation & Testing + +Use the validation scripts in `scripts/` for pre-deployment checks: + +- **scripts/validators.py** - Validate campaign data, bid values, budget amounts before applying changes + +## Troubleshooting + +**Common Issues:** + +1. **Execution timeout** - Reduce scope with `.withLimit()` or process in batches +2. **Quota exceeded** - Reduce API call frequency, use cached data +3. **Type errors** - Remember micros conversion for currency values +4. **Null values** - Always check for null before accessing properties + +**Debug with Logger:** + +```javascript +Logger.log('Debug info: ' + JSON.stringify(data)); +// View logs: View > Logs in script editor +``` + +--- + +This skill provides production-ready patterns for Google Ads automation. Consult the comprehensive API reference for detailed method signatures and advanced use cases. diff --git a/skills/google-ads-scripts/assets/bid-manager-template.js b/skills/google-ads-scripts/assets/bid-manager-template.js new file mode 100644 index 0000000..8eab3cf --- /dev/null +++ b/skills/google-ads-scripts/assets/bid-manager-template.js @@ -0,0 +1,391 @@ +/** + * Bid Manager Template + * + * Template for automated keyword bid management based on performance metrics. + * Implements intelligent bidding strategies with safety controls. + * + * Features: + * - Bid optimization based on ROAS/CPA targets + * - Quality score-based bid adjustments + * - Conversion-driven bid modifications + * - Detailed audit logging + * - Safety limits and dry-run mode + */ + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const CONFIG = { + // Bidding strategy + STRATEGY: 'ROAS', // 'ROAS' or 'CPA' + TARGET_ROAS: 3.0, // Target Return on Ad Spend (300%) + TARGET_CPA: 50.00, // Target Cost Per Acquisition (in currency) + + // Bid adjustment factors + HIGH_PERFORMER_INCREASE: 1.10, // +10% for high performers + LOW_PERFORMER_DECREASE: 0.95, // -5% for low performers + QUALITY_SCORE_FACTOR: 0.05, // ±5% per quality score point deviation + + // Performance thresholds + MIN_CONVERSIONS: 3, // Minimum conversions to evaluate + MIN_CLICKS: 50, // Minimum clicks to evaluate + HIGH_ROAS_THRESHOLD: 3.5, // ROAS above this = high performer + LOW_ROAS_THRESHOLD: 2.0, // ROAS below this = low performer + TARGET_QUALITY_SCORE: 7, // Target quality score + + // Bid limits + MIN_BID_MICROS: 50000, // $0.05 minimum bid + MAX_BID_MICROS: 10000000, // $10.00 maximum bid + MAX_BID_CHANGE_PERCENT: 25, // Maximum 25% bid change per run + + // Date range + DATE_RANGE: 'LAST_30_DAYS', + + // Reporting + SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID', // Replace with your Sheet ID + LOG_SHEET_NAME: 'Bid Manager Log', + NOTIFICATION_EMAIL: Session.getEffectiveUser().getEmail(), + + // Safety + DRY_RUN: false, // Set to true to preview bid changes + MAX_KEYWORDS_TO_PROCESS: 5000 // Prevent accidental mass changes +}; + +// ============================================================================ +// MAIN FUNCTION +// ============================================================================ + +function main() { + try { + Logger.log('Starting Bid Manager...'); + Logger.log('Date: ' + new Date()); + Logger.log('Strategy: ' + CONFIG.STRATEGY); + Logger.log('Mode: ' + (CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE')); + + // Initialize reporting + const sheet = initializeReportingSheet(); + + // Get keywords to optimize + const keywords = getKeywordsToOptimize(); + Logger.log(`Found ${keywords.totalNumEntities()} keywords to evaluate`); + + if (keywords.totalNumEntities() > CONFIG.MAX_KEYWORDS_TO_PROCESS) { + throw new Error(`Too many keywords (${keywords.totalNumEntities()}). Increase MAX_KEYWORDS_TO_PROCESS if intended.`); + } + + // Process keywords + const results = processKeywords(keywords); + + // Generate summary + logResults(sheet, results); + sendEmailNotification(results); + + Logger.log('Bid Manager completed successfully'); + + } catch (error) { + handleError(error); + } +} + +// ============================================================================ +// KEYWORD PROCESSING +// ============================================================================ + +function getKeywordsToOptimize() { + return AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition(`keyword.metrics.clicks >= ${CONFIG.MIN_CLICKS}`) + .orderBy('keyword.metrics.cost DESC') + .get(); +} + +function processKeywords(keywords) { + const results = { + evaluated: 0, + increased: [], + decreased: [], + paused: [], + noChange: [], + errors: [] + }; + + while (keywords.hasNext()) { + const keyword = keywords.next(); + results.evaluated++; + + try { + const decision = evaluateKeyword(keyword); + + if (decision.action === 'PAUSE') { + if (!CONFIG.DRY_RUN) { + keyword.pause(); + } + results.paused.push({ + text: keyword.getText(), + campaign: keyword.getCampaign().getName(), + currentBid: keyword.getMaxCpc() / 1000000, + reason: decision.reason + }); + } else if (decision.newBid && decision.newBid !== keyword.getMaxCpc()) { + const oldBid = keyword.getMaxCpc(); + + if (!CONFIG.DRY_RUN) { + keyword.setMaxCpc(decision.newBid); + } + + const changeRecord = { + text: keyword.getText(), + campaign: keyword.getCampaign().getName(), + oldBid: oldBid / 1000000, + newBid: decision.newBid / 1000000, + change: ((decision.newBid - oldBid) / oldBid * 100).toFixed(1) + '%', + reason: decision.reason + }; + + if (decision.newBid > oldBid) { + results.increased.push(changeRecord); + } else { + results.decreased.push(changeRecord); + } + } else { + results.noChange.push(keyword.getText()); + } + + } catch (error) { + results.errors.push({ + keyword: keyword.getText(), + error: error.message + }); + Logger.log(`Error processing keyword ${keyword.getText()}: ${error.message}`); + } + } + + return results; +} + +function evaluateKeyword(keyword) { + const stats = keyword.getStatsFor(CONFIG.DATE_RANGE); + const conversions = stats.getConversions(); + const clicks = stats.getClicks(); + const cost = stats.getCost(); + const conversionValue = stats.getConversionValue(); + const currentBid = keyword.getMaxCpc(); + const qualityScore = keyword.getQualityScore(); + + // Insufficient data + if (conversions < CONFIG.MIN_CONVERSIONS) { + return { action: 'NONE', reason: `Insufficient conversions (${conversions})` }; + } + + // Calculate performance metrics + let performanceMultiplier = 1.0; + let reason = []; + + if (CONFIG.STRATEGY === 'ROAS') { + const roas = cost > 0 ? conversionValue / (cost / 1000000) : 0; + + if (roas >= CONFIG.HIGH_ROAS_THRESHOLD) { + performanceMultiplier *= CONFIG.HIGH_PERFORMER_INCREASE; + reason.push(`High ROAS (${roas.toFixed(2)})`); + } else if (roas < CONFIG.LOW_ROAS_THRESHOLD) { + performanceMultiplier *= CONFIG.LOW_PERFORMER_DECREASE; + reason.push(`Low ROAS (${roas.toFixed(2)})`); + } + } else if (CONFIG.STRATEGY === 'CPA') { + const cpa = conversions > 0 ? (cost / 1000000) / conversions : Infinity; + + if (cpa <= CONFIG.TARGET_CPA * 0.8) { + performanceMultiplier *= CONFIG.HIGH_PERFORMER_INCREASE; + reason.push(`Low CPA ($${cpa.toFixed(2)})`); + } else if (cpa > CONFIG.TARGET_CPA * 1.2) { + performanceMultiplier *= CONFIG.LOW_PERFORMER_DECREASE; + reason.push(`High CPA ($${cpa.toFixed(2)})`); + } + } + + // Quality score adjustment + if (qualityScore !== null) { + const qsDeviation = qualityScore - CONFIG.TARGET_QUALITY_SCORE; + const qsMultiplier = 1 + (qsDeviation * CONFIG.QUALITY_SCORE_FACTOR); + performanceMultiplier *= qsMultiplier; + reason.push(`QS ${qualityScore}`); + + // Pause very low quality keywords + if (qualityScore < 3) { + return { + action: 'PAUSE', + reason: `Very low quality score (${qualityScore})` + }; + } + } + + // Calculate new bid + const proposedBid = Math.floor(currentBid * performanceMultiplier); + + // Apply safety limits + const safeBid = applySafetyLimits(currentBid, proposedBid); + + // No change needed + if (safeBid === currentBid) { + return { action: 'NONE', reason: 'No change needed' }; + } + + return { + action: 'UPDATE_BID', + newBid: safeBid, + reason: reason.join(', ') + }; +} + +// ============================================================================ +// BID CALCULATION & SAFETY +// ============================================================================ + +function applySafetyLimits(currentBid, proposedBid) { + // Enforce minimum bid + if (proposedBid < CONFIG.MIN_BID_MICROS) { + Logger.log(`Proposed bid ${proposedBid} below minimum, using ${CONFIG.MIN_BID_MICROS}`); + return CONFIG.MIN_BID_MICROS; + } + + // Enforce maximum bid + if (proposedBid > CONFIG.MAX_BID_MICROS) { + Logger.log(`Proposed bid ${proposedBid} above maximum, using ${CONFIG.MAX_BID_MICROS}`); + return CONFIG.MAX_BID_MICROS; + } + + // Enforce maximum change percentage + const changePercent = Math.abs((proposedBid - currentBid) / currentBid * 100); + if (changePercent > CONFIG.MAX_BID_CHANGE_PERCENT) { + const maxChange = currentBid * (CONFIG.MAX_BID_CHANGE_PERCENT / 100); + const cappedBid = proposedBid > currentBid + ? Math.floor(currentBid + maxChange) + : Math.floor(currentBid - maxChange); + Logger.log(`Change ${changePercent.toFixed(1)}% exceeds max ${CONFIG.MAX_BID_CHANGE_PERCENT}%, capping to ${cappedBid}`); + return cappedBid; + } + + return proposedBid; +} + +// ============================================================================ +// REPORTING +// ============================================================================ + +function initializeReportingSheet() { + const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID); + let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME); + + if (!sheet) { + sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME); + sheet.appendRow(['Timestamp', 'Mode', 'Action', 'Keyword', 'Campaign', 'Old Bid', 'New Bid', 'Change', 'Reason']); + sheet.getRange('A1:I1').setFontWeight('bold'); + } + + return sheet; +} + +function logResults(sheet, results) { + const timestamp = new Date(); + const mode = CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'; + + // Log bid increases + results.increased.forEach(item => { + sheet.appendRow([ + timestamp, mode, 'BID_INCREASED', item.text, item.campaign, + item.oldBid, item.newBid, item.change, item.reason + ]); + }); + + // Log bid decreases + results.decreased.forEach(item => { + sheet.appendRow([ + timestamp, mode, 'BID_DECREASED', item.text, item.campaign, + item.oldBid, item.newBid, item.change, item.reason + ]); + }); + + // Log paused keywords + results.paused.forEach(item => { + sheet.appendRow([ + timestamp, mode, 'PAUSED', item.text, item.campaign, + item.currentBid, 0, '-100%', item.reason + ]); + }); + + // Log errors + results.errors.forEach(item => { + sheet.appendRow([ + timestamp, mode, 'ERROR', item.keyword, '', '', '', '', item.error + ]); + }); + + Logger.log(`Logged ${results.increased.length + results.decreased.length + results.paused.length + results.errors.length} actions to sheet`); +} + +function sendEmailNotification(results) { + if (results.increased.length === 0 && results.decreased.length === 0 && results.paused.length === 0) { + Logger.log('No bid changes, skipping email notification'); + return; + } + + const subject = `Bid Manager Report - ${CONFIG.STRATEGY} - ${CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'} - ${new Date().toDateString()}`; + + const body = ` +Bid Manager Results +=================== + +Mode: ${CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'} +Strategy: ${CONFIG.STRATEGY} +${CONFIG.STRATEGY === 'ROAS' ? `Target ROAS: ${CONFIG.TARGET_ROAS}` : `Target CPA: $${CONFIG.TARGET_CPA}`} +Date: ${new Date()} +Keywords Evaluated: ${results.evaluated} + +Actions Taken: +-------------- +Bids Increased: ${results.increased.length} +Bids Decreased: ${results.decreased.length} +Keywords Paused: ${results.paused.length} +No Change: ${results.noChange.length} +Errors: ${results.errors.length} + +Top Bid Increases: +${results.increased.slice(0, 10).map(item => + `- ${item.text} (${item.campaign}): $${item.oldBid.toFixed(2)} → $${item.newBid.toFixed(2)} (${item.change}) - ${item.reason}` +).join('\n')} + +Top Bid Decreases: +${results.decreased.slice(0, 10).map(item => + `- ${item.text} (${item.campaign}): $${item.oldBid.toFixed(2)} → $${item.newBid.toFixed(2)} (${item.change}) - ${item.reason}` +).join('\n')} + +Paused Keywords: +${results.paused.map(item => `- ${item.text} (${item.campaign}): ${item.reason}`).join('\n')} + +${results.errors.length > 0 ? `\nErrors:\n${results.errors.map(item => `- ${item.keyword}: ${item.error}`).join('\n')}` : ''} + +--- +Generated by Google Ads Script + `; + + MailApp.sendEmail(CONFIG.NOTIFICATION_EMAIL, subject, body); + Logger.log('Email notification sent to ' + CONFIG.NOTIFICATION_EMAIL); +} + +// ============================================================================ +// ERROR HANDLING +// ============================================================================ + +function handleError(error) { + Logger.log('FATAL ERROR: ' + error.message); + Logger.log('Stack trace: ' + error.stack); + + MailApp.sendEmail( + CONFIG.NOTIFICATION_EMAIL, + 'Bid Manager Error', + `An error occurred in the Bid Manager script:\n\n${error.message}\n\nStack trace:\n${error.stack}` + ); + + throw error; +} diff --git a/skills/google-ads-scripts/assets/campaign-optimizer-template.js b/skills/google-ads-scripts/assets/campaign-optimizer-template.js new file mode 100644 index 0000000..0fa6d69 --- /dev/null +++ b/skills/google-ads-scripts/assets/campaign-optimizer-template.js @@ -0,0 +1,303 @@ +/** + * Campaign Optimizer Template + * + * Template for optimizing Google Ads campaigns based on performance metrics. + * Customize thresholds and logic to fit your business needs. + * + * Features: + * - Pause underperforming campaigns + * - Adjust budgets based on ROAS + * - Email notifications for actions taken + * - Detailed logging to Google Sheets + */ + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const CONFIG = { + // Performance thresholds + MIN_CONVERSIONS: 5, // Minimum conversions to evaluate + TARGET_ROAS: 3.0, // Target Return on Ad Spend (300%) + LOW_ROAS_THRESHOLD: 2.0, // ROAS below this triggers budget reduction + MIN_QUALITY_SCORE: 5, // Minimum acceptable quality score + + // Budget adjustments + BUDGET_INCREASE_FACTOR: 1.10, // +10% for high performers + BUDGET_DECREASE_FACTOR: 0.90, // -10% for low performers + + // Date range for analysis + DATE_RANGE: 'LAST_30_DAYS', + + // Reporting + SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID', // Replace with your Sheet ID + LOG_SHEET_NAME: 'Campaign Optimizer Log', + NOTIFICATION_EMAIL: Session.getEffectiveUser().getEmail(), + + // Safety limits + DRY_RUN: false, // Set to true to preview changes without applying + MAX_CAMPAIGNS_TO_PROCESS: 1000 // Prevent accidental large-scale changes +}; + +// ============================================================================ +// MAIN FUNCTION +// ============================================================================ + +function main() { + try { + Logger.log('Starting Campaign Optimizer...'); + Logger.log('Date: ' + new Date()); + Logger.log('Mode: ' + (CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE')); + + // Initialize reporting + const sheet = initializeReportingSheet(); + + // Get campaigns to optimize + const campaigns = getCampaignsToOptimize(); + Logger.log(`Found ${campaigns.totalNumEntities()} campaigns to evaluate`); + + if (campaigns.totalNumEntities() > CONFIG.MAX_CAMPAIGNS_TO_PROCESS) { + throw new Error(`Too many campaigns (${campaigns.totalNumEntities()}). Increase MAX_CAMPAIGNS_TO_PROCESS if intended.`); + } + + // Process campaigns + const results = processCampaigns(campaigns); + + // Generate summary + logResults(sheet, results); + sendEmailNotification(results); + + Logger.log('Campaign Optimizer completed successfully'); + + } catch (error) { + handleError(error); + } +} + +// ============================================================================ +// CAMPAIGN PROCESSING +// ============================================================================ + +function getCampaignsToOptimize() { + return AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .orderBy('campaign.metrics.cost DESC') + .get(); +} + +function processCampaigns(campaigns) { + const results = { + evaluated: 0, + paused: [], + budgetIncreased: [], + budgetDecreased: [], + noAction: [], + errors: [] + }; + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + results.evaluated++; + + try { + const decision = evaluateCampaign(campaign); + + if (decision.action === 'PAUSE') { + if (!CONFIG.DRY_RUN) { + campaign.pause(); + } + results.paused.push({ + name: campaign.getName(), + reason: decision.reason + }); + } else if (decision.action === 'INCREASE_BUDGET') { + if (!CONFIG.DRY_RUN) { + increaseBudget(campaign, CONFIG.BUDGET_INCREASE_FACTOR); + } + results.budgetIncreased.push({ + name: campaign.getName(), + reason: decision.reason + }); + } else if (decision.action === 'DECREASE_BUDGET') { + if (!CONFIG.DRY_RUN) { + decreaseBudget(campaign, CONFIG.BUDGET_DECREASE_FACTOR); + } + results.budgetDecreased.push({ + name: campaign.getName(), + reason: decision.reason + }); + } else { + results.noAction.push(campaign.getName()); + } + + } catch (error) { + results.errors.push({ + campaign: campaign.getName(), + error: error.message + }); + Logger.log(`Error processing campaign ${campaign.getName()}: ${error.message}`); + } + } + + return results; +} + +function evaluateCampaign(campaign) { + const stats = campaign.getStatsFor(CONFIG.DATE_RANGE); + const conversions = stats.getConversions(); + const roas = stats.getReturnOnAdSpend(); + const cost = stats.getCost() / 1000000; + + // Insufficient data + if (conversions < CONFIG.MIN_CONVERSIONS) { + return { action: 'NONE', reason: `Insufficient conversions (${conversions})` }; + } + + // High performer - increase budget + if (roas >= CONFIG.TARGET_ROAS) { + return { + action: 'INCREASE_BUDGET', + reason: `High ROAS (${roas.toFixed(2)}), Cost: $${cost.toFixed(2)}` + }; + } + + // Low performer - pause + if (roas < CONFIG.LOW_ROAS_THRESHOLD) { + return { + action: 'PAUSE', + reason: `Low ROAS (${roas.toFixed(2)}) below threshold (${CONFIG.LOW_ROAS_THRESHOLD})` + }; + } + + // Medium performer - decrease budget + if (roas < CONFIG.TARGET_ROAS) { + return { + action: 'DECREASE_BUDGET', + reason: `ROAS (${roas.toFixed(2)}) below target (${CONFIG.TARGET_ROAS})` + }; + } + + return { action: 'NONE', reason: 'Performance within acceptable range' }; +} + +// ============================================================================ +// BUDGET MANAGEMENT +// ============================================================================ + +function increaseBudget(campaign, factor) { + const currentBudget = campaign.getBudget().getAmount(); + const newBudget = Math.floor(currentBudget * factor); + campaign.getBudget().setAmount(newBudget); + Logger.log(`Increased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`); +} + +function decreaseBudget(campaign, factor) { + const currentBudget = campaign.getBudget().getAmount(); + const newBudget = Math.floor(currentBudget * factor); + campaign.getBudget().setAmount(newBudget); + Logger.log(`Decreased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`); +} + +// ============================================================================ +// REPORTING +// ============================================================================ + +function initializeReportingSheet() { + const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID); + let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME); + + if (!sheet) { + sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME); + sheet.appendRow(['Timestamp', 'Mode', 'Action', 'Campaign', 'Reason']); + sheet.getRange('A1:E1').setFontWeight('bold'); + } + + return sheet; +} + +function logResults(sheet, results) { + const timestamp = new Date(); + const mode = CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'; + + // Log paused campaigns + results.paused.forEach(item => { + sheet.appendRow([timestamp, mode, 'PAUSED', item.name, item.reason]); + }); + + // Log budget increases + results.budgetIncreased.forEach(item => { + sheet.appendRow([timestamp, mode, 'BUDGET_INCREASED', item.name, item.reason]); + }); + + // Log budget decreases + results.budgetDecreased.forEach(item => { + sheet.appendRow([timestamp, mode, 'BUDGET_DECREASED', item.name, item.reason]); + }); + + // Log errors + results.errors.forEach(item => { + sheet.appendRow([timestamp, mode, 'ERROR', item.campaign, item.error]); + }); + + Logger.log(`Logged ${results.paused.length + results.budgetIncreased.length + results.budgetDecreased.length + results.errors.length} actions to sheet`); +} + +function sendEmailNotification(results) { + if (results.paused.length === 0 && results.budgetIncreased.length === 0 && results.budgetDecreased.length === 0) { + Logger.log('No actions taken, skipping email notification'); + return; + } + + const subject = `Campaign Optimizer Report - ${CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'} - ${new Date().toDateString()}`; + + const body = ` +Campaign Optimizer Results +========================== + +Mode: ${CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'} +Date: ${new Date()} +Campaigns Evaluated: ${results.evaluated} + +Actions Taken: +-------------- +Paused: ${results.paused.length} +Budget Increased: ${results.budgetIncreased.length} +Budget Decreased: ${results.budgetDecreased.length} +No Action: ${results.noAction.length} +Errors: ${results.errors.length} + +Paused Campaigns: +${results.paused.map(item => `- ${item.name}: ${item.reason}`).join('\n')} + +Budget Increased: +${results.budgetIncreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')} + +Budget Decreased: +${results.budgetDecreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')} + +${results.errors.length > 0 ? `\nErrors:\n${results.errors.map(item => `- ${item.campaign}: ${item.error}`).join('\n')}` : ''} + +--- +Generated by Google Ads Script + `; + + MailApp.sendEmail(CONFIG.NOTIFICATION_EMAIL, subject, body); + Logger.log('Email notification sent to ' + CONFIG.NOTIFICATION_EMAIL); +} + +// ============================================================================ +// ERROR HANDLING +// ============================================================================ + +function handleError(error) { + Logger.log('FATAL ERROR: ' + error.message); + Logger.log('Stack trace: ' + error.stack); + + MailApp.sendEmail( + CONFIG.NOTIFICATION_EMAIL, + 'Campaign Optimizer Error', + `An error occurred in the Campaign Optimizer script:\n\n${error.message}\n\nStack trace:\n${error.stack}` + ); + + throw error; +} diff --git a/skills/google-ads-scripts/references/ads-api-reference.md b/skills/google-ads-scripts/references/ads-api-reference.md new file mode 100644 index 0000000..e8f6b7f --- /dev/null +++ b/skills/google-ads-scripts/references/ads-api-reference.md @@ -0,0 +1,1379 @@ +--- +name: "Google Ads Script - Mission Critical Reference" +description: "Enterprise-grade, offline-accessible comprehensive guide for Google Ads Script development. Covers AdsApp API, campaign management, ad groups, keywords, bidding, performance reporting, targeting options, advanced operations, error handling, and optimization patterns. Designed as the sole authoritative source for mission-critical Google Ads automation when network infrastructure is unavailable." +version: "2025-11-ENTERPRISE" +last_updated: "November 2025" +security_classification: "REFERENCE" +--- + +# GOOGLE ADS SCRIPT ENTERPRISE REFERENCE +## Mission-Critical Documentation + +**Document Version:** 2025-11-ENTERPRISE +**Last Updated:** November 10, 2025 +**API Version:** v22 (Current Stable) +**Offline Accessibility:** GUARANTEED +**Verification Status:** Official Google Ads Documentation Cross-Referenced + +--- + +## TABLE OF CONTENTS + +1. [Enterprise Skill Overview](#enterprise-skill-overview) +2. [Core Architecture](#core-architecture) +3. [AdsApp API Fundamentals](#adsapp-api-fundamentals) +4. [Campaign Operations](#campaign-operations) +5. [Ad Group Management](#ad-group-management) +6. [Keywords & Targeting](#keywords--targeting) +7. [Ads Management](#ads-management) +8. [Bidding Strategy](#bidding-strategy) +9. [Performance Reporting](#performance-reporting) +10. [Budget Management](#budget-management) +11. [Advanced Targeting](#advanced-targeting) +12. [Automated Rules & Alerts](#automated-rules--alerts) +13. [Error Handling & Debugging](#error-handling--debugging) +14. [Performance Optimization](#performance-optimization) +15. [Best Practices](#best-practices) + +--- + +## ENTERPRISE SKILL OVERVIEW + +### Activation Criteria + +This skill activates when developers need: +- Campaign management automation +- Keyword bid optimization +- Performance-based campaign adjustments +- Ad scheduling automation +- Budget allocation and management +- Quality score monitoring +- Conversion tracking setup +- Report generation and analysis +- Pause/enable campaigns based on criteria +- Bulk operations and batch updates + +### Document Guarantees + +✓ NO external links required +✓ ALL AdsApp operations documented +✓ COMPLETE API patterns included +✓ ALL error scenarios covered +✓ Production-ready code examples +✓ Optimization strategies included +✓ Performance metrics reference +✓ Advanced patterns explained + +--- + +## CORE ARCHITECTURE + +### Google Ads Script Runtime Model + +**Execution Model:** +- Runs in Google Ads editor +- JavaScript ES6 compatible +- Access to account-level data +- Time-based and event-based triggers + +**Rate Limits and Quotas:** + +| Limit | Value | Impact | +|-------|-------|--------| +| **Script execution time** | 30 minutes | Per run timeout | +| **API quota** | Depends on account | Shared with other tools | +| **Read operations** | Unlimited (rate limited) | Account data access | +| **Write operations** | Limited | Use batch operations | +| **Daily budget spend** | Account budget | Cannot exceed daily limit | +| **Monthly script runs** | Depends on triggers | Time-based limits | + +**AdsApp Object Hierarchy:** +``` +AdsApp (Root) +├── campaigns() # Campaign selector +├── adGroups() # Ad group selector +├── keywords() # Keyword selector +├── ads() # Ad selector +├── productGroups() # Shopping product groups +├── shoppingCampaigns() # Shopping campaigns +├── campaignTargeting() # Campaign-level targeting +├── currentAccount() # Current account info +├── report() # GAQL reporting +└── mutate() # Batch mutations +``` + +--- + +## ADSAPP API FUNDAMENTALS + +### Core Concepts + +**Selector Pattern:** +```javascript +// All campaigns +const campaigns = AdsApp.campaigns().get(); + +// With conditions +const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .withCondition('campaign.name CONTAINS "Sale"') + .get(); + +// Limit results +const campaigns = AdsApp.campaigns() + .withLimit(100) + .get(); + +// Order by +const campaigns = AdsApp.campaigns() + .orderBy('campaign.metrics.clicks DESC') + .get(); +``` + +**Iterator Pattern:** +```javascript +const campaigns = AdsApp.campaigns().get(); + +while (campaigns.hasNext()) { + const campaign = campaigns.next(); + // Process campaign +} + +// Or convert to array +const campaignsArray = []; +while (campaigns.hasNext()) { + campaignsArray.push(campaigns.next()); +} +``` + +**Statistics:** +```javascript +const campaigns = AdsApp.campaigns().get(); +Logger.log('Total campaigns: ' + campaigns.totalNumEntities()); +``` + +### Date Range Strings + +```javascript +// Predefined ranges +getStatsFor('TODAY') +getStatsFor('YESTERDAY') +getStatsFor('LAST_7_DAYS') +getStatsFor('LAST_14_DAYS') +getStatsFor('LAST_30_DAYS') +getStatsFor('LAST_90_DAYS') +getStatsFor('THIS_MONTH') +getStatsFor('LAST_MONTH') + +// Custom range +const stats = campaign.getStatsFor('20250101', '20251110'); +``` + +--- + +## CAMPAIGN OPERATIONS + +### Getting Campaigns + +**Basic retrieval:** +```javascript +// Get all campaigns +const campaigns = AdsApp.campaigns().get(); + +// Get single campaign by name +const campaignIterator = AdsApp.campaigns() + .withCondition('campaign.name = "My Campaign"') + .get(); + +if (campaignIterator.hasNext()) { + const campaign = campaignIterator.next(); +} +``` + +**Filter campaigns:** +```javascript +// Enabled campaigns only +const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .get(); + +// By budget +const campaigns = AdsApp.campaigns() + .withCondition('campaign.budget_information.budget_amount >= 100000000') + .get(); + +// By type (SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX) +const campaigns = AdsApp.campaigns() + .withCondition('campaign.type = SEARCH') + .get(); + +// Recently modified +const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .withCondition('campaign.name CONTAINS "Q4"') + .get(); +``` + +### Campaign Properties + +**Get campaign info:** +```javascript +const campaign = campaigns.next(); + +const id = campaign.getId(); // Numeric ID +const name = campaign.getName(); // Campaign name +const status = campaign.getStatus(); // ENABLED, PAUSED, REMOVED +const budget = campaign.getBudget().getAmount(); // Daily budget in micros +const campaignType = campaign.getType(); // SEARCH, DISPLAY, etc. +const startDate = campaign.getStartDate(); // Start date (YYYY-MM-DD) +const endDate = campaign.getEndDate(); // End date +const adServingOptimizationStatus = campaign.getAdServingOptimizationStatus(); +``` + +**Statistics:** +```javascript +const stats = campaign.getStatsFor('LAST_30_DAYS'); +const cost = stats.getCost(); // In micros (divide by 1,000,000) +const clicks = stats.getClicks(); +const impressions = stats.getImpressions(); +const conversions = stats.getConversions(); +const ctr = stats.getClickThroughRate(); // As decimal (0-1) +const cpc = stats.getAverageCpc(); // Average cost per click +const cpa = stats.getAveragePageviews(); // NOTE: check specific metric +``` + +### Campaign Management + +**Create campaign:** +```javascript +// Build campaign +const campaign = AdsApp.campaigns().newCampaignBuilder() + .withName('New Campaign') + .withStatus('PAUSED') + .withBudget(5000000) // 5000 in local currency, in micros + .withType('SEARCH') + .build() + .getResult(); +``` + +**Modify campaign:** +```javascript +campaign.setName('Updated Name'); +campaign.setStatus('ENABLED'); // ENABLED, PAUSED, REMOVED + +// Budget (in micros) +campaign.getBudget().setAmount(10000000); // 10000 + +// Start/end dates +campaign.setStartDate('2025-12-01'); +campaign.setEndDate('2025-12-31'); +``` + +**Pause/enable:** +```javascript +campaign.pause(); +campaign.enable(); +campaign.remove(); // Archive campaign +``` + +**Campaign labels:** +```javascript +campaign.applyLabel('MyLabel'); +campaign.removeLabel('MyLabel'); +const labels = campaign.labels(); // Get labels +``` + +--- + +## AD GROUP MANAGEMENT + +### Getting Ad Groups + +**Basic retrieval:** +```javascript +// All ad groups +const adGroups = AdsApp.adGroups().get(); + +// In specific campaign +const adGroups = campaign.adGroups().get(); + +// By name +const adGroupIterator = AdsApp.adGroups() + .withCondition('ad_group.name = "Ad Group Name"') + .get(); + +// By status +const adGroups = AdsApp.adGroups() + .withCondition('ad_group.status = ENABLED') + .get(); + +// By performance +const adGroups = AdsApp.adGroups() + .withCondition('ad_group.metrics.avg_cpc > 200000') // > 0.20 in local currency + .orderBy('ad_group.metrics.cost DESC') + .get(); +``` + +### Ad Group Properties + +**Get info:** +```javascript +const adGroup = adGroups.next(); + +const id = adGroup.getId(); +const name = adGroup.getName(); +const status = adGroup.getStatus(); +const campaign = adGroup.getCampaign(); +const cpiBid = adGroup.getCpcBid(); // Cost per click in micros +const stats = adGroup.getStatsFor('LAST_7_DAYS'); +``` + +**Statistics:** +```javascript +const stats = adGroup.getStatsFor('LAST_30_DAYS'); +const cost = stats.getCost(); +const clicks = stats.getClicks(); +const impressions = stats.getImpressions(); +const conversions = stats.getConversions(); +const conversionRate = stats.getConversionRate(); +const roas = stats.getReturnOnAdSpend(); +``` + +### Ad Group Operations + +**Create ad group:** +```javascript +const adGroup = campaign.newAdGroupBuilder() + .withName('New Ad Group') + .withStatus('PAUSED') + .withCpc(50000) // 0.50 in local currency, in micros + .build() + .getResult(); +``` + +**Modify ad group:** +```javascript +adGroup.setName('Updated Name'); +adGroup.setStatus('ENABLED'); // ENABLED, PAUSED, REMOVED +adGroup.bidding().setCpc(75000); // 0.75 +``` + +**Bidding:** +```javascript +// Get current bid +const bid = adGroup.getCpcBid(); + +// Set CPC bid +adGroup.bidding().setCpc(50000); + +// Set max CPC +adGroup.bidding().setMaxCpc(100000); +``` + +### Keywords in Ad Group + +**Get keywords:** +```javascript +// All keywords in ad group +const keywords = adGroup.keywords().get(); + +// Negative keywords +const negativeKeywords = adGroup.negativeKeywords().get(); +``` + +**Add keyword:** +```javascript +const keyword = adGroup.newKeywordBuilder() + .withText('blue shoes') + .withMatchType('BROAD') + .withFinalUrl('https://example.com/shoes') + .withMaxCpc(50000) + .build() + .getResult(); +``` + +**Remove keyword:** +```javascript +const keyword = adGroup.keywords().get().next(); +keyword.remove(); +``` + +--- + +## KEYWORDS & TARGETING + +### Keyword Operations + +**Get all keywords:** +```javascript +const keywords = AdsApp.keywords().get(); + +// With conditions +const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.match_type = EXACT') + .withCondition('keyword.text CONTAINS "shoe"') + .get(); + +// By quality score +const keywords = AdsApp.keywords() + .withCondition('keyword.quality_info.quality_score >= 7') + .get(); + +// High cost keywords +const keywords = AdsApp.keywords() + .withCondition('keyword.metrics.cost > 500000000') // > 500 in local currency + .orderBy('keyword.metrics.cost DESC') + .get(); +``` + +### Keyword Properties + +**Get keyword details:** +```javascript +const keyword = keywords.next(); + +const id = keyword.getId(); +const text = keyword.getText(); +const matchType = keyword.getMatchType(); // EXACT, PHRASE, BROAD +const maxCpc = keyword.getMaxCpc(); // In micros +const status = keyword.getStatus(); +const destinationUrl = keyword.getDestinationUrl(); +const approvalStatus = keyword.getApprovalStatus(); +const disapprovalReasons = keyword.getDisapprovalReasons(); // Array +``` + +**Quality score:** +```javascript +const qualityScore = keyword.getQualityScore(); // 1-10 or null +const creativeQualityScore = keyword.getCreativeQualityScore(); +const landingPageQualityScore = keyword.getLandingPageQualityScore(); +const postClickQualityScore = keyword.getPostClickQualityScore(); +``` + +**Statistics:** +```javascript +const stats = keyword.getStatsFor('LAST_30_DAYS'); +const cost = stats.getCost(); +const clicks = stats.getClicks(); +const impressions = stats.getImpressions(); +const conversions = stats.getConversions(); +const avgCpc = stats.getAverageCpc(); +const searchImpressionShare = stats.getSearchImpressionShare(); +``` + +### Keyword Management + +**Create keyword:** +```javascript +// In ad group +const keyword = adGroup.newKeywordBuilder() + .withText('blue running shoes') + .withMatchType('PHRASE') + .withMaxCpc(50000) + .build() + .getResult(); +``` + +**Modify keyword:** +```javascript +keyword.setMaxCpc(75000); +keyword.setDestinationUrl('https://example.com/products'); +keyword.pause(); +keyword.enable(); +``` + +**Match types:** +``` +BROAD // Matches query variations (default) +PHRASE // Matches phrase and close variations +EXACT // Matches exact phrase only +``` + +**Negative keywords:** +```javascript +// Create negative keyword +const negativeKeyword = adGroup.newNegativeKeywordBuilder() + .withText('cheap') + .withMatchType('BROAD') + .build() + .getResult(); + +// Campaign-level negative +const campaignNegKeyword = campaign.newNegativeKeywordBuilder() + .withText('wholesale') + .withMatchType('EXACT') + .build() + .getResult(); +``` + +### Targeting Options + +**Location targeting:** +```javascript +const campaign = AdsApp.campaigns().get().next(); + +// Add location +campaign.targeting() + .getLocationTarget() + .newLocationBuilder() + .withBidModifier(1.5) // 50% higher bid + .build(); + +// Get location targets +const locations = campaign.targeting().getLocationTarget().get(); +``` + +**Device targeting:** +```javascript +// Bid modifiers for devices +campaign.targeting() + .getDeviceTarget() + .setBidModifier('MOBILE', 1.2); // 20% higher for mobile + +campaign.targeting() + .getDeviceTarget() + .setBidModifier('TABLET', 0.8); // 20% lower for tablet + +campaign.targeting() + .getDeviceTarget() + .setBidModifier('DESKTOP', 1.0); // Standard bid +``` + +**Audience targeting:** +```javascript +// Add audience +adGroup.targeting() + .getAudienceTarget() + .newAudienceBuilder() + .withAudienceId('1234567890') + .withBidModifier(1.5) + .build(); +``` + +--- + +## ADS MANAGEMENT + +### Ad Types + +**Responsive Search Ads (RSA):** +```javascript +const ad = adGroup.newAd() + .responsiveSearchAdBuilder() + .addHeadline('Headline 1') + .addHeadline('Headline 2') + .addHeadline('Headline 3') + .addDescription('Description 1') + .addDescription('Description 2') + .addFinalUrl('https://example.com') + .build() + .getResult(); +``` + +**Expanded Text Ads (Legacy - still supported):** +```javascript +const ad = adGroup.newAd() + .expandedTextAdBuilder() + .setHeadlinePart1('Headline Part 1') + .setHeadlinePart2('Headline Part 2') + .setDescription1('Description 1') + .setDescription2('Description 2') + .setFinalUrl('https://example.com') + .build() + .getResult(); +``` + +### Getting Ads + +**All ads:** +```javascript +const ads = AdsApp.ads().get(); + +// By status +const ads = AdsApp.ads() + .withCondition('ad.status = ENABLED') + .get(); + +// By ad group +const ads = adGroup.ads().get(); + +// Pause low-performing ads +const ads = AdsApp.ads() + .withCondition('ad.metrics.avg_cpc > 500000') // > 0.50 + .orderBy('ad.metrics.avg_cpc DESC') + .get(); +``` + +### Ad Properties + +**Get ad details:** +```javascript +const ad = ads.next(); + +const id = ad.getId(); +const status = ad.getStatus(); +const type = ad.getType(); // RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc. +const creationTime = ad.getCreationTime(); +const updateTime = ad.getUpdateTime(); +const approvalStatus = ad.getApprovalStatus(); +``` + +**Ad statistics:** +```javascript +const stats = ad.getStatsFor('LAST_7_DAYS'); +const impressions = stats.getImpressions(); +const clicks = stats.getClicks(); +const conversions = stats.getConversions(); +const ctr = stats.getClickThroughRate(); +``` + +### Ad Management + +**Modify ad:** +```javascript +ad.pause(); +ad.enable(); +ad.remove(); +``` + +**Ad labels:** +```javascript +ad.applyLabel('TopAd'); +ad.removeLabel('TopAd'); +``` + +--- + +## BIDDING STRATEGY + +### Bid Types and Adjustments + +**CPC (Cost-Per-Click) Bidding:** +```javascript +// Set CPC at ad group level +adGroup.bidding().setCpc(50000); // 0.50 in local currency + +// Set CPC at keyword level +keyword.setMaxCpc(75000); // 0.75 + +// Get current bid +const bid = keyword.getMaxCpc(); +``` + +**Enhanced CPC:** +```javascript +// Enable enhanced CPC +campaign.bidding().setStrategy('ENHANCED_CPC'); + +// Disable +campaign.bidding().setStrategy('MANUAL_CPC'); +``` + +**Target CPA (Cost Per Acquisition):** +```javascript +campaign.bidding().setStrategy('TARGET_CPA'); +campaign.bidding().setTargetCpa(50000); // 50.00 in local currency +``` + +**Target ROAS (Return on Ad Spend):** +```javascript +campaign.bidding().setStrategy('MAXIMIZE_ROAS'); +campaign.bidding().setTargetRoas(3.0); // 300% ROAS +``` + +**Bid Adjustments:** +```javascript +// Time-of-day bid modifiers +adGroup.adSchedules() + .newAdScheduleBuilder() + .withDayOfWeek('MONDAY') + .withStartHour(9) + .withStartMinute(0) + .withEndHour(17) + .withEndMinute(0) + .withBidModifier(1.2) // 20% higher during 9-5 + .build(); + +// Device bid modifiers +adGroup.devices() + .get() + .next() + .setBidModifier(1.5); // Mobile 50% higher + +// Location bid modifiers +campaign.targeting() + .getLocationTarget() + .get() + .next() + .setBidModifier(1.2); // 20% higher in specific location +``` + +--- + +## PERFORMANCE REPORTING + +### Statistics Methods + +**Common metrics:** +```javascript +const stats = campaign.getStatsFor('LAST_30_DAYS'); + +// Click metrics +stats.getClicks(); +stats.getImpressions(); +stats.getClickThroughRate(); // 0.05 = 5% + +// Cost metrics +stats.getCost(); // In micros +stats.getAverageCpc(); +stats.getAverageCpm(); + +// Conversion metrics +stats.getConversions(); +stats.getConversionRate(); // 0.02 = 2% +stats.getConversionValue(); +stats.getCostPerConversion(); + +// ROI metrics +stats.getReturnOnAdSpend(); // 2.5 = 250% +stats.getAveragePageviews(); +stats.getAveragePosition(); +``` + +### Generating Reports + +**Campaign performance report:** +```javascript +function generateCampaignReport() { + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .orderBy('campaign.metrics.cost DESC') + .get(); + + const report = []; + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + const stats = campaign.getStatsFor('LAST_30_DAYS'); + + report.push({ + name: campaign.getName(), + cost: stats.getCost() / 1000000, // Convert from micros + clicks: stats.getClicks(), + impressions: stats.getImpressions(), + conversions: stats.getConversions(), + cpc: stats.getAverageCpc() / 1000000, + roas: stats.getReturnOnAdSpend() + }); + } + + return report; +} +``` + +**Keyword performance report:** +```javascript +function generateKeywordReport() { + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .orderBy('keyword.metrics.clicks DESC') + .get(); + + const report = []; + while (keywords.hasNext()) { + const keyword = keywords.next(); + const stats = keyword.getStatsFor('LAST_7_DAYS'); + + report.push({ + text: keyword.getText(), + matchType: keyword.getMatchType(), + impressions: stats.getImpressions(), + clicks: stats.getClicks(), + cost: stats.getCost() / 1000000, + conversions: stats.getConversions(), + qualityScore: keyword.getQualityScore() + }); + } + + return report; +} +``` + +### Exporting Reports to Sheets + +```javascript +function exportToSheets(data, sheetName) { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheet = ss.getSheetByName(sheetName); + + // Create sheet if doesn't exist + if (!sheet) { + sheet = ss.insertSheet(sheetName); + } else { + sheet.clear(); + } + + // Get headers + const headers = Object.keys(data[0]); + sheet.appendRow(headers); + + // Add data + data.forEach(row => { + sheet.appendRow(headers.map(header => row[header])); + }); +} + +// Usage +const report = generateCampaignReport(); +exportToSheets(report, 'Campaign Report'); +``` + +--- + +## BUDGET MANAGEMENT + +### Campaign Budgets + +**Get budget:** +```javascript +const budget = campaign.getBudget(); +const dailyBudget = budget.getAmount(); // In micros +``` + +**Set budget:** +```javascript +// Set daily budget +campaign.getBudget().setAmount(5000000); // 5000 in local currency + +// Budget in micros = amount in local currency * 1,000,000 +// Example: 50.00 USD = 50000000 micros +``` + +**Budget info:** +```javascript +const budget = campaign.getBudget(); +const hasExplicitBudget = budget.isExplicitlyShared(); +const budgetPeriod = budget.getPeriod(); // DAILY +``` + +### Budget Allocation + +**Distribute budget across campaigns:** +```javascript +function distributeBudget(totalBudgetMicros, campaignNames) { + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .get(); + + const numCampaigns = campaignNames.length; + const budgetPerCampaign = totalBudgetMicros / numCampaigns; + + let campaignCount = 0; + while (campaigns.hasNext() && campaignCount < numCampaigns) { + const campaign = campaigns.next(); + if (campaignNames.includes(campaign.getName())) { + campaign.getBudget().setAmount(budgetPerCampaign); + campaignCount++; + } + } +} + +// Usage - 5000 total budget split across 5 campaigns = 1000 each +distributeBudget(5000000000, ['Campaign1', 'Campaign2', 'Campaign3', 'Campaign4', 'Campaign5']); +``` + +### Spending Alerts + +**Monitor daily spend:** +```javascript +function checkDailySpend() { + const dailyLimit = 1000000000; // 1000 in local currency + + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .get(); + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + const stats = campaign.getStatsFor('TODAY'); + const spend = stats.getCost(); + + if (spend > dailyLimit) { + campaign.pause(); + Logger.log('Campaign ' + campaign.getName() + ' paused - spend limit exceeded'); + } + } +} +``` + +--- + +## ADVANCED TARGETING + +### Audience Targeting + +**Remarketing lists:** +```javascript +// Create in-market audience targets +const adGroup = AdsApp.adGroups().get().next(); +const targeting = adGroup.targeting(); + +// Get audience targets +const audiences = targeting.getAudienceTarget().get(); +``` + +### Display Network Targeting + +**Placements:** +```javascript +// Add placement +adGroup.display() + .newPlacementBuilder() + .withUrl('example.com') + .withMaxCpc(50000) + .build(); + +// Negative placements +adGroup.display() + .newNegativePlacementBuilder() + .withUrl('competitor.com') + .build(); +``` + +**Topics:** +```javascript +// Add topic +adGroup.display() + .newTopicBuilder() + .withTopicId('12345678') + .build(); + +// Exclude topic +adGroup.display() + .newNegativeTopicBuilder() + .withTopicId('12345678') + .build(); +``` + +**Keywords:** +```javascript +// Contextual keyword +adGroup.display() + .newDisplayKeywordBuilder() + .withText('shoes') + .build(); + +// Negative display keyword +adGroup.display() + .newNegativeDisplayKeywordBuilder() + .withText('cheap') + .build(); +``` + +--- + +## AUTOMATED RULES & ALERTS + +### Pause Low Performers + +```javascript +function pauseLowPerformingKeywords() { + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.metrics.avg_cpc > 500000') // > 0.50 + .withCondition('keyword.metrics.conversions < 1') + .get(); + + let pausedCount = 0; + while (keywords.hasNext()) { + const keyword = keywords.next(); + keyword.pause(); + pausedCount++; + } + + Logger.log('Paused ' + pausedCount + ' low-performing keywords'); +} +``` + +### Bid Optimization Script + +```javascript +function optimizeBids() { + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.metrics.conversions > 5') + .get(); + + const ROAS_TARGET = 2.0; // 200% + + while (keywords.hasNext()) { + const keyword = keywords.next(); + const stats = keyword.getStatsFor('LAST_30_DAYS'); + const roas = stats.getReturnOnAdSpend(); + const currentBid = keyword.getMaxCpc(); + + if (roas > ROAS_TARGET) { + // Increase bid by 10% + keyword.setMaxCpc(currentBid * 1.1); + } else if (roas < 1.0) { + // Decrease bid by 5% + keyword.setMaxCpc(currentBid * 0.95); + } + } +} +``` + +### Quality Score Monitoring + +```javascript +function monitorQualityScores() { + const lowQualityKeywords = AdsApp.keywords() + .withCondition('keyword.quality_info.quality_score < 5') + .withCondition('keyword.status = ENABLED') + .orderBy('keyword.quality_info.quality_score ASC') + .get(); + + Logger.log('Keywords with quality score < 5:'); + while (lowQualityKeywords.hasNext()) { + const keyword = lowQualityKeywords.next(); + Logger.log(keyword.getText() + ' - QS: ' + keyword.getQualityScore()); + } +} +``` + +--- + +## ERROR HANDLING & DEBUGGING + +### Try-Catch Pattern + +```javascript +function safeAdsOperation() { + try { + const campaigns = AdsApp.campaigns() + .withCondition('campaign.name = "NonExistent"') + .get(); + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + // Process + } + } catch (error) { + Logger.log('Error in campaign operation: ' + error.message); + } +} +``` + +### Null Checks + +```javascript +function handleNullSafely() { + const campaigns = AdsApp.campaigns().get(); + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + + // Check for null/undefined + const budget = campaign.getBudget(); + if (!budget) { + Logger.log('No budget for ' + campaign.getName()); + continue; + } + + const amount = budget.getAmount(); + if (amount === null || amount === undefined) { + Logger.log('Budget amount not set'); + continue; + } + } +} +``` + +### Logging Best Practices + +```javascript +function logOperations() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const logSheet = ss.getSheetByName('ScriptLog') || ss.insertSheet('ScriptLog'); + + try { + const campaigns = AdsApp.campaigns().get(); + const count = campaigns.totalNumEntities(); + logSheet.appendRow([new Date(), 'SUCCESS', 'Processed ' + count + ' campaigns']); + } catch (error) { + logSheet.appendRow([new Date(), 'ERROR', error.message, error.stack]); + } +} +``` + +--- + +## PERFORMANCE OPTIMIZATION + +### Batch Operations + +**Process campaigns efficiently:** +```javascript +function optimizeBatchProcessing() { + // SLOW - Individual operations + const keywords = AdsApp.keywords().get(); + while (keywords.hasNext()) { + const keyword = keywords.next(); + if (keyword.getQualityScore() < 5) { + keyword.setMaxCpc(keyword.getMaxCpc() * 0.9); + } + } + + // FAST - Batch collect and process + const keywordsToUpdate = []; + const keywords = AdsApp.keywords() + .withCondition('keyword.quality_info.quality_score < 5') + .get(); + + while (keywords.hasNext()) { + keywordsToUpdate.push(keywords.next()); + } + + // Perform all updates + keywordsToUpdate.forEach(keyword => { + keyword.setMaxCpc(keyword.getMaxCpc() * 0.9); + }); +} +``` + +### Limiting Results + +```javascript +function useEffectiveFiltering() { + // Get only what you need + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.metrics.clicks > 10') + .withCondition('keyword.metrics.conversions = 0') + .withLimit(1000) + .get(); + + // Process limited results +} +``` + +### Caching Results + +```javascript +function cacheReportsToSheets() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const cacheSheet = ss.getSheetByName('Cache') || ss.insertSheet('Cache'); + + // Generate report (expensive operation) + const campaigns = AdsApp.campaigns().get(); + const report = []; + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + report.push([ + campaign.getName(), + campaign.getStatsFor('LAST_7_DAYS').getCost() / 1000000 + ]); + } + + // Cache to sheet + cacheSheet.clear(); + cacheSheet.getRange(1, 1, report.length, 2).setValues(report); +} +``` + +--- + +## BEST PRACTICES + +### Complete Automation Template + +```javascript +/** + * Main entry point for Google Ads automation + */ +function main() { + try { + Logger.log('Starting Ads Script execution: ' + new Date()); + + // Validate setup + validateAccountSetup(); + + // Execute tasks + const results = { + paused: pauseLowQualityKeywords(), + optimized: optimizeHighPerformingKeywords(), + alerts: checkBudgetStatus() + }; + + // Log results + logResults(results); + + Logger.log('Completed successfully'); + } catch (error) { + handleError(error); + } +} + +/** + * Validate script setup + */ +function validateAccountSetup() { + const account = AdsApp.currentAccount(); + if (!account) { + throw new Error('No account access'); + } +} + +/** + * Pause low quality keywords + */ +function pauseLowQualityKeywords() { + let count = 0; + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.quality_info.quality_score < 4') + .get(); + + while (keywords.hasNext()) { + keywords.next().pause(); + count++; + } + + return count; +} + +/** + * Optimize high performers + */ +function optimizeHighPerformingKeywords() { + let count = 0; + const keywords = AdsApp.keywords() + .withCondition('keyword.status = ENABLED') + .withCondition('keyword.metrics.cost_per_conversion < 500000') + .get(); + + while (keywords.hasNext()) { + const keyword = keywords.next(); + const bid = keyword.getMaxCpc(); + keyword.setMaxCpc(bid * 1.05); // 5% increase + count++; + } + + return count; +} + +/** + * Check budget status + */ +function checkBudgetStatus() { + const alerts = []; + const campaigns = AdsApp.campaigns() + .withCondition('campaign.status = ENABLED') + .get(); + + while (campaigns.hasNext()) { + const campaign = campaigns.next(); + const stats = campaign.getStatsFor('TODAY'); + const budget = campaign.getBudget().getAmount(); + const spend = stats.getCost(); + + if (spend > budget * 0.9) { + alerts.push(campaign.getName() + ': 90% of budget consumed'); + } + } + + return alerts; +} + +/** + * Log results to sheet + */ +function logResults(results) { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = ss.getSheetByName('Log') || ss.insertSheet('Log'); + + sheet.appendRow([ + new Date(), + results.paused + ' keywords paused', + results.optimized + ' keywords optimized', + results.alerts.length + ' alerts' + ]); +} + +/** + * Error handler + */ +function handleError(error) { + Logger.log('ERROR: ' + error.message); + + // Send notification + MailApp.sendEmail(Session.getEffectiveUser().getEmail(), + 'Ads Script Error', + 'Error: ' + error.message + '\n\n' + error.stack); +} +``` + +### Code Organization + +```javascript +// File: main.gs +function main() { + CampaignOptimizer.run(); + KeywordManager.run(); + ReportGenerator.run(); +} + +// File: campaign-optimizer.gs +const CampaignOptimizer = { + run: function() { + this.pauseUnderperformers(); + this.optimizeHigh Performers(); + }, + + pauseUnderperformers: function() { + // Implementation + }, + + optimizeHighPerformers: function() { + // Implementation + } +}; + +// File: keyword-manager.gs +const KeywordManager = { + run: function() { + this.updateBids(); + this.removeNegatives(); + }, + + updateBids: function() { + // Implementation + }, + + removeNegatives: function() { + // Implementation + } +}; +``` + +--- + +## QUOTAS AND LIMITS + +| Limit | Value | Notes | +|-------|-------|-------| +| **Execution time** | 30 minutes | Per script run | +| **API calls** | Rate limited | Account limits vary | +| **Campaigns per account** | Unlimited | Practical limit ~5000 | +| **Ad groups per campaign** | Unlimited | Practical limit ~20000 | +| **Keywords per ad group** | Unlimited | Practical limit ~5000 | +| **Bid changes** | Unlimited | Subject to rate limiting | +| **Budget changes** | Unlimited | Subject to rate limiting | +| **Frequency of updates** | Once per hour | Recommended minimum | + +--- + +**End of Google Ads Script Mission-Critical Reference** + +*Use this as your authoritative offline guide for all Google Ads Script development needs.* \ No newline at end of file diff --git a/skills/google-ads-scripts/scripts/validators.py b/skills/google-ads-scripts/scripts/validators.py new file mode 100644 index 0000000..b8b40d6 --- /dev/null +++ b/skills/google-ads-scripts/scripts/validators.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +""" +Google Ads Script Validators + +Validation utilities for Google Ads campaigns, bids, budgets, and keywords. +Use these validators before applying changes to ensure data integrity. + +Based on Google Ads API specifications and best practices. +""" + +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass +import re + + +@dataclass +class ValidationResult: + """Result of a validation check""" + is_valid: bool + errors: List[str] + warnings: List[str] = None + + def __post_init__(self): + if self.warnings is None: + self.warnings = [] + + +class GoogleAdsValidators: + """Comprehensive validation for Google Ads Script operations""" + + # Constants + MIN_BUDGET = 0.01 # Minimum $0.01 + MAX_BUDGET = 999999.99 # Practical maximum + MIN_BID = 0.01 # Minimum $0.01 in currency + MIN_BID_MICROS = 10000 # Minimum 0.01 in micros + MICROS_MULTIPLIER = 1000000 + + @staticmethod + def is_campaign_name_valid(name: str) -> Tuple[bool, Optional[str]]: + """ + Validate campaign name. + + Rules: + - Must be string + - Length 1-255 characters + + Returns: + (is_valid, error_message) + """ + if not name or not isinstance(name, str): + return False, "Campaign name must be a non-empty string" + + if len(name) < 1 or len(name) > 255: + return False, f"Campaign name must be 1-255 characters (got {len(name)})" + + return True, None + + @classmethod + def is_budget_valid(cls, amount: float) -> Tuple[bool, Optional[str]]: + """ + Validate budget amount in local currency. + + Args: + amount: Budget in local currency (e.g., 50.00 for $50) + + Returns: + (is_valid, error_message) + """ + if not isinstance(amount, (int, float)): + return False, "Budget must be a number" + + if amount < cls.MIN_BUDGET: + return False, f"Budget must be at least {cls.MIN_BUDGET}" + + if amount > cls.MAX_BUDGET: + return False, f"Budget exceeds maximum of {cls.MAX_BUDGET}" + + return True, None + + @classmethod + def is_bid_valid(cls, bid: float) -> Tuple[bool, Optional[str]]: + """ + Validate bid amount in local currency. + + Args: + bid: Bid in local currency (e.g., 1.50 for $1.50) + + Returns: + (is_valid, error_message) + """ + if not isinstance(bid, (int, float)): + return False, "Bid must be a number" + + if bid < cls.MIN_BID: + return False, f"Bid must be at least {cls.MIN_BID}" + + return True, None + + @staticmethod + def is_quality_score_valid(score: Any) -> Tuple[bool, Optional[str]]: + """ + Validate quality score. + + Args: + score: Quality score (1-10 or None) + + Returns: + (is_valid, error_message) + """ + if score is None: + return True, None # Quality score can be null + + if not isinstance(score, int): + return False, "Quality score must be an integer" + + if score < 1 or score > 10: + return False, "Quality score must be 1-10" + + return True, None + + @classmethod + def is_cpc_bid_micros_valid(cls, bid_micros: int, max_bid: float = 10000) -> Tuple[bool, Optional[str]]: + """ + Validate CPC bid in micros. + + Args: + bid_micros: Bid in micros (e.g., 50000 for $0.05) + max_bid: Maximum allowed bid in currency + + Returns: + (is_valid, error_message) + """ + if not isinstance(bid_micros, int): + return False, "Bid (micros) must be an integer" + + if bid_micros < cls.MIN_BID_MICROS: + return False, f"Bid must be at least {cls.MIN_BID_MICROS} micros ($0.01)" + + max_bid_micros = int(max_bid * cls.MICROS_MULTIPLIER) + if bid_micros > max_bid_micros: + return False, f"Bid exceeds maximum of {max_bid_micros} micros (${max_bid})" + + return True, None + + @staticmethod + def is_campaign_status_valid(status: str) -> Tuple[bool, Optional[str]]: + """ + Validate campaign status. + + Args: + status: Campaign status + + Returns: + (is_valid, error_message) + """ + valid_statuses = ['ENABLED', 'PAUSED', 'REMOVED'] + + if status not in valid_statuses: + return False, f"Status must be one of {valid_statuses}" + + return True, None + + @staticmethod + def is_campaign_type_valid(campaign_type: str) -> Tuple[bool, Optional[str]]: + """ + Validate campaign type. + + Args: + campaign_type: Campaign type + + Returns: + (is_valid, error_message) + """ + valid_types = ['SEARCH', 'DISPLAY', 'SHOPPING', 'VIDEO', 'PERFORMANCE_MAX'] + + if campaign_type not in valid_types: + return False, f"Campaign type must be one of {valid_types}" + + return True, None + + @staticmethod + def is_keyword_text_valid(text: str) -> Tuple[bool, Optional[str]]: + """ + Validate keyword text. + + Args: + text: Keyword text + + Returns: + (is_valid, error_message) + """ + if not text or not isinstance(text, str): + return False, "Keyword text must be a non-empty string" + + if len(text) < 1 or len(text) > 80: + return False, f"Keyword text must be 1-80 characters (got {len(text)})" + + return True, None + + @staticmethod + def is_match_type_valid(match_type: str) -> Tuple[bool, Optional[str]]: + """ + Validate keyword match type. + + Args: + match_type: Match type + + Returns: + (is_valid, error_message) + """ + valid_types = ['BROAD', 'PHRASE', 'EXACT'] + + if match_type not in valid_types: + return False, f"Match type must be one of {valid_types}" + + return True, None + + @classmethod + def validate_campaign_update(cls, updates: Dict[str, Any]) -> ValidationResult: + """ + Validate campaign update payload. + + Args: + updates: Dictionary with campaign update fields + + Returns: + ValidationResult with is_valid and errors + """ + errors = [] + warnings = [] + + # Validate name + if 'name' in updates: + is_valid, error = cls.is_campaign_name_valid(updates['name']) + if not is_valid: + errors.append(error) + + # Validate budget + if 'budget' in updates: + is_valid, error = cls.is_budget_valid(updates['budget']) + if not is_valid: + errors.append(error) + + # Validate status + if 'status' in updates: + is_valid, error = cls.is_campaign_status_valid(updates['status']) + if not is_valid: + errors.append(error) + + # Validate type + if 'type' in updates: + is_valid, error = cls.is_campaign_type_valid(updates['type']) + if not is_valid: + errors.append(error) + + # Validate dates + if 'start_date' in updates and 'end_date' in updates: + try: + from datetime import datetime + start = datetime.fromisoformat(updates['start_date']) + end = datetime.fromisoformat(updates['end_date']) + if end <= start: + errors.append("End date must be after start date") + except ValueError: + errors.append("Invalid date format (use YYYY-MM-DD)") + + return ValidationResult( + is_valid=len(errors) == 0, + errors=errors, + warnings=warnings + ) + + @classmethod + def validate_keyword_update(cls, updates: Dict[str, Any]) -> ValidationResult: + """ + Validate keyword update payload. + + Args: + updates: Dictionary with keyword update fields + + Returns: + ValidationResult with is_valid and errors + """ + errors = [] + warnings = [] + + # Validate keyword text + if 'text' in updates: + is_valid, error = cls.is_keyword_text_valid(updates['text']) + if not is_valid: + errors.append(error) + + # Validate match type + if 'match_type' in updates: + is_valid, error = cls.is_match_type_valid(updates['match_type']) + if not is_valid: + errors.append(error) + + # Validate bid (if in currency) + if 'max_cpc' in updates: + is_valid, error = cls.is_bid_valid(updates['max_cpc']) + if not is_valid: + errors.append(error) + + # Validate bid (if in micros) + if 'max_cpc_micros' in updates: + is_valid, error = cls.is_cpc_bid_micros_valid(updates['max_cpc_micros']) + if not is_valid: + errors.append(error) + + # Validate status + if 'status' in updates: + is_valid, error = cls.is_campaign_status_valid(updates['status']) + if not is_valid: + errors.append(error) + + return ValidationResult( + is_valid=len(errors) == 0, + errors=errors, + warnings=warnings + ) + + @classmethod + def validate_quota_remaining(cls, current_cost: int, daily_limit: int, used: int) -> Dict[str, Any]: + """ + Track API quota consumption. + + Args: + current_cost: Cost of current operation + daily_limit: Daily quota limit + used: Amount of quota already used + + Returns: + Dictionary with quota status + """ + remaining = daily_limit - used + percent_used = (used / daily_limit) * 100 if daily_limit > 0 else 100 + + return { + 'remaining': remaining, + 'percent_used': percent_used, + 'is_critical': percent_used > 90, + 'should_throttle': percent_used > 85, + 'can_proceed': (used + current_cost) <= daily_limit + } + + +def currency_to_micros(amount: float) -> int: + """ + Convert currency to micros. + + Args: + amount: Amount in local currency (e.g., 5.00 for $5) + + Returns: + Amount in micros (e.g., 5000000) + """ + return int(amount * GoogleAdsValidators.MICROS_MULTIPLIER) + + +def micros_to_currency(micros: int) -> float: + """ + Convert micros to currency. + + Args: + micros: Amount in micros (e.g., 5000000) + + Returns: + Amount in local currency (e.g., 5.00) + """ + return micros / GoogleAdsValidators.MICROS_MULTIPLIER + + +# Example usage +if __name__ == "__main__": + validators = GoogleAdsValidators() + + # Test campaign validation + campaign_data = { + 'name': 'Q4 Sale Campaign', + 'budget': 5000, + 'status': 'ENABLED', + 'type': 'SEARCH' + } + + result = validators.validate_campaign_update(campaign_data) + print(f"Campaign validation: {'✅ Valid' if result.is_valid else '❌ Invalid'}") + if result.errors: + print(f"Errors: {result.errors}") + + # Test currency conversion + bid_currency = 1.50 + bid_micros = currency_to_micros(bid_currency) + print(f"\n${bid_currency} = {bid_micros} micros") + print(f"{bid_micros} micros = ${micros_to_currency(bid_micros)}") + + # Test keyword validation + keyword_data = { + 'text': 'running shoes', + 'match_type': 'PHRASE', + 'max_cpc': 2.50 + } + + result = validators.validate_keyword_update(keyword_data) + print(f"\nKeyword validation: {'✅ Valid' if result.is_valid else '❌ Invalid'}") + if result.errors: + print(f"Errors: {result.errors}")