Core Web Vitals & GEO Audit Pipeline

Audit Core Web Vitals & GEO with Gemini AI and automated Slack alerts.

Introduction

30 Second Summary

Search engines now serve answers directly on the results page. Google's AI Overviews, ChatGPT, and Perplexity pull structured data from websites that load fast and expose clear entities. If your page is slow or invisible to these AI engines, it disappears from both traditional rankings and the new generative results.

In this project, you will build an automated pipeline in Google Apps Script that fetches live Core Web Vitals data, sends it to Gemini AI for a 3-part audit (performance fixes, GEO entity gaps, and conversion readiness), and posts formatted reports to Slack on a daily schedule.

What You'll Build

You'll trigger the pipeline from a Google Sheet and watch a Slack alert arrive within seconds, showing a website's performance score, AI-identified entity gaps for generative search engines, and a ready-to-paste JSON-LD Schema snippet.

By the end of this project, you'll have:

  • A live Core Web Vitals health check where you paste any public URL and see performance, LCP, and CLS scores within seconds.
  • A 3-part AI audit from Gemini that diagnoses technical issues, identifies entity gaps for AI search engines (GEO), generates a JSON-LD Schema snippet, and assesses conversion readiness.
  • Automated Slack alerts that arrive on a daily schedule with formatted diagnostic reports covering all three audit dimensions.
  • Secret Mission: extend to multi-URL batch monitoring to audit an entire client portfolio in one run.

Are there any prerequisites?

You need a Google account and a web browser. No prior experience with Google Apps Script is required. This project walks you through every line of code.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up APIs and Slack Webhook

Your monitoring pipeline depends on three external services: the PageSpeed Insights API for performance data, the Gemini API for AI interpretation, and Slack for delivering alerts.

Without proper credentials, Google blocks your requests after a handful of calls, and your script has nowhere to send alerts. This step gets all three keys in place so every later step can focus on building logic, not fighting authentication errors.

In this step, get ready to:
  • Create a Google Cloud project with the PageSpeed Insights API enabled and generate an API key.
  • Get a Gemini API key from Google AI Studio on the free tier.
  • Create a free Slack workspace with an Incoming Webhook URL for automated alerts.
Create a Google Cloud project and enable the PageSpeed Insights API

The PageSpeed Insights API is Google's service for measuring website performance. To call it reliably, you need an API key tied to a Google Cloud project.

  • Open your browser and go to console.cloud.google.com.
  • Sign in with your Google account.
  • At the top of the page, click the project dropdown (it may say Select a project).
  • Click New Project.
  • Enter SEO-Pulse as the project name.
  • Click Create.
  • Wait a few seconds for the project to be created, then select it from the project dropdown so it becomes your active project.

Now enable the PageSpeed Insights API for this project.

  • In the left navigation menu, click APIs & Services, then click Library.
  • In the search bar, type PageSpeed Insights API and press Enter.
  • Click the PageSpeed Insights API result.
  • Click Enable.

With the API enabled, generate a key so your script can authenticate.

  • In the left navigation menu, click APIs & Services, then click Credentials.
  • Click Create Credentials at the top, then select API key.
  • A dialog appears showing your new API key. Copy it immediately and paste it somewhere safe (a temporary text file or notes app).
  • Click Close.

Why copy the key now?

You will paste this key into your Google Apps Script code in a later step. Storing it in a safe temporary location means you will not need to navigate back to the Credentials page later.

Test your key to confirm it works. Paste the following URL into your browser address bar, replacing your-api-key-here with the key you just copied:

https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://web.dev/&strategy=mobile&category=performance&key=your-api-key-here

Before you press Enter, what do you expect to see? A wall of JSON data, or an error?

  • Press Enter. You should see a large JSON response in the browser containing fields like lighthouseResult and loadingExperience. That confirms your API key is working.

Seeing an error instead of JSON?

If you see a message about the API not being enabled, go back to APIs & Services > Library and confirm the PageSpeed Insights API shows Enabled for your project.

If you see an invalid key error, double-check you copied the entire key string without extra spaces.

Help me troubleshoot my PageSpeed Insights API key error in Google Cloud Console.

Get a Gemini API key from Google AI Studio

The Gemini API will interpret your raw performance numbers and generate plain-language fix recommendations. Google AI Studio lets you create a free-tier key with no billing required.

  • Open a new browser tab and go to aistudio.google.com.
  • Sign in with the same Google account you used for Google Cloud.
  • If this is your first time, accept the Terms of Service when prompted. Google AI Studio will automatically create a default project for you.
  • In the left panel, click Get API key (or navigate to the API Keys page from the dashboard).
  • Click Create API key.
  • Select the project to associate the key with (your default project is fine).
  • Copy the generated API key and save it in the same safe location as your PageSpeed key.

