Tracks where your pages rank each week, catches the ones slipping, and says what to change on each.

UsesClaudeGoogle SheetsSerperSlack

Catch the pages slipping in Google each week

Every Monday at 07:00, one Google search is run for each page you list in your Google Sheet, and where your site sits is compared with the last check. Each reading, with the position, which of your pages ranked, who holds the top spot and who holds the answer box, is added to the same sheet as a rank history. The first week's Slack summary lists where each query stands.

A page is only written up when it has lost ground beyond the threshold you choose; everything else is recorded and left alone. A summary goes to your Slack channel every week, even when nothing moved. Each slipping page is opened first, because a missing page, a redirect or a noindex tag has to be fixed before any content advice matters, and then AI weighs the query, both positions, the results page around it and what the page contains, and posts a specific recommendation as a thread under the summary. If the sheet cannot be read, nothing is written; a search, a post or a write-up that fails is named and the run is marked as failed.

You need Serper, Anthropic, Google Sheets and Slack connected, your domain, a Slack channel ID, and a Google Sheet with a 'Tracked pages' tab listing each page's URL and the Query it should rank for.

ScheduleRun every Monday at 07:00.Run every Monday at 07:00.
ValuesSet siteDomain to blank.Set siteDomain to blank.
ValuesSet slackChannelId to blank.Set slackChannelId to blank.
ValuesSet historySheetUrl to blank.Set historySheetUrl to blank.
ValuesSet trackedPagesTab to Tracked pages.Set trackedPagesTab to Tracked pages.
ValuesSet historyTabName to Rank history.Set historyTabName to Rank history.
ValuesSet searchCountry to us.Set searchCountry to us.
ValuesSet searchLanguage to en.Set searchLanguage to en.
ValuesSet resultsToScan to 50.Set resultsToScan to 50.
ValuesSet positionDropAlert to 3.Set positionDropAlert to 3.
ValuesSet maxPagesToDiagnose to 5.Set maxPagesToDiagnose to 5.
ValuesSet trackedPages to an empty list.Set trackedPages to an empty list.
Decisions & LoopsIf a step below fails, save the error as trackedReadErrorIf a step below fails, save the error as trackedReadError
Try
Google SheetsGoogle SheetsRead the rows of historySheetUrl. Save it as trackedSheet.Read the rows of historySheetUrl. Save it as trackedSheet.
ListsKeep each item in trackedSheet.rows where item.URL isn't empty and item.Query isn't empty and save it as trackedPages.Keep each item in trackedSheet.rows where item.URL isn't empty and item.Query isn't empty and save it as trackedPages.
On error
Decisions & LoopsIf a step below fails, save the error as trackedTabErrorIf a step below fails, save the error as trackedTabError
Try
Google SheetsGoogle SheetsCreate the tab trackedPagesTab in historySheetUrl.Create the tab trackedPagesTab in historySheetUrl.
Google SheetsGoogle SheetsAdd a row to trackedPagesTab in historySheetUrl.Add a row to trackedPagesTab in historySheetUrl.
On error
Decisions & LoopsStop and report a problem: Could not read the '@trackedPagesTab' tab of your sheet. Check the link in 'historySheetUrl' and that the connected Google account can open and edit the sheet, then run this again. Reason: trackedReadError.message.Stop and report a problem: Could not read the '@trackedPagesTab' tab of your sheet. Check the link in 'historySheetUrl' and that the connected Google account can open and edit the sheet, then run this again. Reason: trackedReadError.message.
Decisions & LoopsStop and report a problem: Added a '@trackedPagesTab' tab to your sheet. List the pages to watch there, one per row: the page's address under URL and the search it should rank for under Query. Then run this again.Stop and report a problem: Added a '@trackedPagesTab' tab to your sheet. List the pages to watch there, one per row: the page's address under URL and the search it should rank for under Query. Then run this again.
Decisions & LoopsIf trackedPages is emptyIf trackedPages is empty
Yes
Decisions & LoopsStop and report a problem: Add the pages to watch to the '@trackedPagesTab' tab of your sheet, one per row: the page's address in a column headed URL and the search it should rank for in a column headed Query (the headings go in row 1). Then run this again.Stop and report a problem: Add the pages to watch to the '@trackedPagesTab' tab of your sheet, one per row: the page's address in a column headed URL and the search it should rank for in a column headed Query (the headings go in row 1). Then run this again.
No
No steps
Dates & TimesGet today's date and save it as runDate.Get today's date and save it as runDate.
ValuesSet historyRows to an empty list.Set historyRows to an empty list.
ValuesSet historyHasHeader to false.Set historyHasHeader to false.
Decisions & LoopsIf a step below fails, save the error as historyReadErrorIf a step below fails, save the error as historyReadError
Try
Google SheetsGoogle SheetsRead the rows of historySheetUrl. Save it as storedHistory.Read the rows of historySheetUrl. Save it as storedHistory.
ValuesChange historyRows to storedHistory.rows.Change historyRows to storedHistory.rows.
ValuesChange historyHasHeader to the length of storedHistory.headers is greater than 0.Change historyHasHeader to the length of storedHistory.headers is greater than 0.
On error
Decisions & LoopsIf a step below fails, save the error as createTabErrorIf a step below fails, save the error as createTabError
Try
Google SheetsGoogle SheetsCreate the tab historyTabName in historySheetUrl.Create the tab historyTabName in historySheetUrl.
On error
Decisions & LoopsStop and report a problem: Could not read the '@historyTabName' tab of your history sheet, so this week could not be compared with last week and nothing was written or posted. Check the link in 'historySheetUrl' and that the connected Google account can open the sheet, then run this again. Reason: historyReadError.message.Stop and report a problem: Could not read the '@historyTabName' tab of your history sheet, so this week could not be compared with last week and nothing was written or posted. Check the link in 'historySheetUrl' and that the connected Google account can open the sheet, then run this again. Reason: historyReadError.message.
ValuesShow in the output: No history yet, so this run created the '@historyTabName' tab and starts the record.Show in the output: No history yet, so this run created the '@historyTabName' tab and starts the record.
Decisions & LoopsIf historyHasHeader is noIf historyHasHeader is no
Yes
Decisions & LoopsIf a step below fails, save the error as headerWriteErrorIf a step below fails, save the error as headerWriteError
Try
Google SheetsGoogle SheetsAdd a row to historyTabName in historySheetUrl.Add a row to historyTabName in historySheetUrl.
On error
Decisions & LoopsStop and report a problem: The history sheet could not be written to. Check that 'historySheetUrl' points at a spreadsheet the connected Google account can edit, then run this again.Stop and report a problem: The history sheet could not be written to. Check that 'historySheetUrl' points at a spreadsheet the connected Google account can edit, then run this again.
No
No steps
CodeRun Python code on rows and today and save it as previousReadings.Run Python code on rows and today and save it as previousReadings.
ValuesSet serpSnapshots to an empty list.Set serpSnapshots to an empty list.
ValuesSet searchFailures to an empty list.Set searchFailures to an empty list.
Decisions & LoopsFor each trackedPage in trackedPages:For each trackedPage in trackedPages:
Decisions & LoopsIf a step below fails, save the error as searchErrorIf a step below fails, save the error as searchError
Try
SerperSerperSearch Google for trackedPage.Query. Save it as searchResult.Search Google for trackedPage.Query. Save it as searchResult.
ListsAdd a record of keyword, url, and serp to the end of serpSnapshots and save it as serpSnapshots.Add a record of keyword, url, and serp to the end of serpSnapshots and save it as serpSnapshots.
On error
ListsAdd Could not check '@trackedPage.Query': searchError.message to the end of searchFailures and save it as searchFailures.Add Could not check '@trackedPage.Query': searchError.message to the end of searchFailures and save it as searchFailures.
CodeRun Python code on snapshots, previous, site_domain, run_date, results_scanned, position_drop_alert, max_to_diagnose, sheet_url, and search_failures and save it as decayReport.Run Python code on snapshots, previous, site_domain, run_date, results_scanned, position_drop_alert, max_to_diagnose, sheet_url, and search_failures and save it as decayReport.
ValuesSet problems to decayReport.unchecked.Set problems to decayReport.unchecked.
Decisions & LoopsFor each historyRow in decayReport.rows:For each historyRow in decayReport.rows:
Decisions & LoopsIf a step below fails, save the error as historyWriteErrorIf a step below fails, save the error as historyWriteError
Try
Google SheetsGoogle SheetsAdd a row to historyTabName in historySheetUrl.Add a row to historyTabName in historySheetUrl.
On error
ListsAdd A row of this week's readings could not be written to the history sheet: historyWriteError.message to the end of problems and save it as problems.Add A row of this week's readings could not be written to the history sheet: historyWriteError.message to the end of problems and save it as problems.
ValuesSet summaryTimestamp to nothing.Set summaryTimestamp to nothing.
Decisions & LoopsIf a step below fails, save the error as summaryErrorIf a step below fails, save the error as summaryError
Try
SlackSlackSend a message to slackChannelId. Save it as postedSummary.Send a message to slackChannelId. Save it as postedSummary.decayReport.summary
ValuesChange summaryTimestamp to postedSummary.ts.Change summaryTimestamp to postedSummary.ts.
On error
ListsAdd The summary could not be posted to Slack: summaryError.message to the end of problems and save it as problems.Add The summary could not be posted to Slack: summaryError.message to the end of problems and save it as problems.
Decisions & LoopsFor each finding in decayReport.findings:For each finding in decayReport.findings:
ValuesSet pageSummary to - The page itself was not read this run..Set pageSummary to - The page itself was not read this run..
Decisions & LoopsIf a step below fails, save the error as pageFetchErrorIf a step below fails, save the error as pageFetchError
Try
HTTPFetch finding.url and save it as pageResponse.Fetch finding.url and save it as pageResponse.
CodeRun Python code on status, final_url, page_url, and html and save it as pageSummary.Run Python code on status, final_url, page_url, and html and save it as pageSummary.
On error
ValuesChange pageSummary to - The page could not be read this run (pageFetchError.message), so judge it from the search data alone.Change pageSummary to - The page could not be read this run (pageFetchError.message), so judge it from the search data alone.
Decisions & LoopsIf a step below fails, save the error as adviceErrorIf a step below fails, save the error as adviceError
Try
ClaudeClaudeAsk Claude: 'You are the SEO lead for this site, looking at one page that has lost ground in Google.Ask Claude: 'You are the SEO lead for this site, looking at one page that has lost ground in Google. Everything below was measured this morning: the query, where the site ranks for it now and where it ranked at the last check, what the results page around it looks like, and what the page itself currently contains. Work from that data and nothing else. Never invent a number, a URL, a competitor or a date that is not in it. If something you would want is missing, say what is missing rather than filling it in. Pick exactly one call: REFRESH - this is still the right page for this query, but what is on it has fallen behind what now ranks. CONSOLIDATE - two of your own pages are competing for this query; merge them and redirect the loser. RE-TARGET - the results page has changed shape and this query no longer wants a page like this one; aim the page at a query it can win, or rebuild it in the format that now ranks. FIX - something technical is in the way: the URL does not load, it redirects, it is noindexed, or its canonical points elsewhere. Say this whenever the page data shows one of those, whatever else is going on. WAIT - the movement is inside normal week-to-week noise, or the whole tracked set moved at once, so re-measure before touching the page. Then answer in exactly this shape, in plain Slack text, under 200 words, no preamble: *Call:* <REFRESH, CONSOLIDATE, RE-TARGET, FIX or WAIT> *Why:* <one or two sentences naming the specific evidence, with the numbers> *Do this:* - <two to four changes, each one something a person can start today: a section to add and what it should cover, a claim or figure to update, a page to merge in and which URL survives, a tag to remove, a format to switch to> *Check again:* <when it is reasonable to expect this to move> Rules for the Do this lines. Name the subtopic, the competitor, the question or the number you are reacting to; a line that would read the same on any page is not worth writing. Do not suggest buying links, spinning up more pages for the same query, or anything you would not want a search engineer to read. Where the results page has started answering the query itself, treat the goal as being the source that answer cites and readers still click, not as ranking higher. Here is the data. finding.brief THE PAGE AS IT STANDS TODAY: pageSummary decayReport.context'. Save it as advice. Maximum response length: 900
TextTrim advice.response_text and save it as recommendation.Trim advice.response_text and save it as recommendation.
Decisions & LoopsIf recommendation isn't emptyIf recommendation isn't empty
Yes
SlackSlackReply in a thread in slackChannelId.Reply in a thread in slackChannelId. Reply To: summaryTimestampfinding.heading recommendation
No
ListsAdd No recommendation came back for '@finding.keyword', so nothing was posted for it. to the end of problems and save it as problems.Add No recommendation came back for '@finding.keyword', so nothing was posted for it. to the end of problems and save it as problems.
On error
ListsAdd Could not write up '@finding.keyword': adviceError.message to the end of problems and save it as problems.Add Could not write up '@finding.keyword': adviceError.message to the end of problems and save it as problems.
Decisions & LoopsIf problems isn't emptyIf problems isn't empty
Yes
TextJoin problems with a new line and save it as problemList.Join problems with a new line and save it as problemList.
Decisions & LoopsStop and report a problem: The weekly check ran, but not everything worked:Stop and report a problem: The weekly check ran, but not everything worked: problemList.
No
No steps