← Back to all posts

Quality Score Monitoring and Improvement Script for Google Ads

Published: 2023-08-15

By: Michael Mares


Quality score dashboard for Google Ads

Improve your Google Ads performance with this powerful script that automatically monitors quality scores and provides actionable recommendations for improvement.

Quality Score Monitoring and Improvement Script for Google Ads

Quality Score is one of the most important metrics in Google Ads, directly affecting your ad positions and costs per click. However, monitoring quality scores across a large account and identifying opportunities for improvement can be challenging. This script automates the process by tracking quality scores over time and providing actionable recommendations.

The Problem This Script Solves

  • Manual quality score monitoring is time-consuming across large accounts
  • It’s difficult to identify which components need improvement (expected CTR, ad relevance, landing page experience)
  • Tracking quality score changes over time requires manual data collection
  • Prioritizing which keywords to optimize first isn’t always clear

How The Script Works

This script analyzes your account’s quality scores, identifies low-performing keywords, tracks changes over time, and provides specific recommendations for improvement based on the component scores.

/**
 * Quality Score Monitoring and Improvement Script for Google Ads
 * 
 * This script tracks quality scores over time and provides recommendations
 * for improving low-performing keywords.
 */

// Configuration
var CONFIG = {
  // Spreadsheet URL for tracking quality scores over time
  // Create a blank Google Sheet and paste its URL here
  spreadsheetUrl: 'YOUR_SPREADSHEET_URL',
  
  // Quality score thresholds
  lowQualityScoreThreshold: 5,
  highQualityScoreThreshold: 7,
  
  // Minimum impressions for a keyword to be considered
  minImpressions: 100,
  
  // Campaigns to exclude (exact match)
  excludeCampaigns: ['Brand Campaign'],
  
  // Only include campaigns with these labels (leave empty for all campaigns)
  campaignLabels: [],
  
  // Email notification settings
  sendEmail: true,
  emailAddresses: 'your.email@example.com'
};

// Component score mappings
var COMPONENT_SCORES = {
  '1': 'Below average',
  '2': 'Average',
  '3': 'Above average'
};