Will this cost me anything?

No. New API keys start on the Free Tier by default. Charges only begin after you explicitly click Set up billing in Google AI Studio. You will not need billing for this project.

You now have two API keys stored safely. The Gemini key will be tested in Step 3 when you write the AI diagnostic function.

Create a Slack workspace and generate a webhook URL

Slack is where your pipeline will deliver formatted diagnostic reports. An Incoming Webhook gives your script a unique URL it can POST messages to.

First, create a free Slack workspace (skip this if you already have one you can use).

  • Open a new browser tab and go to https://slack.com/get-started#/createnew.
  • Enter your email address and click Continue.
  • Check your email for a confirmation code, enter it, then follow the prompts to name your workspace (e.g. SEO Monitoring).
  • When prompted to invite team members, skip this step for now.
  • Create a channel called #cwv-alerts where your automated reports will be posted.

Now create a Slack App with Incoming Webhooks enabled.

  • Open a new browser tab and go to https://api.slack.com/apps.
  • Click Create an App.
  • Select From scratch.
  • Enter a name for your app (e.g. SEO-Pulse Bot) and select the workspace you just created.
  • Click Create App.
  • In the left sidebar of your app's settings page, click Incoming Webhooks.
  • Toggle Activate Incoming Webhooks to the On position.
  • Scroll down to the Webhook URLs for Your Workspace section and click Add New Webhook to Workspace.
  • Select the #cwv-alerts channel you created earlier.
  • Click Allow.
  • You will be redirected back to the webhooks page. Copy the generated webhook URL (it starts with https://hooks.slack.com/services/) and save it alongside your other keys.

What is an Incoming Webhook?

An Incoming Webhook is a unique URL that accepts HTTP POST requests containing a JSON payload. When your script sends data to this URL, Slack automatically converts it into a formatted message in your chosen channel.

Test the webhook to confirm messages arrive. Open a terminal and run this command, replacing the URL with your actual webhook URL:

curl -X POST -H "Content-Type: application/json" -d '{"text": "SEO-Pulse test: webhook is working!"}' https://hooks.slack.com/services/your-webhook-url-here

What does this command do?

  • curl sends an HTTP request from your terminal.
  • -X POST specifies this is a POST request (sending data, not retrieving it).
  • -H "Content-Type: application/json" tells Slack the data format is JSON.
  • -d '{"text": "..."}' is the JSON payload containing your message text.

Before you press Enter, do you think the message will appear in your Slack channel immediately, or will there be a delay?

  • Press Enter. Switch to your Slack workspace and open the #cwv-alerts channel. You should see the message SEO-Pulse test: webhook is working! appear within seconds.

Message not appearing in Slack?

Make sure you replaced the full URL in the command with your actual webhook URL (the one starting with https://hooks.slack.com/services/).

Check that you selected the #cwv-alerts channel when authorizing the webhook. If you picked a different channel, look there instead.

On Windows, if curl is not recognized, open PowerShell instead of Command Prompt and try the same command.

Help me troubleshoot my Slack webhook not delivering messages.

All three credentials are in place. Next up, you will create a Google Sheet and write the script that calls the PageSpeed Insights API to fetch live Core Web Vitals data.

Fetch Core Web Vitals into Google Sheets

Your API keys and webhook URL are locked in. You have everything you need to start pulling real data.

Raw performance data is the foundation of any SEO audit. In this step, you will write a script that calls the same PageSpeed Insights API that Google uses to rank websites, extracts the Core Web Vitals scores, and writes them into a Google Sheet. This replaces manual PageSpeed checks that take 2-3 minutes each.

In this step, get ready to:
  • Create a Google Sheet and connect it to the Apps Script editor.
  • Write a function that fetches live Core Web Vitals from the PageSpeed Insights API.
  • Display the fetched metrics in your sheet with a timestamp.
Create a Google Sheet and open the Apps Script editor

Every Apps Script project lives inside a Google Sheets file. You will create the spreadsheet first, then open the built-in code editor.

  • Go to sheets.google.com and click the Blank spreadsheet card to create a new sheet.
  • Click the spreadsheet title at the top left (it defaults to Untitled spreadsheet) and rename it to SEO-Agentic-Pulse.
  • Click Extensions in the top menu bar.
  • Click Apps Script. A new tab opens with the Apps Script editor.
  • In the Apps Script editor, click Untitled project at the top left and rename it to SEO-Agentic-Pulse. Click Rename.
  • Select all the placeholder code in the Code.gs file and delete it.

You now have an empty script editor connected to your spreadsheet.

What is Google Apps Script?

Google Apps Script is a JavaScript-based platform that runs in the cloud and integrates directly with Google Sheets, Gmail, and other Google services. Because the script is "bound" to your spreadsheet, it can read and write cells without any extra authentication setup.

  • Paste the following configuration block into the empty Code.gs file:
// ============================================================
// SEO-Agentic-Pulse: Core Web Vitals Monitor & AI Diagnostic Pipeline
// ============================================================

// --- Configuration ---
var CONFIG = {
  PSI_API_KEY: "your-pagespeed-api-key-here",
  GEMINI_API_KEY: "your-gemini-api-key-here",
  SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/your-webhook-url-here",
  TARGET_URL: "https://www.example.com",
  SHEET_NAME: "CWV_Data"
};
  • Replace your-pagespeed-api-key-here with the PageSpeed Insights API key you generated in Step 1.
  • Replace your-gemini-api-key-here with the Gemini API key from Google AI Studio.
  • Replace https://hooks.slack.com/services/your-webhook-url-here with the Slack webhook URL you created in Step 1.

What does this code do?

The CONFIG object stores all the credentials and settings your pipeline needs in one place. TARGET_URL is the website you will audit. SHEET_NAME is the tab name where results will be written.

  • Below the CONFIG block, paste the main pipeline function:
// --- Main Pipeline ---
function runSEOPulse() {
  try {
    var metrics = fetchCoreWebVitals(CONFIG.TARGET_URL);
    var rowNumber = writeMetricsToSheet(metrics);
    var diagnosis = getGeminiDiagnosis(metrics);
    writeDiagnosisToSheet(diagnosis, rowNumber);
    sendSlackAlert(metrics, diagnosis);
  } catch (error) {
    Logger.log("Pipeline error: " + error.message);
  }
}

What does this code do?

The runSEOPulse() function is the master controller. It calls each stage of the pipeline in sequence: fetch metrics, write them, get an AI diagnosis, write that, and send a Slack alert. The try/catch block ensures any error is logged cleanly instead of crashing silently.

The helper functions it calls do not exist yet. You will build them next.

  • Press Ctrl+S (Windows) or Cmd+S (macOS) to save the project.

The editor should show no red error indicators. Your project structure is set up.

Seeing a red error in the editor?

Check that your API key strings are wrapped in double quotes and that no trailing comma appears after the last property in CONFIG. Also confirm the closing }; is present at the end of the CONFIG object.

help me fix a syntax error in my Apps Script CONFIG object

Write the function to fetch Core Web Vitals

The PageSpeed Insights API accepts any public URL and returns a full Lighthouse audit. Your function will call this API, check the response status, and extract the key performance metrics.

  • Below the runSEOPulse() function, paste the following code:
// --- Step 2: Fetch Core Web Vitals ---
function fetchCoreWebVitals(targetUrl) {
  var apiUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
    + "?url=" + encodeURIComponent(targetUrl)
    + "&strategy=mobile"
    + "&category=performance"
    + "&key=" + CONFIG.PSI_API_KEY;

  var response = UrlFetchApp.fetch(apiUrl, { muteHttpExceptions: true });
  var statusCode = response.getResponseCode();

  if (statusCode !== 200) {
    throw new Error("PageSpeed API returned status " + statusCode);
  }

  var json = JSON.parse(response.getContentText());
  var audits = json.lighthouseResult.audits;
  var performanceScore = json.lighthouseResult.categories.performance.score;

  var metrics = {
    url: targetUrl,
    performanceScore: Math.round(performanceScore * 100),
    lcp: audits["largest-contentful-paint"].displayValue,
    lcpNumeric: audits["largest-contentful-paint"].numericValue,
    cls: audits["cumulative-layout-shift"].displayValue,
    clsNumeric: audits["cumulative-layout-shift"].numericValue,
    timestamp: new Date().toISOString()
  };

  Logger.log("Fetched metrics for: " + targetUrl);
  return metrics;
}

What does this code do?

  • It builds a request URL targeting the PageSpeed Insights v5 endpoint with strategy=mobile and category=performance.
  • UrlFetchApp.fetch() makes the HTTP GET request. muteHttpExceptions: true prevents Apps Script from throwing on non-200 responses so you can handle errors yourself.
  • It parses the JSON response and extracts the overall performance score (a 0-1 float multiplied by 100), LCP (Largest Contentful Paint), and CLS (Cumulative Layout Shift) values.
  • The function returns a tidy metrics object that the rest of the pipeline can consume.

💡 What are LCP and CLS?

LCP measures how long it takes for the largest visible element (hero image, heading) to render. Google considers under 2.5 seconds "good". CLS measures how much page elements shift around while loading. Under 0.1 is "good". These are two of the three Core Web Vitals that directly affect search ranking.

Write the metrics to your sheet and run the pipeline

The fetch function now returns structured data. You need a function that receives that data and writes it to the next available row in your Google Sheet.

  • Below the fetchCoreWebVitals() function, paste the following code:
// --- Step 2: Write Metrics to Sheet ---
function writeMetricsToSheet(metrics) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);

  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.SHEET_NAME);
    sheet.appendRow(["Timestamp", "URL", "Performance Score", "LCP", "CLS", "Diagnosis"]);
  }

  sheet.appendRow([
    metrics.timestamp,
    metrics.url,
    metrics.performanceScore,
    metrics.lcp,
    metrics.cls,
    ""
  ]);

  return sheet.getLastRow();
}

