AutomationFlowsAI & RAG › Send Weekly Reddit Sentiment Digests to Notion with Scraperapi and Openai

Send Weekly Reddit Sentiment Digests to Notion with Scraperapi and Openai

ByScraperAPI @scraperapi on n8n.io

This workflow runs every Monday, scrapes weekly top posts from selected Reddit pages via ScraperAPI, analyzes sentiment and themes with OpenAI GPT-5 Mini, compiles a single weekly digest, and creates a formatted page in a Notion database. Runs every Monday at 9:00 AM on a…

Cron / scheduled trigger★★★★☆ complexityAI-powered15 nodesN8N Nodes Scraperapi OfficialChain LlmOpenAI ChatNotion
AI & RAG Trigger: Cron / scheduled Nodes: 15 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #16447 — we link there as the canonical source.

This workflow follows the Chainllm → OpenAI Chat 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 →

Download .json
{
  "nodes": [
    {
      "id": "78fecc54-92cd-453f-9841-b346f465353b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        288
      ],
      "parameters": {
        "width": 480,
        "height": 784,
        "content": "## Send weekly Reddit sentiment digests to Notion via ScraperAPI\n\n### How it works\n\n1. The workflow is triggered every Monday at 9 am.\n2. It configures source and notion database parameters.\n3. Sources are split to scrape content via ScraperAPI.\n4. The content is prepared and classified with an LLM chain.\n5. Results are parsed, aggregated into a weekly digest, and saved to Notion.\n\n### Setup steps\n\n- [ ] Configure the 'Every Monday at 9am' schedule trigger to the desired time.\n- [ ] Set up the sources and notion_database_id in the 'Configure' node.\n- [ ] Ensure ScraperAPI credentials are correctly configured.\n- [ ] Verify LLM Chain configuration for sentiment classification.\n- [ ] Connect to Notion using the appropriate API token and database settings.\n\n### Customization\n\nConsider modifying the 'Monday at 9am' schedule to fit different reporting requirements, or adjust the sources being analyzed."
      },
      "typeVersion": 1
    },
    {
      "id": "abba02d5-aca5-4187-959b-1f03d7137069",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 304,
        "content": "## Schedule and configure sources\n\nStarts the workflow on a schedule and sets up the sources and Notion database configuration."
      },
      "typeVersion": 1
    },
    {
      "id": "43e181f3-1eb5-4b93-8057-0ce912cf4c29",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 304,
        "content": "## Extract sources and scrape data\n\nSplits sources and scrapes content using ScraperAPI."
      },
      "typeVersion": 1
    },
    {
      "id": "8ff81e9d-1d2e-499d-9a84-3f96dfdd1c74",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 448,
        "content": "## Process and classify content\n\nPrepares content for sentiment analysis and classifies sentiment using an LLM."
      },
      "typeVersion": 1
    },
    {
      "id": "6512de25-07b5-464d-8720-ea47d2618d6a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 304,
        "content": "## Parse results and build digest\n\nParses LLM classifications, aggregates results into a weekly digest, and creates a Notion page."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "When Monday at 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -624,
        416
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "configure",
      "name": "Set Source and Notion ID",
      "type": "n8n-nodes-base.set",
      "position": [
        -336,
        416
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "sources",
              "name": "sources",
              "type": "array",
              "value": "={{ [ { url: 'https://old.reddit.com/r/n8n/top/?t=week', label: 'r/n8n top this week' }, { url: 'https://old.reddit.com/r/webscraping/top/?t=week', label: 'r/webscraping top this week' }, { url: 'https://old.reddit.com/r/automation/top/?t=week', label: 'r/automation top this week' } ] }}"
            },
            {
              "id": "notion-db",
              "name": "notion_database_id",
              "type": "string",
              "value": "REPLACE_WITH_NOTION_DATABASE_ID"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "split-sources",
      "name": "Split Source Items",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -48,
        416
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "sources"
      },
      "typeVersion": 1
    },
    {
      "id": "scrape-via-scraperapi",
      "name": "Scrape Data via ScraperAPI",
      "type": "n8n-nodes-scraperapi-official.scraperApi",
      "maxTries": 2,
      "position": [
        224,
        416
      ],
      "parameters": {
        "apiUrl": "={{ $json.url }}",
        "resource": "api",
        "operation": "apiRequest",
        "apiOptionalParameters": {
          "apiCountryCode": "={{ $json.country || 'us' }}"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "prepare-content",
      "name": "Convert HTML to Plain Text",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        416
      ],
      "parameters": {
        "jsCode": "// Strip HTML to plain text, truncate, and carry source metadata forward\n// so the LLM gets a clean prompt.\n\nconst stripHtml = (s) => String(s == null ? '' : s)\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&nbsp;/gi, ' ')\n  .replace(/&amp;/gi, '&')\n  .replace(/&lt;/gi, '<')\n  .replace(/&gt;/gi, '>')\n  .replace(/&quot;/gi, '\"')\n  .replace(/&#39;/gi, \"'\")\n  .replace(/\\s+/g, ' ')\n  .trim();\n\nconst MAX_CHARS = 30000;\n\nreturn $input.all().map((item, i) => {\n  const meta = ($('Split Source Items').itemMatching(i) || {}).json || {};\n  const response = (item.json && item.json.response) || {};\n  const rawBody = response.body || '';\n  const text = stripHtml(rawBody).slice(0, MAX_CHARS);\n\n  return {\n    json: {\n      source_label: meta.label || '(untitled)',\n      source_url: meta.url || '',\n      status_code: response.statusCode || 0,\n      content: text,\n    },\n    pairedItem: { item: i },\n  };\n});\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "classify-sentiment",
      "name": "Sentiment Analysis Agent",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        752,
        416
      ],
      "parameters": {
        "text": "=Source: {{ $json.source_label }}\nURL: {{ $json.source_url }}\n\nContent:\n\n{{ $json.content }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You are a community sentiment analyst. The user will give you scraped content from a Reddit page \u2014 a subreddit listing (top/new/hot), a search results page, or a single thread. Pages are plain text stripped from HTML and may include Reddit navigation chrome; ignore the chrome and focus on the actual posts, comments and titles. Extract every distinct post or comment you can identify \u2014 a post title alone counts when no body text is available. Classify the sentiment of each (positive | neutral | negative), surface the most common themes (3-5 short labels), and pick up to 5 representative quotes (post titles are valid quotes for listing pages). Return ONLY a JSON object with this exact shape \u2014 no preamble, no markdown fence:\n\n{\n  \"overall_sentiment\": \"positive\" | \"neutral\" | \"negative\" | \"mixed\",\n  \"positive_count\": <integer>,\n  \"neutral_count\": <integer>,\n  \"negative_count\": <integer>,\n  \"top_themes\": [\"short label\", \"...\"],\n  \"sample_quotes\": [{\"sentiment\": \"positive\" | \"neutral\" | \"negative\", \"quote\": \"...\"}],\n  \"notes\": \"one short sentence summarizing tone\"\n}\n\nOnly return zero counts when the content genuinely contains no posts/comments (e.g. blocked page, empty listing). Never make up content."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "openai-chat-model",
      "name": "OpenAI GPT-5 Mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        752,
        576
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini"
        },
        "options": {
          "responseFormat": "json_object"
        },
        "responsesApiEnabled": false
      },
      "typeVersion": 1.3
    },
    {
      "id": "parse-classification",
      "name": "Normalize Sentiment Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        416
      ],
      "parameters": {
        "jsCode": "// Normalize the LLM's classification and re-attach source metadata.\n// With response_format: json_object the Basic LLM Chain surfaces the parsed\n// fields directly on item.json. Older n8n versions wrap them under text /\n// output / response \u2014 handle both shapes.\n\nreturn $input.all().map((item, i) => {\n  const j = item.json || {};\n\n  let parsed = j;\n  const looksParsed = parsed.overall_sentiment !== undefined\n    || parsed.positive_count !== undefined\n    || Array.isArray(parsed.sample_quotes);\n\n  if (!looksParsed) {\n    const raw = j.text || j.output || j.response || '';\n    try {\n      parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n    } catch (e) {\n      parsed = { error: 'Failed to parse model output', raw: String(raw).slice(0, 500) };\n    }\n  }\n\n  const meta = ($('Convert HTML to Plain Text').itemMatching(i) || {}).json || {};\n\n  return {\n    json: {\n      source_label: meta.source_label || '',\n      source_url: meta.source_url || '',\n      overall_sentiment: parsed.overall_sentiment || 'unknown',\n      positive_count: Number(parsed.positive_count) || 0,\n      neutral_count: Number(parsed.neutral_count) || 0,\n      negative_count: Number(parsed.negative_count) || 0,\n      top_themes: Array.isArray(parsed.top_themes) ? parsed.top_themes : [],\n      sample_quotes: Array.isArray(parsed.sample_quotes) ? parsed.sample_quotes : [],\n      notes: parsed.notes || '',\n    },\n    pairedItem: { item: i },\n  };\n});\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "build-weekly-digest",
      "name": "Compile Weekly Digest",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        416
      ],
      "parameters": {
        "jsCode": "// Aggregate all per-source classifications into a single weekly digest.\n// Emits one item with block-sized strings ready for the Notion node.\n\nconst items = $input.all().map(i => i.json).filter(Boolean);\nconst weekOf = new Date().toISOString().slice(0, 10);\n\nconst trunc = (s, n) => {\n  const str = String(s == null ? '' : s);\n  return str.length > n ? str.slice(0, n - 1) + '\u2026' : str;\n};\n\nconst totals = { positive: 0, neutral: 0, negative: 0 };\nconst themeCounts = new Map();\nconst sourceLines = [];\nconst quoteLines = [];\n\nfor (const it of items) {\n  const label = it.source_label || '(unknown source)';\n  const sentiment = it.overall_sentiment || 'unknown';\n  const pos = Number(it.positive_count) || 0;\n  const neu = Number(it.neutral_count) || 0;\n  const neg = Number(it.negative_count) || 0;\n  const themes = Array.isArray(it.top_themes) ? it.top_themes : [];\n  const quotes = Array.isArray(it.sample_quotes) ? it.sample_quotes : [];\n\n  totals.positive += pos;\n  totals.neutral  += neu;\n  totals.negative += neg;\n\n  for (const t of themes) {\n    const key = String(t).toLowerCase().trim();\n    if (!key) continue;\n    themeCounts.set(key, (themeCounts.get(key) || 0) + 1);\n  }\n\n  sourceLines.push(`\u2022 ${label} \u2192 ${sentiment} (pos ${pos} \u00b7 neu ${neu} \u00b7 neg ${neg})`);\n\n  for (const q of quotes.slice(0, 2)) {\n    const text = trunc(q.quote || '', 240);\n    quoteLines.push(`[${q.sentiment || '?'}] \"${text}\" \u2014 ${label}`);\n  }\n}\n\nconst total = totals.positive + totals.neutral + totals.negative;\nconst pct = (n) => total > 0 ? Math.round((n / total) * 100) : 0;\n\nconst topThemes = [...themeCounts.entries()]\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 5)\n  .map(([t]) => t);\n\nconst overall =\n  total === 0 ? 'no signal' :\n  totals.positive > totals.negative * 1.5 ? 'mostly positive' :\n  totals.negative > totals.positive * 1.5 ? 'mostly negative' :\n  'mixed';\n\nconst intro = `${items.length} source${items.length === 1 ? '' : 's'} analyzed for the week of ${weekOf}. Overall tone: ${overall}. ${total} reviews/comments extracted.`;\nconst stats = `Positive: ${totals.positive} (${pct(totals.positive)}%) \u00b7 Neutral: ${totals.neutral} (${pct(totals.neutral)}%) \u00b7 Negative: ${totals.negative} (${pct(totals.negative)}%)`;\nconst themes = topThemes.length ? topThemes.map((t, i) => `${i + 1}. ${t}`).join('\\n') : '(no themes surfaced)';\nconst sources = sourceLines.join('\\n') || '(no sources scraped)';\nconst quotes  = quoteLines.length ? quoteLines.join('\\n\\n') : '(no representative quotes)';\n\nreturn [{\n  json: {\n    page_title: `Weekly sentiment digest \u2014 ${weekOf}`,\n    week_of: weekOf,\n    total_sources: items.length,\n    total_reviews: total,\n    overall,\n    intro_block:   trunc(intro,   1800),\n    stats_block:   trunc(stats,   1800),\n    themes_block:  trunc(themes,  1800),\n    sources_block: trunc(sources, 1800),\n    quotes_block:  trunc(quotes,  1800),\n  },\n}];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "create-notion-page",
      "name": "Add Digest to Notion Page",
      "type": "n8n-nodes-base.notion",
      "position": [
        1664,
        416
      ],
      "parameters": {
        "title": "={{ $json.page_title }}",
        "blockUi": {
          "blockValues": [
            {
              "type": "heading_2",
              "richText": false,
              "textContent": "Overview"
            },
            {
              "type": "paragraph",
              "richText": false,
              "textContent": "={{ $json.intro_block }}"
            },
            {
              "type": "heading_2",
              "richText": false,
              "textContent": "Sentiment counts"
            },
            {
              "type": "paragraph",
              "richText": false,
              "textContent": "={{ $json.stats_block }}"
            },
            {
              "type": "heading_2",
              "richText": false,
              "textContent": "Top themes"
            },
            {
              "type": "paragraph",
              "richText": false,
              "textContent": "={{ $json.themes_block }}"
            },
            {
              "type": "heading_2",
              "richText": false,
              "textContent": "Per-source breakdown"
            },
            {
              "type": "paragraph",
              "richText": false,
              "textContent": "={{ $json.sources_block }}"
            },
            {
              "type": "heading_2",
              "richText": false,
              "textContent": "Sample quotes"
            },
            {
              "type": "paragraph",
              "richText": false,
              "textContent": "={{ $json.quotes_block }}"
            }
          ]
        },
        "options": {},
        "resource": "databasePage",
        "operation": "create",
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Source and Notion ID').first().json.notion_database_id }}"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "connections": {
    "OpenAI GPT-5 Mini": {
      "ai_languageModel": [
        [
          {
            "node": "Sentiment Analysis Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Split Source Items": {
      "main": [
        [
          {
            "node": "Scrape Data via ScraperAPI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Monday at 9am": {
      "main": [
        [
          {
            "node": "Set Source and Notion ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compile Weekly Digest": {
      "main": [
        [
          {
            "node": "Add Digest to Notion Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Sentiment Data": {
      "main": [
        [
          {
            "node": "Compile Weekly Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sentiment Analysis Agent": {
      "main": [
        [
          {
            "node": "Normalize Sentiment Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Source and Notion ID": {
      "main": [
        [
          {
            "node": "Split Source Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert HTML to Plain Text": {
      "main": [
        [
          {
            "node": "Sentiment Analysis Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Data via ScraperAPI": {
      "main": [
        [
          {
            "node": "Convert HTML to Plain Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs every Monday, scrapes weekly top posts from selected Reddit pages via ScraperAPI, analyzes sentiment and themes with OpenAI GPT-5 Mini, compiles a single weekly digest, and creates a formatted page in a Notion database. Runs every Monday at 9:00 AM on a…

Source: https://n8n.io/workflows/16447/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This n8n workflow automates the process of fetching, processing, and storing tech news articles from RSS feeds into a Notion database. It retrieves articles from The Verge and TechCrunch, processes th

OpenAI Chat, Chain Llm, Notion +3
AI & RAG

This n8n workflow automatically generates weekly Instagram Reel content ideas for a D2C brand using fresh Google News trends.

OpenAI Chat, RSS Feed Read, Chain Llm +1
AI & RAG

⚠️ DISCLAIMER: This workflow uses the AnySite LinkedIn community node, which is only available on self-hosted n8n instances. It will not work on n8n.cloud.

OpenAI Chat, Output Parser Structured, Google Sheets +6
AI & RAG

Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing

WordPress, OpenAI, Perplexity +8
AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10