Google Ads Negative Keyword Conflict Checker Script (GAQL)
Automated Google Ads script that cross-references campaign, ad group and shared negative lists against active positive keywords to flag silent blocking.
Google Ads GAQL Negative Collision Architecture
Automated Google Ads script executing GAQL queries across campaign_criterion and ad_group_criterion, checking negative keywords against active phrase and exact match targets, and alerting via webhooks.
Google Ads Negative Keyword Conflict Checker Script (GAQL) Configurator
Brand & Search Protection — Negative Keyword Collision Scanner
Account-wide scan detecting negative keywords that inadvertently block converting search queries or active positive keywords.
1 Input Parameters & Assumptions
| Parameter | Value | Context & Provenance |
|---|---|---|
| Negative Criterion Scope | Campaign & Shared Lists Negative Lists | Evaluates campaign-level negatives and shared negative keyword lists |
| Positive Keyword Scope | Enabled Search Ads Keywords | Active bidded keywords across all active Search and Shopping ad groups |
| Alert Destination | Google Chat Room Webhook Target | PPC team Google Chat room incoming webhook |
2 Explicit Mathematical Formula
Collision Logic: Negative Keyword Match (Exact / Phrase / Broad) ∩ Active Positive Keyword Text
Critical Priority Trigger: Negative keyword blocks a search term with >= 1 conversion in trailing 90 days
Warning Priority Trigger: Negative keyword blocks an enabled positive exact match keyword
Alert Payload: Google Chat Webhook with CID, Campaign Name, Colliding Negative, and Blocked Positive Keyword3 Computed Output Metrics
| Computed Metric | Result | Interpretation & Threshold |
|---|---|---|
| Critical Collision Detection | Active Conflict Level | Instantly identifies negatives blocking historical converting queries |
| Match Type Disambiguation | Exact / Phrase / Broad Match Types | Full phrase and broad-match tokenization prevents false positives while catching true collisions |
Google Ads Negative Keyword Conflict Checker Script (GAQL) — Scope & Limitations
Explicit operational boundaries and constraints defining target use cases and out-of-scope scenarios.
Built For (Target Use Cases)
- Single Google Ads account audits comparing negative keyword lists against enabled positive keywords and search terms.
- Detecting exact, phrase and broad match negative keywords that silently block active search campaigns.
- Routing collision alerts to Slack, Microsoft Teams, Discord, Google Chat or email webhooks.
Not Built For (Limitations & Out-of-Scope)
- Multi-account (MCC) portfolios; the script config targets one Google Ads account ID at a time.
- Shopping or Performance Max campaigns without keyword-level targeting to cross-reference.
- Automatic removal of colliding negatives; it only generates alert payloads, not remediation actions.
Operational Assumptions & Defaults
- Assumes campaign and shared negative lists plus enabled search keywords are queried via GAQL.
- Webhook URL, account ID and dry-run mode are configured manually before deployment.
- Collision detection runs on a fixed daily schedule rather than real-time bid events.
Google Ads Automated Governance Script & Multi-Channel Webhook Builder
Google Ads API & AutomationConfigure production-ready Google Ads scripts with live GAQL linting, dry-run toggles, GTM/GA4 error monitoring tags, and multi-channel webhook formatting for Slack, Microsoft Teams, Discord, Google Chat, and Email alerts.
Script Template & Destination
Thresholds & Execution Settings
Generated Artifacts Preview
/**
* ============================================================================
* Google Ads Governance Automation: Negative Keyword Conflict Checker
* Strategy: negative-keyword-conflict-checker
* Author: Gordon Geraghty - Head of Performance Media (https://gordongeraghty.com)
* Generated:
* ============================================================================
*/
const CONFIG = {
ACCOUNT_ID: "123-456-7890",
WEBHOOK_CHANNEL: "slack",
WEBHOOK_URL: "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
NOTIFICATION_EMAIL: "{{EMAIL_ADDRESS}}",
DRY_RUN: false,
ANOMALY_MULTIPLIER: 2.5,
MONTHLY_BUDGET_LIMIT: 15000,
URL_BATCH_SIZE: 50,
GAQL_QUERY: "SELECT campaign_criterion.keyword.text, campaign_criterion.keyword.match_type, campaign_criterion.negative, campaign.name, campaign.status FROM campaign_criterion WHERE campaign_criterion.type = 'KEYWORD' AND campaign_criterion.negative = TRUE AND campaign.status = 'ENABLED'"
};
function main() {
Logger.log('Starting Google Ads Governance Script: ' + 'Negative Keyword Conflict Checker');
Logger.log('Account ID: ' + CONFIG.ACCOUNT_ID + ' | Dry Run: ' + CONFIG.DRY_RUN);
try {
const report = AdsApp.report(CONFIG.GAQL_QUERY);
const rows = report.rows();
const alertFindings = [];
while (rows.hasNext()) {
const row = rows.next();
if (evaluateRowCondition(row)) {
alertFindings.push(formatFinding(row));
}
}
Logger.log('Total issues / anomalies detected: ' + alertFindings.length);
if (alertFindings.length > 0) {
dispatchAlert(alertFindings);
} else {
Logger.log('All checks passed within normal thresholds.');
}
} catch (err) {
Logger.log('Error executing governance script: ' + err.toString());
}
}
function evaluateRowCondition(row) {
return row['campaign_criterion.negative'] === true || row['campaign_criterion.negative'] === 'true';
}
function formatFinding(row) {
return {
campaign: row['campaign.name'] || 'Unknown Campaign',
detail: JSON.stringify(row),
timestamp: new Date().toISOString()
};
}
function dispatchAlert(findings) {
if (CONFIG.DRY_RUN) {
Logger.log('[DRY-RUN MODE] Alerts logged to console only (no webhook dispatched).');
Logger.log(JSON.stringify(findings, null, 2));
return;
}
const payload = buildWebhookPayload(findings);
if (CONFIG.WEBHOOK_CHANNEL === 'email') {
MailApp.sendEmail({
to: CONFIG.NOTIFICATION_EMAIL,
subject: '[ALERT] Google Ads Governance: ' + 'Negative Keyword Conflict Checker',
body: 'Issues detected:
' + JSON.stringify(findings, null, 2)
});
Logger.log('Dispatched alert email to: ' + CONFIG.NOTIFICATION_EMAIL);
return;
}
if (CONFIG.WEBHOOK_URL && CONFIG.WEBHOOK_URL.startsWith('http')) {
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(CONFIG.WEBHOOK_URL, options);
Logger.log('Webhook dispatched to ' + CONFIG.WEBHOOK_CHANNEL + '. Status: ' + response.getResponseCode());
} else {
Logger.log('Warning: No valid WEBHOOK_URL configured.');
}
}
function buildWebhookPayload(findings) {
return {
"text": "*Negative Keyword Collision Detected: Campaign High-Volume Shopping* [CRITICAL]",
"attachments": [
{
"color": "#e01e5a",
"title": "Negative Keyword Collision Detected: Campaign High-Volume Shopping",
"text": "An account or campaign negative keyword is blocking an active, high-intent positive keyword from serving.",
"fields": [
{
"title": "Colliding Keyword",
"value": "running shoes sale",
"short": true
},
{
"title": "Match Type",
"value": "PHRASE",
"short": true
},
{
"title": "Blocking Negative Level",
"value": "CAMPAIGN",
"short": true
},
{
"title": "Account ID",
"value": "123-456-7890",
"short": true
}
],
"footer": "Gordon Geraghty Ads Governance Automation",
"ts": 1748736000
}
]
};
}
Deploy to Google Ads Scripts Console
Navigate to Google Ads > Tools & Settings > Scripts, click "+" to create a new script, paste the configured code, authorize account access, and set a daily schedule.
How Negative Keyword Collisions Waste High-Intent Budget
Shared negative lists and campaign-level broad negatives frequently block exact and phrase match positive keywords without triggering any visible alert in the Google Ads user interface. This script cross-checks all serving campaigns hourly to ensure active revenue-generating keywords remain unobstructed.
Implementation Code & Script
Copy and paste into Google Ads Scripts editor. Filters to serving campaigns and sends alert emails.
/**
* Google Ads Negative Keyword Conflict Checker (GAQL)
* Author: Gordon Geraghty - Head of Performance Media
* Licence: MIT
* Filter: campaign.status = "ENABLED" AND campaign.serving_status = "SERVING"
*/
const CONFIG = {
EMAIL_RECIPIENT: 'alerts@example.com',
SPREADSHEET_URL: '', // Optional: Paste a Google Sheet URL to log findings
};
function main() {
Logger.log('Starting Negative Keyword Conflict Audit...');
// 1. Fetch all active positive keywords in serving campaigns
const positiveQuery =
'SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ' +
'ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type ' +
'FROM keyword_view ' +
'WHERE campaign.status = "ENABLED" ' +
'AND campaign.serving_status = "SERVING" ' +
'AND ad_group.status = "ENABLED" ' +
'AND ad_group_criterion.status = "ENABLED"';
const positiveRows = AdsApp.search(positiveQuery);
const positiveKeywords = [];
while (positiveRows.hasNext()) {
const row = positiveRows.next();
positiveKeywords.push({
campaignName: row.campaign.name,
adGroupName: row.adGroup.name,
text: row.adGroupCriterion.keyword.text.toLowerCase().trim(),
matchType: row.adGroupCriterion.keyword.matchType,
});
}
Logger.log('Found ' + positiveKeywords.length + ' active positive keywords.');
// 2. Fetch all campaign and ad group negative keywords
const negativeQuery =
'SELECT campaign.name, ad_group.name, ' +
'campaign_criterion.keyword.text, campaign_criterion.keyword.match_type, ' +
'campaign_criterion.negative ' +
'FROM campaign_criterion ' +
'WHERE campaign.status = "ENABLED" ' +
'AND campaign.serving_status = "SERVING" ' +
'AND campaign_criterion.negative = TRUE';
const negativeRows = AdsApp.search(negativeQuery);
const conflicts = [];
while (negativeRows.hasNext()) {
const neg = negativeRows.next();
const negText = neg.campaignCriterion?.keyword?.text?.toLowerCase().trim();
if (!negText) continue;
positiveKeywords.forEach(function(pos) {
if (pos.campaignName === neg.campaign.name && pos.text.indexOf(negText) !== -1) {
conflicts.push({
campaign: pos.campaignName,
adGroup: pos.adGroupName,
positiveKeyword: pos.text,
negativeKeyword: negText,
});
}
});
}
Logger.log('Audit complete. Conflicts detected: ' + conflicts.length);
if (conflicts.length > 0 && CONFIG.EMAIL_RECIPIENT) {
let body = 'Google Ads Negative Keyword Conflicts Detected:\n\n';
conflicts.forEach(function(c) {
body += 'Campaign: ' + c.campaign + ' | Ad Group: ' + c.adGroup + '\n';
body += 'Positive: "' + c.positiveKeyword + '" BLOCKED BY Negative: "' + c.negativeKeyword + '"\n\n';
});
MailApp.sendEmail(CONFIG.EMAIL_RECIPIENT, 'Google Ads Conflict Alert: ' + conflicts.length + ' blocked keywords', body);
}
}Google Ads Script Preview & QA Guide
Run the script in "Preview" mode inside Google Ads to inspect logger output and confirm webhook delivery before scheduling.
Pre-Production Verification Checklist
Confirm zero execution errors and inspect output logs in Google Ads Script console.
Check Slack, Teams, or Discord channel to confirm test collision alert message arrives formatted.
Set script frequency to Daily at 06:00 AM to detect new negative keyword list additions before morning spend.
Terminal Diagnostic & Debug Commands
Tests destination webhook endpoint connectivity and message rendering.
curl -X POST -H "Content-Type: application/json" -d '{"text":"⚠️ [TEST] Negative Keyword Conflict: \"enterprise\" blocked by campaign negative \"enterprise software\""}' https://hooks.slack.com/services/YOUR/WEBHOOK/URLFailure Remediation & Troubleshooting
Cause: First-time script creation requires explicit OAuth permissions approval in Google Ads UI.
Fix: Click "Authorize" button in the Google Ads Scripts editor banner and accept Google account access.
Cause: Querying millions of search terms without campaign status = ENABLED filtering.
Fix: Add filters to restrict GAQL query to active enabled campaigns with impression volume > 0.
How to cite and attribute this tool
MIT LicenceThis resource is free, open and un-gated under the MIT Open Source Licence. You are encouraged to use, integrate and cite it with attribution:
@misc{geraghty_negative_keyword_conflict_checker,
author = {Geraghty, Gordon},
title = {Google Ads Negative Keyword Conflict Checker Script (GAQL)},
year = {2026},
url = {https://gordongeraghty.com/resources/performance-media/negative-keyword-conflict-checker},
note = {Head of Performance Media, Empire Amplify}
}Changelog & Version History
v1.0.0Initial release using GAQL through AdsApp.search with serving campaign status filtering.
Strategic Takeaway & Operational Guidelines
Broad and phrase match negative keywords added to prevent wasted spend frequently collide with newly launched product lines or high-intent brand variations. This script prevents silent revenue loss by alerting on conflicts immediately.