What does this code do?

  • It gets the active spreadsheet and looks for a tab named CWV_Data.
  • If that tab does not exist yet, it creates one and writes the header row automatically.
  • It appends a new row with the timestamp, URL, performance score, LCP, and CLS. The Diagnosis column is left blank for now.
  • It returns the row number so later functions know exactly where to write the AI diagnosis.
  • Press Ctrl+S (Windows) or Cmd+S (macOS) to save the project.

Before you run the script, think about what you expect to happen. The pipeline calls fetchCoreWebVitals and writeMetricsToSheet (which both exist), then tries to call getGeminiDiagnosis (which does not exist yet). What do you think will happen?

  • In the Apps Script editor, select runSEOPulse from the function dropdown at the top of the editor.
  • Click the Run button (the play icon).

Authorization prompt on first run

Google Apps Script will show an authorization dialog the first time you run a script. It asks permission to access your spreadsheet and make external network requests.

  • Click Review permissions.
  • Select your Google account.
  • If you see a "This app isn't verified" warning, click Advanced, then click Go to SEO-Agentic-Pulse (unsafe). This is safe because you wrote the script yourself.
  • Click Allow to grant permissions.

You only need to authorize once. Future runs skip this dialog.

The script takes about 10-15 seconds to complete. It fetches a live Lighthouse audit from Google's servers, so network latency is normal.

  • Switch back to your SEO-Agentic-Pulse spreadsheet tab in your browser.

