How do you build a daily SEO rank report in n8n?

Quick Answer: You can build a daily SEO rank report in n8n by connecting SEMrush's API to pull keyword position data on a schedule, running a change detection step to flag ranking shifts, and delivering a formatted summary to Slack. The whole workflow runs automatically, no manual exports or spreadsheet wrangling required.
Rank tracking is one of those tasks that eats 20-30 minutes every morning if you do it manually. Pull data from SEMrush, paste it into a sheet, compare against yesterday, highlight the movers, send a summary to the team. This tutorial shows you how to build that entire process inside n8n so it runs itself, every day, before you've had coffee.
By the end, you'll have a working workflow that pulls keyword rankings from SEMrush on a daily schedule, detects position changes against the previous day's data, and pushes a clean report to a Slack channel.
What You Need Before You Start
- An n8n instance (self-hosted or n8n Cloud)
- A SEMrush API key (available on any paid SEMrush plan)
- A Slack workspace with a bot token and a reporting channel
- A Google Sheet to store historical ranking data (acts as your persistent memory between runs)
Step 1: Set Up the Schedule Trigger
Every automated report starts with a trigger. In n8n, open a new workflow and add a Schedule Trigger node.
Configure it to run once per day. A good default is 07:00 in your local timezone, so the report lands in Slack before your team starts their morning stand-up.
Settings to apply:
- Trigger interval: Days
- Run every: 1
- At hour: 7
- At minute: 0
Save the node. This is the heartbeat of your entire workflow.
Step 2: Pull Keyword Rankings from SEMrush
Add an HTTP Request node after the Schedule Trigger. SEMrush exposes ranking data through its API, and n8n's HTTP Request node handles the call cleanly.
Configure the HTTP Request node:
- Method:
GET - URL:
https://api.semrush.com/ - Query parameters (add each as a separate field):
type=domain_ranks(orphrase_organicfor keyword-level data)key= your SEMrush API keydomain= the domain you're trackingdatabase= your target country database (e.g.,usfor the US)export_columns=Ph,Po,Nq,Cp,Ur(Phrase, Position, Search Volume, CPC, URL)
For keyword-level position tracking, use type=phrase_organic and add a phrase parameter for each keyword. If you're tracking a list of keywords, you'll loop through them with an n8n Split In Batches node (covered in Step 3).
What the response looks like:
SEMrush returns a semicolon-delimited text response. Add a Code node after the HTTP Request to parse it into clean JSON:
const lines = $input.first().json.data.split('\n');
const headers = lines[0].split(';');
const rows = lines.slice(1).filter(line => line.trim() !== '');
return rows.map(row => {
const values = row.split(';');
const obj = {};
headers.forEach((header, i) => {
obj[header.trim()] = values[i]?.trim();
});
return { json: obj };
});
This gives you one item per keyword with fields like Keyword, Position, Search Volume, and URL.
Step 3: Loop Through a Keyword List
If you're tracking a defined list of keywords rather than pulling all rankings for a domain, store your keyword list in a Google Sheet and read it at the start of the workflow.
Add a Google Sheets node before the HTTP Request:
- Operation: Read Rows
- Sheet: your keyword tracking sheet (e.g., "Keywords")
- Range: A:A (keywords in column A)
Then add a Split In Batches node to iterate through each keyword and fire a separate SEMrush API call per keyword. Set batch size to 1 to keep the data clean and avoid rate limit issues.
Connect: Google Sheets → Split In Batches → HTTP Request → Code (parser)
Step 4: Read Yesterday's Rankings from Google Sheets
Change detection requires a baseline. Your Google Sheet doubles as a historical store. After parsing today's data, add another Google Sheets node to read the previous day's rankings.
Structure your tracking sheet with these columns:
| Date | Keyword | Position | URL | Search Volume |
Google Sheets node settings:
- Operation: Read Rows
- Filter by date: yesterday's date (use an expression like
{{ $now.minus({days: 1}).toFormat('yyyy-MM-dd') }})
Then add a Merge node to join today's data with yesterday's data on the Keyword field. Use the Merge by Key mode with Keyword as the matching field.
This gives you a combined dataset where each item contains both the current position and the previous position for the same keyword.
Step 5: Detect Position Changes
Add a Code node after the Merge node to calculate the change for each keyword and flag significant movers.
return $input.all().map(item => {
const today = parseInt(item.json.Position_today) || 0;
const yesterday = parseInt(item.json.Position_yesterday) || 0;
const change = yesterday - today; // positive = improved, negative = dropped
let status = 'stable';
if (change >= 3) status = 'improved';
if (change <= -3) status = 'dropped';
return {
json: {
keyword: item.json.Keyword,
position_today: today,
position_yesterday: yesterday,
change: change,
status: status,
url: item.json.URL_today
}
};
});
You can adjust the threshold (currently set to 3 positions) based on how sensitive you want your alerts to be. If you also want to improve the destination page itself, these ranking shifts often point to pages that need stronger messaging or conversion-focused updates, which is why it's worth reviewing proven SaaS landing page best practices and examples alongside your SEO workflow.
Step 6: Write Today's Data Back to Google Sheets
Before sending the report, persist today's rankings so they become tomorrow's baseline.
Add a Google Sheets node:
- Operation: Append Row
- Map the fields: Date (today's date), Keyword, Position, URL, Search Volume
This runs after the change detection step so you always have a rolling daily history.
Step 7: Format and Send the Slack Report
Add a Code node to build the Slack message. A clean report separates improved, dropped, and stable keywords so the reader can scan it in under 30 seconds.
const items = $input.all().map(i => i.json);
const improved = items.filter(i => i.status === 'improved');
const dropped = items.filter(i => i.status === 'dropped');
const formatRow = (i) =>
`• *${i.keyword}*: ${i.position_today} (was ${i.position_yesterday}, ${i.change > 0 ? '+' : ''}${i.change})`;
const message = [
`*Daily SEO Rank Report – ${new Date().toDateString()}*`,
'',
improved.length > 0 ? `*Improved (${improved.length})*\n${improved.map(formatRow).join('\n')}` : '',
dropped.length > 0 ? `*Dropped (${dropped.length})*\n${dropped.map(formatRow).join('\n')}` : '',
`_${items.length} keywords tracked_`
].filter(Boolean).join('\n');
return [{ json: { message } }];
Then add a Slack node:
- Resource: Message
- Operation: Send
- Channel: your reporting channel (e.g.,
#seo-reports) - Text:
{{ $json.message }}
Step 8: Add an Error Handler
Any production workflow needs a fallback. Add an Error Trigger node and connect it to a second Slack node that posts to a #workflow-errors channel if any step fails.
This prevents silent failures where the report simply doesn't arrive and nobody notices until rankings have already shifted.
What the Finished Workflow Looks Like
Schedule Trigger (07:00 daily)
→ Google Sheets (read keyword list)
→ Split In Batches
→ HTTP Request (SEMrush API)
→ Code (parse response)
→ Google Sheets (read yesterday's data)
→ Merge (by keyword)
→ Code (change detection)
→ Google Sheets (append today's data)
→ Code (format Slack message)
→ Slack (send report)
Total nodes: 10-11 depending on your error handler setup. Build time for someone familiar with n8n: roughly 90 minutes on first attempt.
Common Issues and How to Fix Them
SEMrush returns empty data
Check your database parameter matches the correct regional database for your target market. Also confirm your API key has sufficient units. Each keyword lookup costs API credits.
Merge node returns no matches
This usually means your date formatting is inconsistent between the write step and the read step. Pin the format to yyyy-MM-dd throughout using $now.toFormat('yyyy-MM-dd').
Slack message is too long Slack blocks messages above 4,000 characters. If you're tracking more than 50-60 keywords, filter the Slack output to show only keywords with a change of 5 or more positions, and link to the full Google Sheet for the complete picture.
Rate limiting from SEMrush Add a Wait node (set to 1 second) between batches in your Split In Batches loop. SEMrush's API allows bursts but sustained rapid requests will return 429 errors.
How to Extend This Workflow
Once the core workflow runs reliably, three extensions add real value:
Weekly summary digest: Add a second Schedule Trigger set to Mondays that reads the last 7 days of Google Sheets data and sends a weekly trend summary alongside the daily report.
Threshold alerts only: Duplicate the workflow and strip out the daily summary. Configure this version to only fire the Slack node when a keyword drops more than 5 positions in a single day. Use this for client-facing monitoring where signal-to-noise matters.
Google Looker Studio connection: Your Google Sheet is already structured as a clean data table. Connect it to Looker Studio (free) and build a live ranking trend chart that updates every morning automatically.
FAQs
What does an n8n daily SEO rank report workflow do? It pulls keyword position data from SEMrush on an automated daily schedule, compares each keyword's current position against the previous day's data stored in Google Sheets, identifies significant ranking changes, and delivers a formatted summary to a Slack channel. The workflow runs without any manual input once it's set up.
Do I need coding experience to build this in n8n? No, but you need to be comfortable reading and editing basic JavaScript. The Code nodes in this workflow use straightforward array methods. If you can follow the examples above and adjust variable names, you can build this workflow. The HTTP Request, Google Sheets, and Slack nodes are all point-and-click configuration.
How much does this workflow cost to run daily?
n8n Cloud pricing starts at around $20/month. SEMrush API costs depend on your plan and keyword volume. Each phrase_organic lookup costs 10 API units. Tracking 50 keywords daily uses 500 units, which is well within most paid SEMrush plans. Slack and Google Sheets are free for this use case.
Can I use a different rank tracking API instead of SEMrush? Yes. DataForSEO and Bright Data both have REST APIs that work with n8n's HTTP Request node using the same pattern shown in this tutorial. The parsing step in the Code node will need adjusting to match the response format of whichever API you choose.
What happens if the workflow fails overnight? The Error Trigger node at the end of the workflow catches any failure and sends a Slack alert to your errors channel. Without that node, failures are silent. Always add error handling to scheduled workflows before you consider them production-ready.
Is this workflow suitable for agency use across multiple clients? With modifications, yes. The simplest approach is to duplicate the workflow once per client and swap the domain, keyword sheet, and Slack channel. A more advanced setup uses a single workflow that reads a client configuration sheet and loops through each client's settings dynamically. If you reach the point where you need outside help scaling SEO reporting, automation, or broader growth systems, SaaS Hackers also lets you find an expert or browse vetted B2B SaaS SEO agencies for implementation support.
Find a B2B SaaS Expert
We've collected a directory of B2B SaaS experts and agencies that we've reviewed and categorised based on service and specialism for your review.


