This workflow corresponds to n8n.io template #17715 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Monitor and score RSS opportunities in Google Sheets with Gmail",
"nodes": [
{
"id": "note",
"name": "Template Guide",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-560
],
"parameters": {
"color": 4,
"width": 1180,
"height": 500,
"content": "# Monitor and score RSS opportunities in Google Sheets with Gmail\n\n## Who's it for\nFreelancers, consultants, recruiters, sales teams and partnership managers who repeatedly scan RSS feeds and need a reliable review queue without automating outreach.\n\n## How it works\nThe workflow checks one or more RSS feeds manually or every 30 minutes. It processes feeds sequentially, normalizes each entry, remembers URLs for 90 days and calculates an explainable score from editable positive, high-value and exclusion keywords. Every new opportunity is appended to Google Sheets. Gmail sends one digest only when an item reaches the configured priority threshold. Applications, proposals and outreach always remain under human control.\n\n## How to set up\nOpen **Configure Template** and add RSS URLs, keywords, a threshold, the Google Sheet tab and your notification email. Create a sheet from the included CSV headers, select it in **Save Opportunities**, connect Google Sheets and Gmail credentials, then run the workflow manually before activating the schedule.\n\n## Requirements\n- n8n Cloud or self-hosted n8n\n- One or more public RSS feeds\n- Google Sheets OAuth2\n- Gmail OAuth2 for notifications\n\n## How to customize\nAdjust keywords and weights in **Normalize, Deduplicate & Score**, change the schedule, or replace Gmail with Slack or another review channel. No paid AI API is required."
},
"typeVersion": 1
},
{
"id": "note-config",
"name": "Step 1 - Configuration",
"type": "n8n-nodes-base.stickyNote",
"position": [
260,
40
],
"parameters": {
"width": 520,
"height": 440,
"content": "## 1. Configure\n\nSet the RSS feed URLs, scoring keywords, threshold, Google Sheet tab and notification recipient here."
},
"typeVersion": 1
},
{
"id": "note-process",
"name": "Step 2 - Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
800,
40
],
"parameters": {
"width": 1040,
"height": 560,
"content": "## 2. Process\n\nFeeds run sequentially. Entries are normalized, deduplicated for 90 days and scored with visible reasons."
},
"typeVersion": 1
},
{
"id": "note-output",
"name": "Step 3 - Review Queue",
"type": "n8n-nodes-base.stickyNote",
"position": [
1860,
40
],
"parameters": {
"width": 1080,
"height": 560,
"content": "## 3. Store and notify\n\nEvery new item is stored in Google Sheets. Gmail only sends a digest when an item reaches the configured threshold."
},
"typeVersion": 1
},
{
"id": "manual",
"name": "Run Manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
80,
180
],
"parameters": {},
"typeVersion": 1
},
{
"id": "schedule",
"name": "Every 30 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
80,
340
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 30
}
]
}
},
"typeVersion": 1.2
},
{
"id": "fields",
"name": "Configure Template",
"type": "n8n-nodes-base.set",
"position": [
340,
260
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "config-rssFeedUrls",
"name": "rssFeedUrls",
"type": "string",
"value": "PASTE_ONE_OR_MORE_RSS_URLS_HERE"
},
{
"id": "config-searchName",
"name": "searchName",
"type": "string",
"value": "AI automation opportunities"
},
{
"id": "config-priorityThreshold",
"name": "priorityThreshold",
"type": "number",
"value": 75
},
{
"id": "config-includeKeywords",
"name": "includeKeywords",
"type": "string",
"value": "n8n, automation, api, ai agent, claude, openai, next.js"
},
{
"id": "config-highValueKeywords",
"name": "highValueKeywords",
"type": "string",
"value": "long term, ongoing, migration, self-hosted, production, architecture"
},
{
"id": "config-excludeKeywords",
"name": "excludeKeywords",
"type": "string",
"value": "unpaid, commission only, exposure"
},
{
"id": "config-googleSheetTab",
"name": "googleSheetTab",
"type": "string",
"value": "Opportunities"
},
{
"id": "config-notificationEmail",
"name": "notificationEmail",
"type": "string",
"value": "user@example.com"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "config",
"name": "Build Search Configuration",
"type": "n8n-nodes-base.code",
"position": [
600,
260
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const config = $input.first().json;\nconst list = (value) => String(value || \"\").split(\",\").map((item) => item.trim().toLowerCase()).filter(Boolean);\nconst urls = String(config.rssFeedUrls || \"\").split(/\\r?\\n|,/).map((url) => url.trim()).filter((url) => /^https?:\\/\\//i.test(url));\nconst scoring = {\n priorityThreshold: Number(config.priorityThreshold || 75),\n includeKeywords: list(config.includeKeywords),\n highValueKeywords: list(config.highValueKeywords),\n excludeKeywords: list(config.excludeKeywords),\n};\n\nreturn urls.slice(0, 10).map((rssUrl, index) => ({\n json: {\n name: urls.length === 1 ? config.searchName : config.searchName + \" \" + (index + 1),\n rssUrl,\n scoring,\n },\n}));",
"language": "javaScript"
},
"typeVersion": 2
},
{
"id": "loop",
"name": "Loop Over Searches",
"type": "n8n-nodes-base.splitInBatches",
"position": [
860,
260
],
"parameters": {
"options": {},
"batchSize": 1
},
"typeVersion": 3
},
{
"id": "rss",
"name": "Read RSS Feed",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
1120,
380
],
"parameters": {
"url": "={{ $json.rssUrl }}"
},
"typeVersion": 1.2,
"alwaysOutputData": true
},
{
"id": "score",
"name": "Normalize, Deduplicate & Score",
"type": "n8n-nodes-base.code",
"position": [
1380,
380
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const search = $(\"Loop Over Searches\").item.json;\nconst entries = $input.all().slice(0, 50).map((item) => item.json || {});\nconst state = $getWorkflowStaticData(\"global\");\nstate.seen = state.seen || {};\nconst now = Date.now();\nconst retentionMs = 90 * 24 * 60 * 60 * 1000;\n\nfor (const [key, seenAt] of Object.entries(state.seen)) {\n if (now - Number(seenAt) > retentionMs) delete state.seen[key];\n}\n\nconst plain = (value) => String(value || \"\").replace(/<[^>]+>/g, \" \").replace(/ /gi, \" \").replace(/&/g, \"&\").replace(/\\s+/g, \" \").trim();\nconst candidates = [];\n\nfor (const entry of entries) {\n const title = plain(entry.title);\n const url = plain(entry.link || entry.guid);\n if (!title || !/^https?:\\/\\//i.test(url) || state.seen[url]) continue;\n\n const description = plain(entry.contentSnippet || entry.description || entry.content);\n const haystack = (title + \" \" + description).toLowerCase();\n let score = 20;\n const reasons = [];\n\n for (const keyword of search.scoring.includeKeywords) {\n if (haystack.includes(keyword.toLowerCase())) { score += 8; reasons.push(\"match: \" + keyword); }\n }\n for (const keyword of search.scoring.highValueKeywords) {\n if (haystack.includes(keyword.toLowerCase())) { score += 10; reasons.push(\"high value: \" + keyword); }\n }\n for (const keyword of search.scoring.excludeKeywords) {\n if (haystack.includes(keyword.toLowerCase())) { score -= 30; reasons.push(\"risk: \" + keyword); }\n }\n\n score = Math.max(0, Math.min(100, score));\n state.seen[url] = now;\n candidates.push({\n detected_at: new Date(now).toISOString(),\n source_search: search.name,\n title,\n url,\n published_at: entry.isoDate || entry.pubDate || \"\",\n score,\n score_reasons: reasons.join(\" | \") || \"No configured keyword matched\",\n categories: (Array.isArray(entry.categories) ? entry.categories : [entry.category]).filter(Boolean).join(\", \"),\n budget_label: (haystack.match(/budget[^.\\n]{0,80}/i) || [\"\"])[0],\n status: score >= search.scoring.priorityThreshold ? \"priority_review\" : \"to_review\",\n });\n}\n\nreturn [{ json: { candidates, priorityThreshold: search.scoring.priorityThreshold } }];",
"language": "javaScript"
},
"typeVersion": 2
},
{
"id": "has-new",
"name": "Has New Opportunities?",
"type": "n8n-nodes-base.if",
"position": [
1640,
380
],
"parameters": {
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.candidates.length }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "expand",
"name": "Expand Opportunities",
"type": "n8n-nodes-base.code",
"position": [
1900,
300
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "return ($input.first().json.candidates || []).map((candidate) => ({ json: candidate }));",
"language": "javaScript"
},
"typeVersion": 2
},
{
"id": "sheets",
"name": "Save Opportunities",
"type": "n8n-nodes-base.googleSheets",
"position": [
2160,
300
],
"parameters": {
"columns": {
"value": {
"url": "={{ $json.url }}",
"score": "={{ $json.score }}",
"title": "={{ $json.title }}",
"status": "={{ $json.status }}",
"categories": "={{ $json.categories }}",
"detected_at": "={{ $json.detected_at }}",
"budget_label": "={{ $json.budget_label }}",
"published_at": "={{ $json.published_at }}",
"score_reasons": "={{ $json.score_reasons }}",
"source_search": "={{ $json.source_search }}"
},
"schema": [
{
"id": "detected_at",
"type": "string",
"displayName": "detected_at"
},
{
"id": "source_search",
"type": "string",
"displayName": "source_search"
},
{
"id": "title",
"type": "string",
"displayName": "title"
},
{
"id": "url",
"type": "string",
"displayName": "url"
},
{
"id": "published_at",
"type": "string",
"displayName": "published_at"
},
{
"id": "score",
"type": "number",
"displayName": "score"
},
{
"id": "score_reasons",
"type": "string",
"displayName": "score_reasons"
},
{
"id": "categories",
"type": "string",
"displayName": "categories"
},
{
"id": "budget_label",
"type": "string",
"displayName": "budget_label"
},
{
"id": "status",
"type": "string",
"displayName": "status"
}
],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Configure Template').first().json.googleSheetTab }}"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": ""
}
},
"typeVersion": 4.5
},
{
"id": "digest",
"name": "Build Priority Digest",
"type": "n8n-nodes-base.code",
"position": [
2420,
300
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const items = $input.all().map((item) => item.json || {});\nconst threshold = Number($(\"Configure Template\").first().json.priorityThreshold || 75);\nconst priority = items.filter((item) => Number(item.score) >= threshold);\nconst lines = priority.map((item) => [\n \"Score \" + item.score + \"/100 - \" + item.title,\n item.url,\n item.score_reasons,\n].join(\"\\n\"));\n\nreturn [{ json: {\n priorityCount: priority.length,\n subject: priority.length + \" priority automation opportunity\" + (priority.length === 1 ? \"\" : \"ies\") + \" to review\",\n message: lines.join(\"\\n\\n---\\n\\n\") || \"No priority opportunity in this run.\",\n} }];",
"language": "javaScript"
},
"typeVersion": 2
},
{
"id": "has-priority",
"name": "Has Priority Opportunities?",
"type": "n8n-nodes-base.if",
"position": [
2680,
300
],
"parameters": {
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.priorityCount }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "gmail",
"name": "Send Priority Digest",
"type": "n8n-nodes-base.gmail",
"position": [
2940,
220
],
"parameters": {
"sendTo": "={{ $('Configure Template').first().json.notificationEmail }}",
"message": "={{ $json.message }}",
"options": {},
"subject": "={{ $json.subject }}"
},
"typeVersion": 2.1
},
{
"id": "wait",
"name": "Wait Before Next Search",
"type": "n8n-nodes-base.wait",
"position": [
3200,
380
],
"parameters": {
"unit": "seconds",
"amount": 2,
"resume": "timeInterval",
"options": {}
},
"typeVersion": 1.1
},
{
"id": "done",
"name": "Finished",
"type": "n8n-nodes-base.noOp",
"position": [
1120,
120
],
"parameters": {},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "UTC",
"executionOrder": "v1",
"executionTimeout": 180,
"saveManualExecutions": true,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "none"
},
"connections": {
"Run Manually": {
"main": [
[
{
"node": "Configure Template",
"type": "main",
"index": 0
}
]
]
},
"Read RSS Feed": {
"main": [
[
{
"node": "Normalize, Deduplicate & Score",
"type": "main",
"index": 0
}
]
]
},
"Every 30 Minutes": {
"main": [
[
{
"node": "Configure Template",
"type": "main",
"index": 0
}
]
]
},
"Configure Template": {
"main": [
[
{
"node": "Build Search Configuration",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Searches": {
"main": [
[
{
"node": "Finished",
"type": "main",
"index": 0
}
],
[
{
"node": "Read RSS Feed",
"type": "main",
"index": 0
}
]
]
},
"Save Opportunities": {
"main": [
[
{
"node": "Build Priority Digest",
"type": "main",
"index": 0
}
]
]
},
"Expand Opportunities": {
"main": [
[
{
"node": "Save Opportunities",
"type": "main",
"index": 0
}
]
]
},
"Send Priority Digest": {
"main": [
[
{
"node": "Wait Before Next Search",
"type": "main",
"index": 0
}
]
]
},
"Build Priority Digest": {
"main": [
[
{
"node": "Has Priority Opportunities?",
"type": "main",
"index": 0
}
]
]
},
"Has New Opportunities?": {
"main": [
[
{
"node": "Expand Opportunities",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait Before Next Search",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Next Search": {
"main": [
[
{
"node": "Loop Over Searches",
"type": "main",
"index": 0
}
]
]
},
"Build Search Configuration": {
"main": [
[
{
"node": "Loop Over Searches",
"type": "main",
"index": 0
}
]
]
},
"Has Priority Opportunities?": {
"main": [
[
{
"node": "Send Priority Digest",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait Before Next Search",
"type": "main",
"index": 0
}
]
]
},
"Normalize, Deduplicate & Score": {
"main": [
[
{
"node": "Has New Opportunities?",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs manually or every 30 minutes to read one or more RSS feeds, score and deduplicate new opportunities, append them to Google Sheets, and email a Gmail digest only when items meet a priority threshold. Runs manually or on a 30-minute schedule. Builds a list of…
Source: https://n8n.io/workflows/17715/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow acts as a 24/7 digital real estate scout. It automatically monitors multiple RSS news feeds, filters out stories you have already seen, uses Groq-powered AI to score the investment poten
This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch
Loan eligibility workflow. Uses formTrigger, googleSheets, gmail. Event-driven trigger; 53 nodes.
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.