You should see a new tab at the bottom named CWV_Data. Click it. A header row and one data row are now visible, showing a timestamp, URL, performance score (a number out of 100), LCP value, and CLS value.

What about the getGeminiDiagnosis error?

The try/catch in runSEOPulse() caught the error when it tried to call getGeminiDiagnosis (which does not exist yet) and logged it quietly. The pipeline still successfully fetched the metrics and wrote them to the sheet before the error occurred. You will build the AI diagnosis function in the next step.

🙋‍♀️ No CWV_Data tab appeared?

  • Open the Apps Script editor and click Executions in the left sidebar. Check if the run shows an error. Common causes: an invalid API key (status 400 or 403), or a typo in CONFIG.PSI_API_KEY.
  • Make sure you selected runSEOPulse from the function dropdown before clicking Run. Running the wrong function produces no sheet output.
  • Confirm the TARGET_URL in your CONFIG is a valid public URL (including https://).

help me debug why my runSEOPulse function did not write data to the sheet

✔️ Awesome, I've got everything!

Great. Double check you have saved Code.gs and that your CWV_Data tab shows one data row with real metrics.

ⓧ I'd like to double check the full code

Your Code.gs should contain the following code. Scroll through and confirm each section matches:

  • The comment header and CONFIG object (with your real API keys filled in).
  • The runSEOPulse() function.
  • The fetchCoreWebVitals() function.
  • The writeMetricsToSheet() function.

Scroll up through this step and confirm each code block you pasted matches the blocks above. If anything is missing, re-paste that section and save again.

You just automated what takes 2-3 minutes to do manually. Every time this script runs, it pulls fresh performance data from Google's servers and logs it in your sheet. Next up, you will send those raw numbers to Gemini AI and get back plain-language fix recommendations.

Add AI Diagnostics with Gemini

Build the 3-Part AI Audit with Gemini

In the last step, you proved that the pipeline can pull live Core Web Vitals data into a sheet automatically. But raw numbers like a performance score of 45 or an LCP of 4.2s do not tell a client what to fix.

In this step, you will connect the Gemini AI API to perform a 3-part audit: technical CWV fixes for developers, Generative Engine Optimization (GEO) entity analysis with a JSON-LD Schema snippet, and conversion readiness for paid media teams.

In this step, get ready to:
  • Write a structured 3-part prompt that instructs Gemini to produce technical fixes, GEO entity gaps with JSON-LD, and conversion recommendations.
  • Send the prompt to the Gemini API and parse the structured response.
  • Write the full AI audit to the Diagnosis column in the same row as the metrics.
Build a 3-part structured prompt for Gemini

The quality of an AI response depends on the quality of the prompt. By injecting exact metric values and specifying a 3-part response format (Technical, GEO, Conversion), you get structured recommendations that serve developers, SEO specialists, and marketing teams simultaneously.

  • In the Apps Script editor, scroll to the bottom of your Code.gs file (below the closing } of writeMetricsToSheet).
  • Add the first part of the getGeminiDiagnosis function by pasting this code:
