Production-ready prompts, scripts, frameworks and AI agents for Google Ads professionals. No payment required.
var CONFIG = {
// ═══════════════════════════════════════════════════════════════════════════
// ALERT THRESHOLDS
// ═══════════════════════════════════════════════════════════════════════════
SPEND_SPIKE_PERCENT: 50, // Alert if today's spend is 50%+ above average
CPA_SPIKE_PERCENT: 75, // Alert if today's CPA is 75%+ above average
ZERO_CONV_SPEND: 200, // Alert if campaign spends $200+ with 0 conv today
PROJECTED_OVERSPEND: 130, // Alert if projected daily spend > 130% of budget
BASELINE_DAYS: 14, // Days to calculate baseline averages
MIN_BASELINE_SPEND: 10, // Skip campaigns with <$10 avg daily spend
// ═══════════════════════════════════════════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════════════════════════════════════════
EMAIL_RECIPIENTS: [],
SLACK_WEBHOOK_URL: '',
SPREADSHEET_URL: 'CREATE_NEW',
};
function main() {
var campaigns = getCampaignData();
var baselines = getBaselines();
var alerts = [];
campaigns.forEach(function(c) {
var base = baselines[c.name];
if (!base || base.avgSpend < CONFIG.MIN_BASELINE_SPEND) return;
// Spend spike
if (c.cost > base.avgSpend * (1 + CONFIG.SPEND_SPIKE_PERCENT / 100)) {
alerts.push({
campaign: c.name,
type: 'SPEND_SPIKE',
severity: 'high',
message: 'Spend $' + c.cost.toFixed(2) + ' vs avg $' + base.avgSpend.toFixed(2) +
' (+' + (((c.cost / base.avgSpend) - 1) * 100).toFixed(0) + '%)',
});
}
// CPA spike
if (c.conversions > 0 && base.avgCpa > 0) {
var todayCpa = c.cost / c.conversions;
if (todayCpa > base.avgCpa * (1 + CONFIG.CPA_SPIKE_PERCENT / 100)) {
alerts.push({
campaign: c.name,
type: 'CPA_SPIKE',
severity: 'medium',
message: 'CPA $' + todayCpa.toFixed(2) + ' vs avg $' + base.avgCpa.toFixed(2),
});
}
}
// Zero conversions with high spend
if (c.conversions === 0 && c.cost >= CONFIG.ZERO_CONV_SPEND) {
alerts.push({
campaign: c.name,
type: 'ZERO_CONV',
severity: 'high',
message: 'Spent $' + c.cost.toFixed(2) + ' with 0 conversions today',
});
}
// Projected overspend
var hoursElapsed = new Date().getHours() + (new Date().getMinutes() / 60);
if (hoursElapsed > 2) {
var projected = (c.cost / hoursElapsed) * 24;
var budget = c.budget;
if (projected > budget * (CONFIG.PROJECTED_OVERSPEND / 100)) {
alerts.push({
campaign: c.name,
type: 'PROJECTED_OVERSPEND',
severity: 'medium',
message: 'Projected $' + projected.toFixed(2) + ' vs budget $' + budget.toFixed(2),
});
}
}
});
if (alerts.length > 0) {
logAlerts(alerts);
notify(alerts);
}
Logger.log('Check complete. ' + alerts.length + ' anomalies detected.');
}
function getCampaignData() {
var campaigns = [];
var iter = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.get();
while (iter.hasNext()) {
var camp = iter.next();
var stats = camp.getStatsFor('TODAY');
campaigns.push({
name: camp.getName(),
cost: stats.getCost(),
clicks: stats.getClicks(),
conversions: stats.getConversions(),
budget: camp.getBudget().getAmount(),
});
}
return campaigns;
}
function getBaselines() {
var endDate = new Date();
var startDate = new Date();
startDate.setDate(startDate.getDate() - CONFIG.BASELINE_DAYS);
var fmt = function(d) {
return Utilities.formatDate(d, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');
};
var query = 'SELECT CampaignName, Cost, Conversions ' +
'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
'WHERE CampaignStatus = ENABLED ' +
'DURING ' + fmt(startDate) + ',' + fmt(endDate);
var report = AdsApp.report(query);
var rows = report.rows();
var data = {};
while (rows.hasNext()) {
var row = rows.next();
var name = row['CampaignName'];
if (!data[name]) data[name] = { totalSpend: 0, totalConv: 0, days: 0 };
data[name].totalSpend += parseFloat(row['Cost']);
data[name].totalConv += parseFloat(row['Conversions']);
data[name].days++;
}
var baselines = {};
Object.keys(data).forEach(function(name) {
var d = data[name];
baselines[name] = {
avgSpend: d.totalSpend / CONFIG.BASELINE_DAYS,
avgCpa: d.totalConv > 0 ? d.totalSpend / d.totalConv : 0,
};
});
return baselines;
}
function logAlerts(alerts) {
var ss;
if (CONFIG.SPREADSHEET_URL === 'CREATE_NEW') {
ss = SpreadsheetApp.create('Anomaly Alerts ' + new Date().toISOString().split('T')[0]);
Logger.log('Log: ' + ss.getUrl());
} else {
ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
}
var sheet = ss.getActiveSheet();
if (sheet.getLastRow() === 0) {
sheet.appendRow(['Timestamp', 'Campaign', 'Type', 'Severity', 'Details']);
}
var now = new Date().toISOString();
alerts.forEach(function(a) {
sheet.appendRow([now, a.campaign, a.type, a.severity, a.message]);
});
}
function notify(alerts) {
var high = alerts.filter(function(a) { return a.severity === 'high'; });
var subject = '⚠️ ' + alerts.length + ' anomalies' +
(high.length > 0 ? ' (' + high.length + ' critical)' : '');
var body = alerts.map(function(a) {
return '[' + a.severity.toUpperCase() + '] ' + a.campaign + ': ' + a.message;
}).join('\n');
CONFIG.EMAIL_RECIPIENTS.forEach(function(email) {
MailApp.sendEmail(email, subject, body);
});
if (CONFIG.SLACK_WEBHOOK_URL) {
UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: '*' + subject + '*\n```\n' + body + '\n```' }),
});
}
}
| Type | Trigger | Default Threshold |
|---|---|---|
SPEND_SPIKE | Today’s spend vs 14-day average | +50% |
CPA_SPIKE | Today’s CPA vs 14-day average | +75% |
ZERO_CONV | Spend with zero conversions | $200+ |
PROJECTED_OVERSPEND | Projected daily spend vs budget | >130% |
Schedule to run every hour during business hours. The script compares today’s running totals against the 14-day baseline, so alerts get more accurate as the day progresses.