function main() {
  // Initialize spreadsheet for tracking
  var spreadsheet = initializeSpreadsheet();
  
  // Get current date for tracking
  var today = new Date();
  var dateString = Utilities.formatDate(today, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
  
  // Get quality score data
  var qualityScoreData = getQualityScoreData();
  
  // Calculate account-level metrics
  var accountMetrics = calculateAccountMetrics(qualityScoreData);
  
  // Record data in spreadsheet
  recordQualityScoreData(spreadsheet, dateString, qualityScoreData, accountMetrics);
  
  // Generate recommendations
  var recommendations = generateRecommendations(qualityScoreData);
  
  // Send email report if enabled
  if (CONFIG.sendEmail) {
    sendEmailReport(accountMetrics, recommendations);
  }
  
  Logger.log('Script completed. Analyzed ' + Object.keys(qualityScoreData).length + ' keywords.');
  Logger.log('Account average quality score: ' + accountMetrics.avgQualityScore.toFixed(2));
}

function initializeSpreadsheet() {
  var spreadsheet = SpreadsheetApp.openByUrl(CONFIG.spreadsheetUrl);
  
  // Check if sheets exist, create if not
  var sheets = {
    'Summary': ['Date', 'Avg QS', 'Low QS %', 'Med QS %', 'High QS %', 'Impressions'],
    'Keywords': ['Date', 'Campaign', 'Ad Group', 'Keyword', 'Match Type', 'Quality Score', 
                'Exp. CTR', 'Ad Relevance', 'Landing Page Exp.', 'Impressions', 'Clicks', 'CTR', 'Avg. CPC'],
    'Recommendations': ['Date', 'Campaign', 'Ad Group', 'Keyword', 'Quality Score', 'Issue', 'Recommendation']
  };
  
  Object.keys(sheets).forEach(sheetName => {
    var sheet = spreadsheet.getSheetByName(sheetName);
    if (!sheet) {
      sheet = spreadsheet.insertSheet(sheetName);
      sheet.appendRow(sheets[sheetName]);
      sheet.getRange(1, 1, 1, sheets[sheetName].length).setFontWeight('bold');
    }
  });
  
  return spreadsheet;
}

function getQualityScoreData() {
  var qualityScoreData = {};
  
  // Build report query
  var report = AdsApp.report(
    'SELECT CampaignName, AdGroupName, Criteria, KeywordMatchType, QualityScore, ' +
    'SearchPredictedCtr, CreativeQualityScore, PostClickQualityScore, ' +
    'Impressions, Clicks, Ctr, AverageCpc ' +
    'FROM KEYWORDS_PERFORMANCE_REPORT ' +
    'WHERE Status = ENABLED ' +
    'AND Impressions > ' + CONFIG.minImpressions + ' ' +
    'DURING LAST_30_DAYS'
  );
  
  // Process report data
  var rows = report.rows();
  while (rows.hasNext()) {
    var row = rows.next();
    var campaignName = row['CampaignName'];
    
    // Skip excluded campaigns
    if (CONFIG.excludeCampaigns.indexOf(campaignName) !== -1) {
      continue;
    }
    
    var keywordText = row['Criteria'];
    var adGroupName = row['AdGroupName'];
    var matchType = row['KeywordMatchType'];
    var qualityScore = parseInt(row['QualityScore'], 10);
    var expectedCtr = row['SearchPredictedCtr'];
    var adRelevance = row['CreativeQualityScore'];
    var landingPageExp = row['PostClickQualityScore'];
    var impressions = parseInt(row['Impressions'], 10);
    var clicks = parseInt(row['Clicks'], 10);
    var ctr = parseFloat(row['Ctr'].replace(/%/g, ''));
    var avgCpc = parseFloat(row['AverageCpc'].replace(/,/g, ''));
    
    // Create unique key for the keyword
    var key = campaignName + '|' + adGroupName + '|' + keywordText + '|' + matchType;
    
    qualityScoreData[key] = {
      campaign: campaignName,
      adGroup: adGroupName,
      keyword: keywordText,
      matchType: matchType,
      qualityScore: qualityScore,
      expectedCtr: expectedCtr,
      adRelevance: adRelevance,
      landingPageExp: landingPageExp,
      impressions: impressions,
      clicks: clicks,
      ctr: ctr,
      avgCpc: avgCpc
    };
  }
  
  return qualityScoreData;
}

function calculateAccountMetrics(qualityScoreData) {
  var totalKeywords = Object.keys(qualityScoreData).length;
  var totalQualityScore = 0;
  var totalImpressions = 0;
  var lowQSCount = 0;
  var medQSCount = 0;
  var highQSCount = 0;
  
  Object.values(qualityScoreData).forEach(data => {
    totalQualityScore += data.qualityScore;
    totalImpressions += data.impressions;
    
    if (data.qualityScore <= CONFIG.lowQualityScoreThreshold) {
      lowQSCount++;
    } else if (data.qualityScore >= CONFIG.highQualityScoreThreshold) {
      highQSCount++;
    } else {
      medQSCount++;
    }
  });
  
  return {
    totalKeywords: totalKeywords,
    avgQualityScore: totalKeywords > 0 ? totalQualityScore / totalKeywords : 0,
    totalImpressions: totalImpressions,
    lowQSPercent: totalKeywords > 0 ? (lowQSCount / totalKeywords) * 100 : 0,
    medQSPercent: totalKeywords > 0 ? (medQSCount / totalKeywords) * 100 : 0,
    highQSPercent: totalKeywords > 0 ? (highQSCount / totalKeywords) * 100 : 0
  };
}

function recordQualityScoreData(spreadsheet, dateString, qualityScoreData, accountMetrics) {
  // Record summary data
  var summarySheet = spreadsheet.getSheetByName('Summary');
  summarySheet.appendRow([
    dateString,
    accountMetrics.avgQualityScore.toFixed(2),
    accountMetrics.lowQSPercent.toFixed(2) + '%',
    accountMetrics.medQSPercent.toFixed(2) + '%',
    accountMetrics.highQSPercent.toFixed(2) + '%',
    accountMetrics.totalImpressions
  ]);
  
  // Record keyword data
  var keywordsSheet = spreadsheet.getSheetByName('Keywords');
  Object.values(qualityScoreData).forEach(data => {
    keywordsSheet.appendRow([
      dateString,
      data.campaign,
      data.adGroup,
      data.keyword,
      data.matchType,
      data.qualityScore,
      data.expectedCtr,
      data.adRelevance,
      data.landingPageExp,
      data.impressions,
      data.clicks,
      data.ctr.toFixed(2) + '%',
      data.avgCpc.toFixed(2)
    ]);
  });
}

function generateRecommendations(qualityScoreData) {
  var recommendations = [];
  
  Object.values(qualityScoreData).forEach(data => {
    // Only generate recommendations for low quality score keywords
    if (data.qualityScore <= CONFIG.lowQualityScoreThreshold) {
      var issues = [];
      var recs = [];
      
      // Check expected CTR
      if (data.expectedCtr === 'Below average') {
        issues.push('Low expected CTR');
        recs.push('Improve ad copy relevance, use more compelling calls-to-action, and consider adding the keyword in the headline');
      }
      
      // Check ad relevance
      if (data.adRelevance === 'Below average') {
        issues.push('Low ad relevance');
        recs.push('Ensure the keyword appears in your ad text, especially in headlines, and align ad messaging with search intent');
      }
      
      // Check landing page experience
      if (data.landingPageExp === 'Below average') {
        issues.push('Poor landing page experience');
        recs.push('Improve page load speed, ensure keyword relevance on landing page, and enhance mobile experience');
      }
      
      // If all components are average or above but QS is still low
      if (issues.length === 0 && data.qualityScore <= 4) {
        issues.push('Generally low quality score despite average components');
        recs.push('Consider restructuring this keyword into a more tightly themed ad group or try different match types');
      }
      
      // Add recommendations if issues were found
      if (issues.length > 0) {
        recommendations.push({
          campaign: data.campaign,
          adGroup: data.adGroup,
          keyword: data.keyword,
          matchType: data.matchType,
          qualityScore: data.qualityScore,
          issues: issues.join('; '),
          recommendations: recs.join('; '),
          impressions: data.impressions
        });
      }
    }
  });
  
  // Sort recommendations by impressions (highest first) to prioritize high-impact keywords
  recommendations.sort((a, b) => b.impressions - a.impressions);
  
  // Record recommendations in spreadsheet
  var spreadsheet = SpreadsheetApp.openByUrl(CONFIG.spreadsheetUrl);
  var recsSheet = spreadsheet.getSheetByName('Recommendations');
  var today = new Date();
  var dateString = Utilities.formatDate(today, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
  
  recommendations.forEach(rec => {
    recsSheet.appendRow([
      dateString,
      rec.campaign,
      rec.adGroup,
      rec.keyword,
      rec.qualityScore,
      rec.issues,
      rec.recommendations
    ]);
  });
  
  return recommendations;
}

function sendEmailReport(accountMetrics, recommendations) {
  var accountName = AdsApp.currentAccount().getName();
  var subject = 'Google Ads Quality Score Report - ' + accountName;
  
  var body = '<h2>Quality Score Report for ' + accountName + '</h2>';
  
  // Account summary
  body += '<h3>Account Summary</h3>';
  body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
  body += '<tr><th>Metric</th><th>Value</th></tr>';
  body += '<tr><td>Average Quality Score</td><td>' + accountMetrics.avgQualityScore.toFixed(2) + '</td></tr>';
  body += '<tr><td>Low QS Keywords (1-5)</td><td>' + accountMetrics.lowQSPercent.toFixed(2) + '%</td></tr>';
  body += '<tr><td>Medium QS Keywords (6-7)</td><td>' + accountMetrics.medQSPercent.toFixed(2) + '%</td></tr>';
  body += '<tr><td>High QS Keywords (8-10)</td><td>' + accountMetrics.highQSPercent.toFixed(2) + '%</td></tr>';
  body += '<tr><td>Total Keywords Analyzed</td><td>' + accountMetrics.totalKeywords + '</td></tr>';
  body += '</table>';
  
  // Top recommendations
  var topRecs = recommendations.slice(0, Math.min(10, recommendations.length));
  
  if (topRecs.length > 0) {
    body += '<h3>Top ' + topRecs.length + ' Recommendations (by Impression Volume)</h3>';
    body += '<table border="1" cellpadding="3" style="border-collapse: collapse;">';
    body += '<tr><th>Campaign</th><th>Ad Group</th><th>Keyword</th><th>QS</th><th>Issues</th><th>Recommendations</th></tr>';
    
    topRecs.forEach(rec => {
      body += '<tr>';
      body += '<td>' + rec.campaign + '</td>';
      body += '<td>' + rec.adGroup + '</td>';
      body += '<td>' + rec.keyword + ' (' + rec.matchType + ')</td>';
      body += '<td>' + rec.qualityScore + '</td>';
      body += '<td>' + rec.issues + '</td>';
      body += '<td>' + rec.recommendations + '</td>';
      body += '</tr>';
    });
    
    body += '</table>';
  } else {
    body += '<p>No recommendations generated. All keywords have quality scores above the threshold.</p>';
  }
  
  // Link to spreadsheet
  body += '<p>View the full report in the <a href="' + CONFIG.spreadsheetUrl + '">Google Sheet</a>.</p>';
  
  MailApp.sendEmail({
    to: CONFIG.emailAddresses,
    subject: subject,
    htmlBody: body
  });
}

How to Implement This Script

  1. Create a new Google Sheet to store your quality score data
  2. Log in to your Google Ads account
  3. Navigate to Tools & Settings > Bulk Actions > Scripts
  4. Click the ”+” button to create a new script
  5. Copy and paste the code above
  6. Update the CONFIG object with your specific settings:
    • Add your Google Sheet URL to spreadsheetUrl
    • Adjust the quality score thresholds if needed
    • Set appropriate minimum impressions threshold
    • Add any campaigns you want to exclude
    • Update the email address for reports
  7. Click “Save” and then “Preview” to test the script
  8. Once you’re satisfied, schedule it to run weekly or monthly

Understanding the Output

The script creates three sheets in your Google Spreadsheet:

  1. Summary: Tracks account-level quality score metrics over time
  2. Keywords: Records detailed quality score data for all keywords
  3. Recommendations: Provides specific improvement suggestions for low-quality keywords

The email report highlights your account’s overall quality score health and provides actionable recommendations for your highest-impression, low-quality keywords.

Performance Impact

Improving quality scores can have a significant impact on your account:

  • Lower CPCs: Higher quality scores can reduce your cost-per-click by 50% or more
  • Better Ad Positions: Improved quality scores help ads appear in higher positions
  • Higher CTR: Addressing quality score issues often leads to higher click-through rates
  • Increased Conversion Rates: Better landing page experience improves conversion rates

Best Practices for Quality Score Improvement

  1. Focus on High-Impact Keywords: Prioritize improvements for keywords with high impression volume
  2. Address All Three Components: Work on expected CTR, ad relevance, and landing page experience
  3. Create Tightly Themed Ad Groups: Group similar keywords together with highly relevant ads
  4. Test Multiple Ad Variations: Create at least 3 ads per ad group to find the highest CTR version
  5. Optimize Landing Pages: Ensure fast load times, mobile-friendliness, and relevant content

Advanced Customization

You can extend this script to:

  • Automatically pause keywords with persistently low quality scores
  • Create new ad variations for ad groups with low expected CTR
  • Generate more specific landing page recommendations based on URL analysis
  • Compare quality scores against competitors using Auction Insights data

Have you struggled with quality score issues in your Google Ads account? What strategies have worked best for you? Let me know in the comments!


Latest Posts

View All Posts