// --- Step 3: Get 3-Part AI Audit from Gemini ---
function getGeminiDiagnosis(metrics) {
  var prompt = "You are a Senior Technical SEO and GEO Specialist. "
    + "Analyze these metrics and provide a 3-part audit.\n\n"
    + "URL: " + metrics.url + "\n"
    + "Performance Score: " + metrics.performanceScore + "/100\n"
    + "LCP: " + metrics.lcp + " (good: under 2.5s)\n"
    + "CLS: " + metrics.cls + " (good: under 0.1)\n\n"
    + "PART 1 - TECHNICAL CWV: Give 2 specific performance fixes.\n"
    + "PART 2 - GEO & SCHEMA: List 2 entities that AI search engines "
    + "(Google AI Overviews, ChatGPT, Perplexity) cannot extract from "
    + "this page. Then generate a valid JSON-LD Schema snippet.\n"
    + "PART 3 - CONVERSION: Assess CTA clarity for paid media. "
    + "Give 1 recommendation.\n\n"
    + "Respond in this exact format:\n"
    + "STATUS: [PASS/NEEDS WORK/CRITICAL]\n"
    + "--- PART 1: TECHNICAL ---\n"
    + "FIX 1: [action]\nFIX 2: [action]\n"
    + "--- PART 2: GEO & SCHEMA ---\n"
    + "ENTITY GAP 1: [entity]\nENTITY GAP 2: [entity]\n"
    + "JSON-LD: [valid JSON-LD snippet]\n"
    + "--- PART 3: CONVERSION ---\n"
    + "CTA: [recommendation]";
  var apiUrl = "https://generativelanguage.googleapis.com/v1beta/"
    + "models/gemini-2.5-flash:generateContent?key=" + CONFIG.GEMINI_API_KEY;
  var payload = {
    contents: [{ parts: [{ text: prompt }] }]
  };

What does this code do?

  • The prompt variable injects the real metric values and instructs Gemini to return a 3-part audit: technical fixes, GEO entity gaps with a JSON-LD snippet, and conversion readiness.
  • The apiUrl points to the Gemini 2.5 Flash generateContent endpoint with your API key appended as a query parameter.
  • The payload wraps the prompt in the JSON structure the Gemini API expects.

💡 Why a 3-part structured format?

Without explicit format instructions, Gemini returns free-form paragraphs that are harder to parse and display. By specifying three labelled sections (TECHNICAL, GEO & SCHEMA, CONVERSION), you get a consistent structure every time that reads cleanly in the sheet, in Slack, and in a portfolio presentation.

The GEO section specifically targets how AI search engines (Google AI Overviews, ChatGPT, Perplexity) consume page content. Identifying missing entities shows a client exactly what structured data to add so their pages appear in generative search results.

Your function is not complete yet. You will add the code that sends the request and parses the response next.

Send the request and parse Gemini's response
  • Still inside the getGeminiDiagnosis function, paste this code directly below the payload variable (after the closing }; of the payload object):
  var options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  var response = UrlFetchApp.fetch(apiUrl, options);
  var statusCode = response.getResponseCode();

  if (statusCode !== 200) {
    throw new Error("Gemini API returned status " + statusCode + ": " + response.getContentText());
  }

  var json = JSON.parse(response.getContentText());
  var diagnosis = json.candidates[0].content.parts[0].text;

  Logger.log("Gemini diagnosis received");
  return diagnosis;
}

What does this code do?

  • The options object configures the HTTP POST request with the correct content type and stringified payload.
  • UrlFetchApp.fetch sends the request to the Gemini API. If the API returns anything other than a 200 status, the function throws an error with the full response text for debugging.
  • The response JSON is parsed and the diagnosis text is extracted from json.candidates[0].content.parts[0].text which is the standard response path for the Gemini generateContent endpoint.
  • Save your file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Select runSEOPulse from the function dropdown at the top of the editor.
  • Click the Run button (the play icon).
  • After the run completes, click Execution log at the top of the editor to view the log output.

