Overview
This workflow tracks up to 200 target keywords every week, records your domain's position in Google search results, diffs against the previous week's positions, and fires alerts when rankings drop beyond a configurable threshold. It produces a weekly report with gainers, losers, and stable keywords, feeding directly into SEO dashboards or Slack digests.
Trigger
Cron schedule (every Monday at 7 AM UTC)
Schedule
Runs every Monday at 7 AM UTC
Workflow Steps
Load keyword list
Read the target keywords and domain from a configuration file.
Query Google for each keyword
Call the Scavio API with platform google for each keyword, requesting 30 results to cover page one and two.
Extract domain positions
Scan organic results for each keyword and record the position where the target domain appears.
Diff against previous week
Load last week's positions and compute deltas for each keyword.
Categorize changes
Sort keywords into gainers, losers, stable, and not-found buckets based on threshold.
Generate report and alert
Build a structured report, save to disk, and send alert for any significant drops.
Python Implementation
import requests
import json
import time
from pathlib import Path
from datetime import datetime
API_KEY = "your_scavio_api_key"
DOMAIN = "yourdomain.com"
DROP_THRESHOLD = 3
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
def get_position(keyword: str) -> int | None:
res = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": API_KEY},
json={"platform": "google", "query": keyword, "num": 30},
timeout=15,
)
res.raise_for_status()
for item in res.json().get("organic", []):
if DOMAIN in item.get("link", ""):
return item["position"]
return None
def run():
keywords = json.loads(Path("keywords.json").read_text())
history_path = Path("rank_history.json")
previous = json.loads(history_path.read_text()) if history_path.exists() else {}
current_positions = {}
gainers = []
losers = []
stable = []
for i, kw in enumerate(keywords):
pos = get_position(kw)
current_positions[kw] = pos
prev = previous.get(kw)
if pos and prev:
delta = prev - pos # positive = improved
if delta >= DROP_THRESHOLD:
gainers.append({"keyword": kw, "from": prev, "to": pos, "change": delta})
elif delta <= -DROP_THRESHOLD:
losers.append({"keyword": kw, "from": prev, "to": pos, "change": delta})
else:
stable.append(kw)
if i % 20 == 19:
time.sleep(1)
# Save current as new baseline
history_path.write_text(json.dumps(current_positions, indent=2))
report = {
"date": datetime.utcnow().strftime("%Y-%m-%d"),
"total_keywords": len(keywords),
"tracked": sum(1 for v in current_positions.values() if v),
"gainers": gainers,
"losers": losers,
"stable_count": len(stable),
}
Path(f"rank_report_{report['date']}.json").write_text(json.dumps(report, indent=2))
if losers:
msg = f"Rank Drop Alert: {len(losers)} keywords dropped\n"
for l in losers[:10]:
msg += f" {l['keyword']}: #{l['from']} -> #{l['to']}\n"
requests.post(SLACK_WEBHOOK, json={"text": msg}, timeout=10)
print(f"Report: {len(gainers)} up, {len(losers)} down, {len(stable)} stable")
if __name__ == "__main__":
run()JavaScript Implementation
const API_KEY = "your_scavio_api_key";
const DOMAIN = "yourdomain.com";
const DROP_THRESHOLD = 3;
const SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL";
async function getPosition(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: 30 }),
});
if (!res.ok) throw new Error(`scavio ${res.status}`);
const data = await res.json();
for (const item of data.organic ?? []) {
if (item.link?.includes(DOMAIN)) return item.position;
}
return null;
}
async function run() {
const fs = await import("fs/promises");
const keywords = JSON.parse(await fs.readFile("keywords.json", "utf8"));
let previous = {};
try { previous = JSON.parse(await fs.readFile("rank_history.json", "utf8")); } catch {}
const currentPositions = {};
const gainers = [];
const losers = [];
const stable = [];
for (let i = 0; i < keywords.length; i++) {
const kw = keywords[i];
const pos = await getPosition(kw);
currentPositions[kw] = pos;
const prev = previous[kw];
if (pos && prev) {
const delta = prev - pos;
if (delta >= DROP_THRESHOLD) gainers.push({ keyword: kw, from: prev, to: pos, change: delta });
else if (delta <= -DROP_THRESHOLD) losers.push({ keyword: kw, from: prev, to: pos, change: delta });
else stable.push(kw);
}
if (i % 20 === 19) await new Promise((r) => setTimeout(r, 1000));
}
await fs.writeFile("rank_history.json", JSON.stringify(currentPositions, null, 2));
const date = new Date().toISOString().slice(0, 10);
const report = { date, totalKeywords: keywords.length, tracked: Object.values(currentPositions).filter(Boolean).length, gainers, losers, stableCount: stable.length };
await fs.writeFile(`rank_report_${date}.json`, JSON.stringify(report, null, 2));
if (losers.length) {
const msg = `Rank Drop Alert: ${losers.length} keywords dropped\n` +
losers.slice(0, 10).map((l) => ` ${l.keyword}: #${l.from} -> #${l.to}`).join("\n");
await fetch(SLACK_WEBHOOK, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: msg }) });
}
console.log(`Report: ${gainers.length} up, ${losers.length} down, ${stable.length} stable`);
}
run();Platforms Used
Web search with knowledge graph, PAA, and AI overviews