Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:32:43 +08:00
commit e959028259
8 changed files with 2925 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -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.

61
plugin.lock.json Normal file
View File

@@ -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": []
}
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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}")