You should see Gemini diagnosis received in the log output. You will also see an error mentioning writeDiagnosisToSheet because that function does not exist yet. That is expected.

Not seeing the Gemini log message?

  • If you see a status 400 error, check that CONFIG.GEMINI_API_KEY contains your actual Gemini API key from Google AI Studio, not the placeholder text.
  • If you see a status 404 error, verify the model name in apiUrl is exactly gemini-2.5-flash with no typos.
  • If you see a syntax error, make sure the closing } of the function is on its own line after return diagnosis; and that there are no missing brackets.

Help me debug my Gemini API call in Google Apps Script.

Write the AI diagnostic to your sheet

The Gemini response is now returned by getGeminiDiagnosis, but it only lives in memory. You need a function to write it into the Diagnosis column of the same row where the metrics landed.

  • Below the closing } of getGeminiDiagnosis, paste this new function:
// --- Step 3: Write Diagnosis to Sheet ---
function writeDiagnosisToSheet(diagnosis, rowNumber) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  sheet.getRange(rowNumber, 6).setValue(diagnosis);
}

What does this code do?

  • getRange(rowNumber, 6) targets column 6 (the Diagnosis column) in the exact row where writeMetricsToSheet just wrote the metrics.
  • setValue(diagnosis) pastes the full AI diagnostic text into that cell.
  • Save Code.gs by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Before you run this, what do you expect to see in the Diagnosis column of your sheet?

  • Select runSEOPulse from the function dropdown and click Run.
  • Switch back to your Google Sheet and look at the latest row in the CWV_Data tab.

The Diagnosis column now contains a multi-section AI audit with a STATUS line, two technical fixes, two GEO entity gaps, a JSON-LD Schema snippet, and a conversion recommendation. For example, you might see "ENTITY GAP 1: Organization schema missing" and a ready-to-paste JSON-LD block.

Diagnosis column still empty?

  • Check the Execution log for errors. If you see a TypeError about getRange, confirm that writeDiagnosisToSheet is outside the getGeminiDiagnosis function (not nested inside it).
  • If the cell shows the text undefined, the Gemini response was not parsed correctly. Verify that json.candidates[0].content.parts[0].text matches your code exactly.
  • Make sure you are looking at the newest row at the bottom of the sheet, not the row from your previous test.

Help me figure out why my Diagnosis column is empty after running the pipeline.

✔️ Awesome, I've got everything!

Great. Double check you have saved Code.gs and that your Diagnosis column is populated with AI-generated text.

ⓧ I'd like to double check the full code

Your Code.gs file is now too long to show in one block. Scroll through your file and confirm these sections appear in this order:

  • CONFIG object at the top with all five keys.
  • runSEOPulse() function with calls to fetchCoreWebVitals, writeMetricsToSheet, getGeminiDiagnosis, writeDiagnosisToSheet, and sendSlackAlert.
  • fetchCoreWebVitals() function that calls the PageSpeed Insights API.
  • writeMetricsToSheet() function that appends a row to the sheet.
  • getGeminiDiagnosis() function that builds the prompt, calls the Gemini API, and returns the diagnosis text.
  • writeDiagnosisToSheet() function that writes the diagnosis to column 6.

Your pipeline now fetches live performance data and gets AI to produce a full 3-part audit covering technical SEO, GEO entity visibility, and conversion readiness. Next up, you will push these diagnostics directly into Slack so your team sees them without opening the sheet.

Send Slack Alerts and Schedule Automation

Your pipeline now fetches live Core Web Vitals data and sends it to Gemini for interpretation. The AI diagnostic lands in your spreadsheet.

The problem is that nobody proactively checks spreadsheets. In a real agency, a report sitting in a tab is invisible until someone remembers to open it. In this step, you will push the diagnostic report to Slack the moment it generates, and schedule the entire pipeline to run daily without manual intervention.

In this step, get ready to:
  • Write a sendSlackAlert() function that formats CWV metrics into a Slack Block Kit message.
  • Run the full pipeline and verify a formatted alert appears in your Slack channel.
  • Add a time-based trigger to automate the pipeline on a daily schedule.
Format the CWV scores into a Slack Block Kit payload

Slack's Block Kit lets you send richly formatted messages with headers, fields, and code blocks. You will build a message object that shows the URL, performance score with a status emoji, LCP, CLS, and the full AI diagnosis.

  • In the Apps Script editor, scroll to the bottom of Code.gs (below the writeDiagnosisToSheet function).
  • Add the sendSlackAlert function by pasting this code:
