Overview
This workflow generates a weekly SEO report that combines Google Search Console impressions and clicks, GA4 organic traffic and conversions, and live SERP data from Scavio. The combined view reveals correlations between ranking changes and business metrics that are invisible when data lives in separate dashboards.
Trigger
Cron schedule (every Monday at 8:00 AM UTC)
Schedule
Runs every Monday at 8:00 AM UTC
Workflow Steps
Pull GSC data for the past week
Query Google Search Console API for impressions, clicks, CTR, and average position per keyword.
Pull GA4 organic traffic data
Query GA4 Reporting API for organic sessions, bounce rate, and conversions per landing page.
Fetch current SERP positions
Call Scavio for each tracked keyword to get the current position, SERP features, and AI Overview status.
Join data on keyword and URL
Combine GSC, GA4, and SERP data into a unified row per keyword showing the full picture from ranking to conversion.
Generate report with alerts
Flag keywords where drops in position correlate with drops in clicks and sessions. Output as JSON for dashboard or email.
Python Implementation
import requests
import json
from pathlib import Path
from datetime import datetime
API_KEY = "your_scavio_api_key"
DOMAIN = "yourdomain.com"
def get_serp_data(keyword: str) -> dict:
res = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": API_KEY},
json={"platform": "google", "query": keyword, "num": 20, "ai_overview": True},
timeout=15,
)
res.raise_for_status()
data = res.json()
position = None
for item in data.get("organic", []):
if DOMAIN in item.get("link", ""):
position = item.get("position")
break
return {
"serp_position": position,
"has_ai_overview": bool(data.get("ai_overview")),
"has_featured_snippet": bool(data.get("featured_snippet")),
"organic_count": len(data.get("organic", [])),
}
def generate_weekly_report(keywords: list[str], gsc_data: dict, ga4_data: dict) -> dict:
rows = []
for kw in keywords:
serp = get_serp_data(kw)
gsc = gsc_data.get(kw, {})
ga4 = ga4_data.get(kw, {})
row = {
"keyword": kw,
"serp_position": serp["serp_position"],
"has_ai_overview": serp["has_ai_overview"],
"gsc_impressions": gsc.get("impressions", 0),
"gsc_clicks": gsc.get("clicks", 0),
"gsc_ctr": gsc.get("ctr", 0),
"ga4_sessions": ga4.get("sessions", 0),
"ga4_conversions": ga4.get("conversions", 0),
}
# Alert: position dropped AND traffic dropped
row["alert"] = (
serp["serp_position"] is not None
and serp["serp_position"] > 10
and gsc.get("clicks", 0) < gsc.get("prev_clicks", gsc.get("clicks", 0)) * 0.8
)
rows.append(row)
alerts = [r for r in rows if r["alert"]]
date = datetime.utcnow().strftime("%Y-%m-%d")
return {"date": date, "keywords": len(rows), "alerts": len(alerts), "rows": rows}
# GSC and GA4 data would come from their respective APIs
gsc = {"best search API": {"impressions": 2400, "clicks": 180, "ctr": 0.075, "prev_clicks": 200}}
ga4 = {"best search API": {"sessions": 170, "conversions": 12}}
report = generate_weekly_report(["best search API"], gsc, ga4)
print(f"Weekly report: {report['keywords']} keywords, {report['alerts']} alerts")
for row in report["rows"]:
status = "ALERT" if row["alert"] else "OK"
print(f" [{status}] {row['keyword']}: #{row['serp_position']} | {row['gsc_clicks']} clicks | {row['ga4_sessions']} sessions")JavaScript Implementation
const API_KEY = "your_scavio_api_key";
const DOMAIN = "yourdomain.com";
async function getSerpData(keyword) {
const res = await fetch("https://api.scavio.dev/api/v1/search", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify({ platform: "google", query: keyword, num: 20, ai_overview: true }),
});
if (!res.ok) throw new Error(`scavio ${res.status}`);
const data = await res.json();
let position = null;
for (const item of data.organic ?? []) {
if (item.link?.includes(DOMAIN)) { position = item.position; break; }
}
return { serpPosition: position, hasAiOverview: !!data.ai_overview };
}
async function weeklyReport(keywords, gscData, ga4Data) {
const rows = [];
for (const kw of keywords) {
const serp = await getSerpData(kw);
const gsc = gscData[kw] ?? {};
const ga4 = ga4Data[kw] ?? {};
rows.push({ keyword: kw, serpPosition: serp.serpPosition, hasAiOverview: serp.hasAiOverview, gscClicks: gsc.clicks ?? 0, ga4Sessions: ga4.sessions ?? 0, alert: serp.serpPosition > 10 });
}
console.log(`Report: ${rows.length} keywords, ${rows.filter((r) => r.alert).length} alerts`);
for (const r of rows) console.log(` [${r.alert ? "ALERT" : "OK"}] ${r.keyword}: #${r.serpPosition}`);
}
await weeklyReport(["best search API"], { "best search API": { clicks: 180 } }, { "best search API": { sessions: 170 } });Platforms Used
Web search with knowledge graph, PAA, and AI overviews