// --- Step 4: Send Slack Alert ---
function sendSlackAlert(metrics, diagnosis) {
  // Choose an emoji based on score thresholds
  var statusEmoji = metrics.performanceScore >= 90 ? "white_check_mark" :
                    metrics.performanceScore >= 50 ? "warning" : "red_circle";

  // Build the Slack Block Kit message payload
  var message = {
    text: "SEO-Agentic-Pulse: CWV Report for " + metrics.url,
    blocks: [
      {
        type: "header",
        text: { type: "plain_text", text: "Core Web Vitals Report" }
      },
      {
        type: "section",
        fields: [
          { type: "mrkdwn", text: "*URL:*\n" + metrics.url },
          { type: "mrkdwn", text: "*Score:*\n:" + statusEmoji + ": " + metrics.performanceScore + "/100" },
          { type: "mrkdwn", text: "*LCP:*\n" + metrics.lcp },
          { type: "mrkdwn", text: "*CLS:*\n" + metrics.cls }
        ]
      },
      {
        type: "section",
        text: { type: "mrkdwn", text: "*AI Diagnosis:*\n```" + diagnosis + "```" }
      }
    ]
  };

What does this code do?

The statusEmoji variable picks a Slack emoji name based on the performance score: a green checkmark for 90+, a warning triangle for 50-89, and a red circle for anything below 50.

The message object uses Slack's Block Kit format. The text field is a fallback for notifications. The blocks array structures the visible message into a header, a section with metric fields, and a section showing the AI diagnosis wrapped in a code block.

Post the payload to your Slack webhook

The message payload is ready. Now you need to send it as a POST request to your Slack webhook URL using UrlFetchApp.

  • Immediately below the message object (still inside the sendSlackAlert function), add the POST request code:
  // Send the message to Slack via POST
  var options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(message),
    muteHttpExceptions: true
  };

  var response = UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, options);

  if (response.getResponseCode() !== 200) {
    throw new Error("Slack webhook returned status " + response.getResponseCode());
  }

  Logger.log("Slack alert sent successfully");
}

What does this code do?

The options object configures a POST request with a JSON content type. JSON.stringify(message) serialises the Block Kit payload into a string that Slack can parse.

If Slack returns anything other than a 200 status code, the function throws an error. The try/catch in runSEOPulse catches it and logs the failure so the rest of the pipeline remains intact.

  • Save Code.gs by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Before you run the full pipeline, think about what you expect to happen. The runSEOPulse function already calls sendSlackAlert(metrics, diagnosis). Do you think you will see a message in Slack, or will it fail?

  • In the function dropdown at the top of the editor, select runSEOPulse.
  • Click Run.
  • If prompted, authorise the script again (Slack requires the external URL fetch permission).
  • Switch to your Slack channel.

You should see a formatted message with the header "Core Web Vitals Report", the target URL, a score with a status emoji, LCP and CLS values, and the AI diagnosis wrapped in a code block.

No message in Slack?

  • Check the Execution log in the Apps Script editor for errors. If you see "Slack webhook returned status 403" or "400", your webhook URL may be incorrect.
  • Confirm the SLACK_WEBHOOK_URL in your CONFIG object starts with https://hooks.slack.com/services/ and is the full URL you copied when creating the webhook.
  • Make sure the Slack App is still installed in your workspace and the Incoming Webhooks toggle is still on.

Help me troubleshoot why my Slack webhook is not receiving messages from Google Apps Script.

Schedule the pipeline with a time-based trigger

Right now the pipeline only runs when you manually click Run. A time-based trigger tells Apps Script to execute runSEOPulse on a schedule you choose, even when the spreadsheet is closed.

  • In the Apps Script editor, click the Triggers icon (the clock symbol) in the left sidebar.
  • Click the + Add Trigger button in the bottom-right corner.
  • In the Choose which function to run dropdown, select runSEOPulse.
  • In the Choose which deployment should run dropdown, select Head.
  • In the Select event source dropdown, choose Time-driven.
  • In the Select type of time based trigger dropdown, choose Day timer.
  • In the Select time of day dropdown, pick any time band that works for you (for example, 9am to 10am).
  • Click Save.

Why is the time a range instead of exact?

Apps Script randomises the exact execution time within the hour you select. If you pick 9am to 10am, the trigger fires once somewhere in that window and keeps the same approximate time each day. This prevents millions of scripts from hitting Google's servers at exactly the same second.

You should now see your new trigger listed on the Triggers page with the function name runSEOPulse, the event type Time-driven, and the schedule you chose.

Trigger failed to save?

  • If you see an authorisation popup, click through and grant the permissions. Apps Script needs your approval to run on a schedule.
  • If the save button is greyed out, make sure you selected a function in the first dropdown. Triggers without a function cannot be saved.

Help me fix my Apps Script time-based trigger that won't save.

✔️ Awesome, I've got everything!

Great. Double check you have saved Code.gs and that your trigger is visible on the Triggers page.

ⓧ I'd like to double check the full code

Your Code.gs file is now longer than 30 lines, so let's confirm each section is in place. Scroll through your file and verify you have these sections in order:

  • The CONFIG object at the top with all five keys.
  • The runSEOPulse() function calling fetchCoreWebVitals, writeMetricsToSheet, getGeminiDiagnosis, writeDiagnosisToSheet, and sendSlackAlert.
  • The fetchCoreWebVitals() function that calls the PageSpeed Insights API.
  • The writeMetricsToSheet() function that appends a row.
  • The getGeminiDiagnosis() function that calls the Gemini API.
  • The writeDiagnosisToSheet() function that writes to column 6.
  • The sendSlackAlert() function you just added, with the message payload and POST request.

If any section is missing, scroll back up in this step and copy the relevant code block.

Your monitoring pipeline is now fully autonomous. It fetches live performance data, interprets it with AI, posts a formatted diagnostic to Slack, and runs every day without you lifting a finger. Next up, you will extend this to monitor multiple URLs at once in the Secret Mission.

Secret mission

Multi-URL Portfolio Monitoring

Your single-URL monitor is great for one site. But real agencies manage 10-50 client websites at once. In this secret mission, you'll extend your pipeline to audit an entire portfolio in one run, with conditional Slack alerts only for sites that are underperforming.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on free-tier services with no ongoing costs.

Resources you used:

  • Google Cloud project with PageSpeed Insights API enabled (free, no charges).
  • Gemini API key on free tier (no billing linked).
  • Slack free workspace with 1 app installed (Incoming Webhooks).
  • Google Apps Script time-based trigger (runs within free quota).
  • Google Sheet with CWV_Data and URLs tabs.

Keep everything running

No action needed. Choose this if you plan to keep monitoring your URLs or want to extend the project further.

  • Your daily trigger continues to run runSEOPulse() on schedule. All services are free tier with no charges.
  • The Slack app remains installed. Slack free workspaces allow up to 10 apps.
  • Your Google Cloud project stays active with the PageSpeed Insights API enabled within free quota.

Pause - I'll come back to this later

Shut down the scheduled trigger to stop automated runs. Your code and data remain intact for when you return.

  • Open your Apps Script editor from the Google Sheet by clicking Extensions, then Apps Script.
  • Click the clock icon on the left sidebar to open Triggers.
  • Find the runSEOPulse trigger, click the three-dot menu on the right, and select Delete trigger.
  • Your code, sheet data, API keys, and Slack app all remain intact. Re-add the trigger when you want to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each item below to fully clean up.

  • Delete the Apps Script trigger: Open the Apps Script editor, click the clock icon to open Triggers, then delete the runSEOPulse trigger.
  • Delete the Gemini API key: Open the Google Cloud API Credentials page. Find the API key you created for this project, click the Actions icon, and select Delete API key. Confirm by clicking Delete in the modal.
  • Delete the Google Cloud project: Go to the Manage resources page in the Google Cloud Console. Select your project, click Delete, enter the project ID, and click Shut down.
  • Remove the Slack app: In Slack, click your workspace name in the sidebar. Select Tools & settings, then click Manage apps. Select Installed Apps from the left sidebar. Find your app, click it, then click the Configuration tab. Scroll down to Remove App and confirm removal.
  • Delete the Google Sheet: Right-click the Google Sheet file in Google Drive and select Move to trash. This removes the sheet, the Apps Script code, and all stored CWV data.

Nice Work!

Nice Work!

You just built a production-ready SEO performance monitoring pipeline that fetches live Core Web Vitals data, interprets it with AI, and pushes diagnostic reports to Slack on autopilot.

Here's what you learned:

  • Connected to the PageSpeed Insights API to fetch real Core Web Vitals data (LCP, CLS, performance score) that Google uses to rank websites in search results.
  • Integrated the Gemini AI API to transform raw metrics into structured, plain-language diagnostic recommendations with specific fix actions.
  • Built an automated Slack alert system with scheduled triggers that push formatted CWV reports to your team daily without manual intervention.
  • Secret Mission: Extended the pipeline to multi-URL batch monitoring with conditional alerting, rate-limit handling, and summary reporting for portfolio-scale SEO management.

Ready to quiz yourself?