AutomationFlowsAI & RAG › Send an Ai‑curated Weekly Sports Newsletter with Reddit, Gpt‑4o-mini, Gemini…

Send an Ai‑curated Weekly Sports Newsletter with Reddit, Gpt‑4o-mini, Gemini…

Original n8n title: Send an Ai‑curated Weekly Sports Newsletter with Reddit, Gpt‑4o-mini, Gemini and Outlook

ByAI Solutions @legalgpts on n8n.io

Automatically aggregates sports news for a configurable topic (e.g., "University of Florida Football" or "Atlanta Falcons") from Reddit, Google News, Yahoo Sports, NCAA.com, and BBC Sport, then curates and delivers a branded HTML email newsletter. Schedule Trigger fires weekly…

Cron / scheduled trigger★★★★★ complexityAI-powered35 nodesRedditRSS Feed ReadOpenAI ChatAgentGoogle GeminiHTTP RequestMicrosoft Outlook
AI & RAG Trigger: Cron / scheduled Nodes: 35 Complexity: ★★★★★ AI nodes: yes Added:
Send an Ai‑curated Weekly Sports Newsletter with Reddit, Gpt‑4o-mini, Gemini… — n8n workflow card showing Reddit, RSS Feed Read, OpenAI Chat integration

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

This workflow follows the Agent → Googlegemini 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
{
  "id": "lRHRDtEnZtPurZ0P",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "N8N-Sports Digest Newsletter",
  "tags": [
    {
      "id": "DrJfO3FIj4VGV9fL",
      "name": "Newsletter",
      "createdAt": "2025-06-14T22:02:24.176Z",
      "updatedAt": "2025-06-14T22:02:24.176Z"
    },
    {
      "id": "p7d66niWAG5ijZY7",
      "name": "AI",
      "createdAt": "2025-10-19T20:35:12.051Z",
      "updatedAt": "2026-03-22T18:22:29.537Z"
    },
    {
      "id": "hP3XwSnYlr76mfgu",
      "name": "Sports",
      "createdAt": "2026-04-18T05:49:56.619Z",
      "updatedAt": "2026-04-18T05:49:56.619Z"
    }
  ],
  "nodes": [
    {
      "id": "5e36beab-5978-4abd-94f5-f5e044496216",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 580,
        "height": 1176,
        "content": "## \ud83c\udfc6 Sports Digest \u2014 AI-Curated Weekly Newsletter\n\nAutomatically aggregates sports news for a **configurable topic** (e.g., \"University of Florida Football\" or \"Atlanta Falcons\") from Reddit, Google News, Yahoo Sports, NCAA.com, and BBC Sport, then curates and delivers a branded HTML email newsletter.\n\n### How it works\n1. **Schedule Trigger** fires weekly on Friday at 9 AM.\n2. **Config \u2014 Topic & Recipients** \u2014 Set your `topic`, `subreddit`, and `recipient_email` in one node.\n3. **News Collection** \u2014 Five sources run in parallel: Reddit, Google News RSS, Yahoo Sports RSS, NCAA.com RSS, and BBC Sport RSS.\n4. **Normalization** \u2014 Each source is standardized into a uniform schema (title, date, link, source, score, description).\n5. **Article Selection** \u2014 All sources are merged, deduplicated, filtered to the last 60 days, and 7 articles are randomly selected (\u22651 per source where available).\n6. **AI Curation** \u2014 GPT-4o-mini summarizes each article with a headline, 2-sentence summary, and \"why it matters\" note. Output is structured JSON.\n7. **Image Generation** \u2014 Gemini 2.5 Flash generates 2 AI images (for the top 2 articles) in parallel, each uploaded to your WordPress media library.\n8. **HTML Assembly** \u2014 Images are attached to the first 2 articles; all articles are rendered into a responsive branded HTML email.\n9. **Delivery** \u2014 Newsletter is sent via Microsoft Outlook to the configured recipient email.\n\n### Setup\n- **Config node**: Set `topic`, `subreddit`, and `recipient_email` before activating.\n- **Reddit**: Configure Reddit OAuth2 credential.\n- **OpenAI**: Add OpenAI API credential \u2014 requires GPT-4o-mini access.\n- **Gemini**: Add Google PaLM API credential for Gemini 2.5 Flash image generation.\n- **WordPress**: Add HTTP Basic Auth credential for your WordPress site (for image upload).\n- **Outlook**: Add Microsoft Outlook OAuth2 credential for email delivery.\n\n### Customization\n- Change the schedule in the trigger node (daily, weekly, etc.).\n- Adjust the article count (default: 7) in the **Select Articles** node.\n- Swap the WordPress upload for any image hosting service.\n- Update the HTML template in **Build Newsletter HTML** to match your brand colors.\n\n### Community Nodes\n\u26a0\ufe0f This workflow uses `@n8n/n8n-nodes-langchain.googleGemini` for AI image generation. This is a **LangChain community node** \u2014 requires a **self-hosted** n8n instance.\n\nvisit https://iportgpt.com/n8n_assets/sportsdig.html for instructions"
      },
      "typeVersion": 1
    },
    {
      "id": "7ff99a55-e0e6-487a-9ba1-b0d199c0080a",
      "name": "Section \u2014 Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 440,
        "content": "## \u2699\ufe0f Configuration\nEdit the **Config \u2014 Topic & Recipients** Set node.\nAll downstream nodes read `topic`, `subreddit`, and `recipient_email` from this single source."
      },
      "typeVersion": 1
    },
    {
      "id": "b1af7d49-50a2-4e28-b14b-2a5c440f3d3e",
      "name": "Section \u2014 News Collection",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 1112,
        "content": "## \ud83d\udce5 News Collection\nFetches up to 25 posts from each of 5 parallel sources: Reddit, Google News RSS, Yahoo Sports RSS, NCAA.com RSS, and BBC Sport RSS."
      },
      "typeVersion": 1
    },
    {
      "id": "6792ee90-3a66-44e2-8a12-f27baed61b3a",
      "name": "Section \u2014 Normalize & Select",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1184,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 584,
        "height": 1112,
        "content": "## \ud83d\udd04 Normalize & Select\nEach source is normalized to a uniform schema. All items are merged, deduplicated, filtered to 60 days, and 7 articles are randomly selected (\u22651 per source where available)."
      },
      "typeVersion": 1
    },
    {
      "id": "96c99b81-c3da-40a9-b732-565cdbf42bf6",
      "name": "Section \u2014 AI Generation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1776,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 1312,
        "height": 840,
        "content": "## \ud83e\udd16 AI Content Generation\nGPT-4o-mini summarizes selected articles into newsletter-ready JSON. Gemini 2.5 Flash generates **2 images** (top 2 articles only) in parallel and uploads them to WordPress."
      },
      "typeVersion": 1
    },
    {
      "id": "39f035ee-3b27-4b81-b305-5a18dc15552b",
      "name": "Section \u2014 Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3104,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 840,
        "content": "## \ud83d\udce4 Delivery\nSends the assembled HTML newsletter via Microsoft Outlook to the `recipient_email` configured in the Config node. Replace with Gmail or SMTP as needed."
      },
      "typeVersion": 1
    },
    {
      "id": "229b384a-8985-4964-9e86-4c7572ddf32c",
      "name": "Warning \u2014 Community Node",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2288,
        592
      ],
      "parameters": {
        "color": 3,
        "width": 300,
        "height": 148,
        "content": "\u26a0\ufe0f **Community Node Required**\n`googleGemini` is a LangChain community node.\n**Self-hosted n8n only.**\nUpdate the model ID if `gemini-2.5-flash-image` is unavailable in your instance."
      },
      "typeVersion": 1
    },
    {
      "id": "c5be9576-f160-44ca-9e06-e4993b64844d",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        640,
        256
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                5
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "e7032b11-d87b-4b71-b0f6-8c8bbeb2ca7e",
      "name": "Config \u2014 Topic & Recipients",
      "type": "n8n-nodes-base.set",
      "position": [
        832,
        256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "config-topic",
              "name": "topic",
              "type": "string",
              "value": "Your Sports Topic Here"
            },
            {
              "id": "config-subreddit",
              "name": "subreddit",
              "type": "string",
              "value": "your_subreddit"
            },
            {
              "id": "config-recipient",
              "name": "recipient_email",
              "type": "string",
              "value": "your@email.com"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b72145e5-3f2a-4129-b27d-9f806bed3053",
      "name": "Fetch \u2014 Reddit Posts",
      "type": "n8n-nodes-base.reddit",
      "position": [
        1024,
        256
      ],
      "parameters": {
        "limit": 25,
        "filters": {
          "category": "new"
        },
        "operation": "getAll",
        "subreddit": "={{ $('Config \u2014 Topic & Recipients').first().json.subreddit }}"
      },
      "credentials": {
        "redditOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6fc7e10e-5a5d-480c-83c0-f34096b89ecb",
      "name": "Fetch \u2014 Google News RSS",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        1024,
        416
      ],
      "parameters": {
        "url": "={{ 'https://news.google.com/rss/search?q=' + encodeURIComponent($('Config \u2014 Topic & Recipients').first().json.topic) + '&hl=en-US&gl=US&ceid=US:en' }}",
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "3d8d1b51-edae-419c-8529-06379c426900",
      "name": "Fetch \u2014 NCAA RSS",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        1024,
        768
      ],
      "parameters": {
        "url": "https://www.ncaa.com/news/ncaa/d1/rss.xml",
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "1141aa5c-4815-4636-ad48-459c35a1e5f1",
      "name": "Normalize \u2014 Reddit",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        256
      ],
      "parameters": {
        "jsCode": "// Format Reddit posts to standard structure\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const j = item.json;\n  if (j.selftext === '[removed]' || j.removed_by_category) continue;\n  const link = j.url || (j.permalink ? 'https://reddit.com' + j.permalink : '');\n  if (!j.title || !link) continue;\n  let date = '';\n  if (j.created_utc) date = new Date(j.created_utc * 1000).toISOString().split('T')[0];\n  else if (j.created) date = new Date(j.created * 1000).toISOString().split('T')[0];\n  results.push({\n    json: {\n      title: j.title,\n      date: date,\n      link: link,\n      source: 'Reddit',\n      score: j.score || j.ups || 0,\n      description: (j.selftext || '').substring(0, 300)\n    }\n  });\n}\n\nreturn results.length > 0 ? results : [{ json: { empty: true, source: 'Reddit' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6326982f-7525-4ca7-90e6-00e7e2db64e0",
      "name": "Normalize \u2014 Google News",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        416
      ],
      "parameters": {
        "jsCode": "// Format Google News RSS items to standard structure\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const j = item.json;\n  const link = j.link || j.url || j.guid || '';\n  if (!j.title || !link) continue;\n  let date = '';\n  if (j.pubDate) date = new Date(j.pubDate).toISOString().split('T')[0];\n  else if (j.isoDate) date = new Date(j.isoDate).toISOString().split('T')[0];\n  let description = (j.description || j.content || j.contentSnippet || '')\n    .replace(/<[^>]*>/g, '').substring(0, 300);\n  results.push({\n    json: { title: j.title, date: date, link: link, source: 'Google News', score: 0, description: description }\n  });\n}\n\nreturn results.length > 0 ? results : [{ json: { empty: true, source: 'Google News' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "5cc2e4de-53a5-4d16-b99d-018d084fc56d",
      "name": "Normalize \u2014 NCAA",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        768
      ],
      "parameters": {
        "jsCode": "// Normalize NCAA.com RSS feed\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const j = item.json;\n  const link = j.link || j.url || j.guid || '';\n  if (!j.title || !link) continue;\n  let date = '';\n  if (j.pubDate) date = new Date(j.pubDate).toISOString().split('T')[0];\n  else if (j.isoDate) date = new Date(j.isoDate).toISOString().split('T')[0];\n  let description = (j.description || j.contentSnippet || '').replace(/<[^>]*>/g, '').substring(0, 300);\n  results.push({\n    json: { title: j.title, date: date, link: link, source: 'NCAA.com', score: 0, description: description }\n  });\n}\n\nreturn results.length > 0 ? results : [{ json: { empty: true, source: 'NCAA.com' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6c047b36-a3e6-410e-8684-4369c2520d2e",
      "name": "Merge \u2014 All Sources",
      "type": "n8n-nodes-base.merge",
      "position": [
        1520,
        272
      ],
      "parameters": {
        "numberInputs": 5
      },
      "typeVersion": 3.2
    },
    {
      "id": "5859c918-1e0d-49cb-a174-d62fc23dc472",
      "name": "Select \u2014 7 Articles",
      "type": "n8n-nodes-base.code",
      "position": [
        1664,
        320
      ],
      "parameters": {
        "jsCode": "// Randomly select 7 articles with representation from all available sources\n// ONLY include articles from the last 60 days\nconst items = $input.all();\nconst topic = ($('Config \u2014 Topic & Recipients').first().json.topic || '').toLowerCase();\nconst seen = new Set();\n\nconst now = new Date();\nconst cutoffDate = new Date(now.getTime() - (60 * 24 * 60 * 60 * 1000));\nconst cutoffStr = cutoffDate.toISOString().split('T')[0];\n\nfunction shuffle(arr) {\n  const a = [...arr];\n  for (let i = a.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    [a[i], a[j]] = [a[j], a[i]];\n  }\n  return a;\n}\n\nconst sources = ['Reddit', 'Google News', 'Yahoo Sports', 'NCAA.com', 'BBC Sport'];\nconst bySource = {};\nfor (const s of sources) bySource[s] = [];\n\nfor (const item of items) {\n  const j = item.json;\n  if (j.empty || !j.title || !j.link || seen.has(j.link)) continue;\n  seen.add(j.link);\n  if (j.date && j.date < cutoffStr) continue;\n  const article = { title: j.title, date: j.date || '', link: j.link, source: j.source };\n  if (bySource[j.source]) bySource[j.source].push(article);\n}\n\n// Take up to 1 guaranteed from each source, then fill remainder randomly\nconst selected = [];\nfor (const s of sources) {\n  const shuffled = shuffle(bySource[s]);\n  if (shuffled.length > 0) selected.push(shuffled[0]);\n  bySource[s] = shuffled.slice(1);\n}\n\nconst pool = shuffle([].concat(...sources.map(s => bySource[s])));\nwhile (selected.length < 7 && pool.length > 0) selected.push(pool.shift());\n\nreturn [{ json: {\n  articles: shuffle(selected),\n  topic: topic,\n  cutoffDate: cutoffStr,\n  articlesSelected: selected.length\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "cf011e05-5e05-43d7-8a55-dd649ec26bfc",
      "name": "LLM \u2014 GPT-4o-mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1856,
        480
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2eea4858-9a72-4044-8807-1a0d9e19b1e9",
      "name": "AI \u2014 Summarize Articles",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1856,
        320
      ],
      "parameters": {
        "text": "=Summarize these {{ $json.articles.length }} articles for a sports newsletter about: {{ $('Config \u2014 Topic & Recipients').first().json.topic }}\n\n{{ JSON.stringify($json.articles) }}\n\nReturn JSON:\n{\"articles\":[{\"headline\":\"...\",\"summary\":\"2 sentences\",\"relevance\":\"Why it matters for fans\",\"source_url\":\"use exact link from input\",\"source_platform\":\"Reddit/Google News/Yahoo Sports/NCAA.com/BBC Sport\"}],\"newsletter_date\":\"{{ $now.format('MMMM d, yyyy') }}\",\"intro_summary\":\"2 sentence overview\"}",
        "options": {
          "systemMessage": "You are a sports journalist. Write concise summaries relevant to the specified topic. Return valid JSON only, no markdown."
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6d5b1429-af86-42ca-9ef6-bf534b02f421",
      "name": "Parse AI \u2014 Build Image Prompts",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        320
      ],
      "parameters": {
        "jsCode": "// Extract first 2 articles and create image prompts (only 2 images)\nconst aiResponse = $input.item.json.output || $input.item.json.data || '';\nlet data;\n\ntry {\n  const match = aiResponse.match(/\\{[\\s\\S]*\\}/);\n  data = match ? JSON.parse(match[0]) : null;\n  if (!data || !data.articles) throw new Error('No articles found');\n} catch (e) {\n  return [{ json: { error: 'Could not parse AI response', articles: [] } }];\n}\n\nconst articlesWithImages = data.articles.map((article, index) => ({\n  ...article,\n  imageIndex: index,\n  hasImage: index < 2,\n  imagePrompt: index < 2 ? 'Dynamic sports action illustration for: \"' + article.headline + '\". Style: vivid, athletic, high energy, photorealistic. Aspect ratio 16:9. Sports theme.' : null\n}));\n\nreturn [{\n  json: {\n    ...data,\n    articles: articlesWithImages,\n    articlesForImages: articlesWithImages.filter(a => a.hasImage),\n    newsletter_date: data.newsletter_date,\n    intro_summary: data.intro_summary\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "258aaa8d-be26-4af1-907b-c5a20a242cb2",
      "name": "Image Prompt \u2014 Article 1",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        240
      ],
      "parameters": {
        "jsCode": "// Extract first article's image prompt\nconst articles = $input.item.json.articles || [];\nconst firstArticle = articles.find(a => a.imageIndex === 0);\nif (!firstArticle || !firstArticle.imagePrompt) {\n  return [{ json: { prompt: 'Dynamic sports action illustration. Style: vivid, athletic, photorealistic.', articleIndex: 0 } }];\n}\nreturn [{ json: { prompt: firstArticle.imagePrompt, articleIndex: 0 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6d904989-b20a-4561-b40b-95088447cab6",
      "name": "Image Prompt \u2014 Article 2",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        400
      ],
      "parameters": {
        "jsCode": "// Extract second article's image prompt\nconst articles = $input.item.json.articles || [];\nconst secondArticle = articles.find(a => a.imageIndex === 1);\nif (!secondArticle || !secondArticle.imagePrompt) {\n  return [{ json: { prompt: 'Dynamic sports action illustration. Style: vivid, athletic, photorealistic.', articleIndex: 1 } }];\n}\nreturn [{ json: { prompt: secondArticle.imagePrompt, articleIndex: 1 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3e4eaaff-de3a-4047-b71c-c0a290fdb40a",
      "name": "Gemini \u2014 Generate Image 1",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2480,
        240
      ],
      "parameters": {
        "prompt": "={{ $json.prompt }}",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash-image",
          "cachedResultName": "models/gemini-2.5-flash-image"
        },
        "options": {
          "sampleCount": 1,
          "binaryPropertyOutput": "image1"
        },
        "resource": "image"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b1f507c7-f5c6-4c32-9368-ef9f945810c9",
      "name": "Gemini \u2014 Generate Image 2",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2480,
        400
      ],
      "parameters": {
        "prompt": "={{ $json.prompt }}",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash-image",
          "cachedResultName": "models/gemini-2.5-flash-image"
        },
        "options": {
          "sampleCount": 1,
          "binaryPropertyOutput": "image2"
        },
        "resource": "image"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7d00b595-76de-4337-b2bd-63a60f792829",
      "name": "Merge \u2014 Generated Images",
      "type": "n8n-nodes-base.merge",
      "position": [
        2640,
        320
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2,
      "alwaysOutputData": true
    },
    {
      "id": "05fe2af7-c1ef-4bbc-aef9-2f2a94463f9d",
      "name": "Prep \u2014 Images for Upload",
      "type": "n8n-nodes-base.code",
      "position": [
        2800,
        320
      ],
      "parameters": {
        "jsCode": "// Prepare the 2 generated images for WordPress upload (one item per image)\nconst merged = $input.first();\nconst binaries = merged.binary || {};\nconst keys = ['image1','image2'];\nconst out = [];\n\nfor (let i = 0; i < keys.length; i++) {\n  const k = keys[i];\n  const b = binaries[k];\n  if (!b) continue;\n  const fileName = 'sports-digest-img-' + (i+1) + '.png';\n  out.push({\n    json: { image_index: i, filename: fileName },\n    binary: { data: { ...b, fileName } }\n  });\n}\n\nreturn out.length ? out : [{ json: { image_index: -1, filename: 'sports-digest-img-0.png', no_images: true } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "5411d07f-0eef-4708-9901-6ac77cf950de",
      "name": "WP \u2014 Upload Image",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2992,
        320
      ],
      "parameters": {
        "url": "https://YOUR-WORDPRESS-SITE.com/wp-json/wp/v2/media",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "binaryData",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Disposition",
              "value": "=attachment; filename=\"{{ $json.filename }}\""
            }
          ]
        },
        "inputDataFieldName": "data"
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "2e7e7364-f17b-497b-89eb-f0fccbd8d8d6",
      "name": "Collect \u2014 Image URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        320
      ],
      "parameters": {
        "jsCode": "// Collect WordPress media URLs back into an imageMap keyed by article index\nconst items = $input.all();\nconst imageMap = {};\n\nfor (const item of items) {\n  const j = item.json || {};\n  const url = j.source_url || (j.guid && j.guid.rendered) || (j.media_details && j.media_details.sizes && j.media_details.sizes.full && j.media_details.sizes.full.source_url);\n  const slug = j.slug || '';\n  const title = (j.title && (j.title.rendered || j.title)) || '';\n  const m = slug.match(/sports-digest-img-(\\d+)/i) || title.match(/sports-digest-img-(\\d+)/i);\n  if (!url || !m) continue;\n  const idx = parseInt(m[1], 10) - 1;\n  if (Number.isFinite(idx) && idx >= 0 && idx <= 1) imageMap['image_' + idx] = url;\n}\n\nreturn [{ json: { imageMap } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a6d66f8b-d31c-4f6f-a3ad-be4498b121a0",
      "name": "Attach \u2014 Images to Articles",
      "type": "n8n-nodes-base.code",
      "position": [
        3360,
        320
      ],
      "parameters": {
        "jsCode": "// Merge article data with uploaded image URLs\nconst articleData = $('Parse AI \u2014 Build Image Prompts').first().json;\nconst imageData = $input.item.json.imageMap || {};\n\nconst articlesWithImages = (articleData.articles || []).map((article, index) => {\n  const url = imageData['image_' + index];\n  if (index < 2 && url) {\n    return { ...article, imageUrl: url, imageWidth: '180px', imageHeight: 'auto' };\n  }\n  return article;\n});\n\nreturn [{\n  json: {\n    articles: articlesWithImages,\n    newsletter_date: articleData.newsletter_date,\n    intro_summary: articleData.intro_summary,\n    topic: $('Config \u2014 Topic & Recipients').first().json.topic\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "35db228f-1c32-4be8-9c0f-9bc8be5c7b7b",
      "name": "Build \u2014 Newsletter HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        3552,
        320
      ],
      "parameters": {
        "jsCode": "// Build branded HTML newsletter\n// Update colors and branding below to match your organization\nconst data = $input.item.json;\nconst topic = data.topic || 'Sports Digest';\n\n// === BRANDING \u2014 customize these values ===\nconst brandPrimary = '#002657';    // Navy\nconst brandAccent = '#FA4616';     // Orange\nconst brandLight = '#c9c7c8';      // Silver\nconst logoUrl = 'YOUR-LOGO-URL-HERE';\nconst logoAlt = 'Your Organization';\nconst footerText = 'Curated by Your Organization \u00b7 Powered by AI';\n// ==========================================\n\nconst articlesHtml = (data.articles || []).map((a, idx) => {\n  let imageHtml = '';\n  if (a.imageUrl) {\n    imageHtml = '<div style=\"margin-bottom:16px;overflow:hidden;border-radius:8px;\"><img src=\"' + a.imageUrl + '\" alt=\"' + (a.headline || '') + '\" style=\"width:100%;height:160px;object-fit:cover;object-position:center;display:block;\"></div>';\n  }\n  const borderColor = idx === 0 ? brandAccent : brandPrimary;\n  return '<tr><td style=\"padding:20px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;border-left:4px solid ' + borderColor + ';\">' +\n    imageHtml +\n    '<h3 style=\"margin:0 0 12px;color:' + brandPrimary + ';font-size:17px;line-height:1.4;\">' + (a.headline || '') + '</h3>' +\n    '<p style=\"margin:0 0 12px;color:#374151;font-size:14px;line-height:1.6;\">' + (a.summary || '') + '</p>' +\n    '<p style=\"margin:0 0 12px;color:#6b7280;font-size:13px;font-style:italic;\"><b>Why it matters:</b> ' + (a.relevance || '') + '</p>' +\n    '<p style=\"margin:0;font-size:12px;\"><a href=\"' + (a.source_url || '#') + '\" style=\"color:' + brandAccent + ';text-decoration:none;font-weight:600;\">Read full article \u2192</a> <span style=\"color:#9ca3af;\">[' + (a.source_platform || 'Source') + ']</span></p>' +\n    '</td></tr><tr><td style=\"height:16px;\"></td></tr>';\n}).join('');\n\nconst html = '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>' + topic + ' Digest</title></head>' +\n  '<body style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif;padding:20px;background:#f5f5f5;margin:0;\">' +\n  '<div style=\"max-width:680px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 12px rgba(0,0,0,0.15);\">' +\n  '<div style=\"background:' + brandPrimary + ';padding:28px 24px 20px;text-align:center;\">' +\n  '<h1 style=\"margin:0;color:#ffffff;font-size:24px;letter-spacing:1px;text-transform:uppercase;font-weight:700;\">' + topic + ' Digest</h1>' +\n  '<p style=\"margin:6px 0 0;color:' + brandLight + ';font-size:13px;letter-spacing:0.5px;\">' + (data.newsletter_date || '') + '</p>' +\n  '</div>' +\n  '<div style=\"padding:20px 24px;background:' + brandAccent + ';border-bottom:3px solid ' + brandPrimary + ';\">' +\n  '<p style=\"margin:0;color:#ffffff;font-size:15px;line-height:1.6;\">' + (data.intro_summary || '') + '</p>' +\n  '</div>' +\n  '<div style=\"padding:24px;\">' +\n  '<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"width:100%;border-collapse:collapse;\">' +\n  articlesHtml +\n  '</table></div>' +\n  '<div style=\"padding:24px;background:' + brandPrimary + ';border-top:3px solid ' + brandAccent + ';text-align:center;\">' +\n  '<p style=\"margin:4px 0 0;color:' + brandLight + ';font-size:12px;font-weight:600;\">' + footerText + '</p>' +\n  '</div>' +\n  '</div></body></html>';\n\nreturn [{ json: { newsletter_html: html, newsletter_date: data.newsletter_date, topic: topic, article_count: (data.articles || []).length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "cdb76e88-2609-4088-8e99-98e00662cb3d",
      "name": "Outlook \u2014 Send Newsletter",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        3760,
        320
      ],
      "parameters": {
        "subject": "={{ $('Config \u2014 Topic & Recipients').first().json.topic }} Digest \u2014 {{ $now.toFormat('MMMM d, yyyy') }}",
        "bodyContent": "={{ $('Build \u2014 Newsletter HTML').first().json.newsletter_html }}",
        "toRecipients": "={{ $('Config \u2014 Topic & Recipients').first().json.recipient_email }}",
        "additionalFields": {
          "bodyContentType": "html"
        }
      },
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b012cace-ab94-4942-8837-ad5459627551",
      "name": "Parse \u2014 Yahoo Sports XML",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        576
      ],
      "parameters": {
        "jsCode": "const xml = $input.first().json.data;\n\nconst items = [];\nconst itemRegex = /<item[^>]*>([\\s\\S]*?)<\\/item>/gi;\nconst titleRegex = /<title><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/title>|<title>([\\s\\S]*?)<\\/title>/i;\nconst linkRegex = /<link>([\\s\\S]*?)<\\/link>/i;\nconst descRegex = /<description><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/description>|<description>([\\s\\S]*?)<\\/description>/i;\nconst pubDateRegex = /<pubDate>([\\s\\S]*?)<\\/pubDate>/i;\nconst guidRegex = /<guid[^>]*>([\\s\\S]*?)<\\/guid>/i;\nconst categoryRegex = /<category><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/category>|<category>([\\s\\S]*?)<\\/category>/gi;\n\nlet match;\nwhile ((match = itemRegex.exec(xml)) !== null) {\n  const itemXml = match[1];\n  const titleMatch = titleRegex.exec(itemXml);\n  const linkMatch = linkRegex.exec(itemXml);\n  const descMatch = descRegex.exec(itemXml);\n  const pubDateMatch = pubDateRegex.exec(itemXml);\n  const guidMatch = guidRegex.exec(itemXml);\n  const categories = [];\n  let catMatch;\n  while ((catMatch = categoryRegex.exec(itemXml)) !== null) {\n    categories.push(catMatch[1] || catMatch[2] || '');\n  }\n  items.push({\n    json: {\n      title: (titleMatch && (titleMatch[1] || titleMatch[2]) || '').trim(),\n      link: (linkMatch && linkMatch[1] || '').trim(),\n      description: (descMatch && (descMatch[1] || descMatch[2]) || '').replace(/<[^>]+>/g, '').trim(),\n      pubDate: (pubDateMatch && pubDateMatch[1] || '').trim(),\n      guid: (guidMatch && guidMatch[1] || '').trim(),\n      categories: categories,\n      source: 'Yahoo Sports'\n    }\n  });\n}\n\nif (items.length === 0) {\n  return [{ json: { error: 'No items parsed', rawSnippet: xml.substring(0, 500) } }];\n}\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "6cf88b96-1f9f-440d-ae9a-70770677a522",
      "name": "Fetch \u2014 Yahoo Sports RSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1024,
        576
      ],
      "parameters": {
        "url": "https://sports.yahoo.com/rss/",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-bot/1.0)"
            },
            {
              "name": "Accept",
              "value": "application/rss+xml, application/xml, text/xml"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "98769e5f-b7c9-415b-b4bd-b68cbafe370b",
      "name": "Parse \u2014 BBC Sport XML",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        928
      ],
      "parameters": {
        "jsCode": "const xml = $input.first().json.data;\n\nconst items = [];\nconst itemRegex = /<item[^>]*>([\\s\\S]*?)<\\/item>/gi;\nconst titleRegex = /<title><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/title>|<title>([\\s\\S]*?)<\\/title>/i;\nconst linkRegex = /<link>([\\s\\S]*?)<\\/link>/i;\nconst descRegex = /<description><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/description>|<description>([\\s\\S]*?)<\\/description>/i;\nconst pubDateRegex = /<pubDate>([\\s\\S]*?)<\\/pubDate>/i;\nconst guidRegex = /<guid[^>]*>([\\s\\S]*?)<\\/guid>/i;\nconst categoryRegex = /<category><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/category>|<category>([\\s\\S]*?)<\\/category>/gi;\n\nlet match;\nwhile ((match = itemRegex.exec(xml)) !== null) {\n  const itemXml = match[1];\n  const titleMatch = titleRegex.exec(itemXml);\n  const linkMatch = linkRegex.exec(itemXml);\n  const descMatch = descRegex.exec(itemXml);\n  const pubDateMatch = pubDateRegex.exec(itemXml);\n  const guidMatch = guidRegex.exec(itemXml);\n  const categories = [];\n  let catMatch;\n  while ((catMatch = categoryRegex.exec(itemXml)) !== null) {\n    categories.push(catMatch[1] || catMatch[2] || '');\n  }\n  items.push({\n    json: {\n      title: (titleMatch && (titleMatch[1] || titleMatch[2]) || '').trim(),\n      link: (linkMatch && linkMatch[1] || '').trim(),\n      description: (descMatch && (descMatch[1] || descMatch[2]) || '').replace(/<[^>]+>/g, '').trim(),\n      pubDate: (pubDateMatch && pubDateMatch[1] || '').trim(),\n      guid: (guidMatch && guidMatch[1] || '').trim(),\n      categories: categories,\n      source: 'BBC Sport'\n    }\n  });\n}\n\nif (items.length === 0) {\n  return [{ json: { error: 'No items parsed', rawSnippet: xml.substring(0, 500) } }];\n}\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "8c04763b-a87e-4bce-b483-a04a59e682a6",
      "name": "Fetch \u2014 BBC Sport RSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1024,
        928
      ],
      "parameters": {
        "url": "https://feeds.bbci.co.uk/sport/rss.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-bot/1.0)"
            },
            {
              "name": "Accept",
              "value": "application/rss+xml, application/xml, text/xml"
            }
          ]
        }
      },
      "typeVersion": 4.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "5004c787-115a-48ec-9710-21a0c1be2a3a",
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Config \u2014 Topic & Recipients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch \u2014 NCAA RSS": {
      "main": [
        [
          {
            "node": "Normalize \u2014 NCAA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize \u2014 NCAA": {
      "main": [
        [
          {
            "node": "Merge \u2014 All Sources",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "LLM \u2014 GPT-4o-mini": {
      "ai_languageModel": [
        [
          {
            "node": "AI \u2014 Summarize Articles",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "WP \u2014 Upload Image": {
      "main": [
        [
          {
            "node": "Collect \u2014 Image URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize \u2014 Reddit": {
      "main": [
        [
          {
            "node": "Merge \u2014 All Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge \u2014 All Sources": {
      "main": [
        [
          {
            "node": "Select \u2014 7 Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select \u2014 7 Articles": {
      "main": [
        [
          {
            "node": "AI \u2014 Summarize Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect \u2014 Image URLs": {
      "main": [
        [
          {
            "node": "Attach \u2014 Images to Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch \u2014 Reddit Posts": {
      "main": [
        [
          {
            "node": "Normalize \u2014 Reddit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch \u2014 BBC Sport RSS": {
      "main": [
        [
          {
            "node": "Parse \u2014 BBC Sport XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse \u2014 BBC Sport XML": {
      "main": [
        [
          {
            "node": "Merge \u2014 All Sources",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "AI \u2014 Summarize Articles": {
      "main": [
        [
          {
            "node": "Parse AI \u2014 Build Image Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build \u2014 Newsletter HTML": {
      "main": [
        [
          {
            "node": "Outlook \u2014 Send Newsletter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch \u2014 Google News RSS": {
      "main": [
        [
          {
            "node": "Normalize \u2014 Google News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize \u2014 Google News": {
      "main": [
        [
          {
            "node": "Merge \u2014 All Sources",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch \u2014 Yahoo Sports RSS": {
      "main": [
        [
          {
            "node": "Parse \u2014 Yahoo Sports XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Prompt \u2014 Article 1": {
      "main": [
        [
          {
            "node": "Gemini \u2014 Generate Image 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Prompt \u2014 Article 2": {
      "main": [
        [
          {
            "node": "Gemini \u2014 Generate Image 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge \u2014 Generated Images": {
      "main": [
        [
          {
            "node": "Prep \u2014 Images for Upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse \u2014 Yahoo Sports XML": {
      "main": [
        [
          {
            "node": "Merge \u2014 All Sources",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Prep \u2014 Images for Upload": {
      "main": [
        [
          {
            "node": "WP \u2014 Upload Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini \u2014 Generate Image 1": {
      "main": [
        [
          {
            "node": "Merge \u2014 Generated Images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini \u2014 Generate Image 2": {
      "main": [
        [
          {
            "node": "Merge \u2014 Generated Images",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Attach \u2014 Images to Articles": {
      "main": [
        [
          {
            "node": "Build \u2014 Newsletter HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config \u2014 Topic & Recipients": {
      "main": [
        [
          {
            "node": "Fetch \u2014 Reddit Posts",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch \u2014 Google News RSS",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch \u2014 NCAA RSS",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch \u2014 Yahoo Sports RSS",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch \u2014 BBC Sport RSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI \u2014 Build Image Prompts": {
      "main": [
        [
          {
            "node": "Image Prompt \u2014 Article 1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Image Prompt \u2014 Article 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Automatically aggregates sports news for a configurable topic (e.g., "University of Florida Football" or "Atlanta Falcons") from Reddit, Google News, Yahoo Sports, NCAA.com, and BBC Sport, then curates and delivers a branded HTML email newsletter. Schedule Trigger fires weekly…

Source: https://n8n.io/workflows/15136/ — 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 workflow automatically generates and publishes a full-length, SEO-optimized WordPress article on a weekly schedule. Topics are sourced from a SharePoint list. The AI agent writes the article body

Microsoft SharePoint, HTTP Request, OpenAI +6
AI & RAG

We’ve released Version 4 of our AI Powered Blog Automation workflow. We heard your complains and made a complete redesign built for serious content creators.

RSS Feed Read, OpenAI Chat, Text Classifier +6
AI & RAG

Automate Microsoft Teams Meeting Analysis with GPT-4.1, Outlook & Mem.ai Watch the YouTube video to get started Follow along with the blog post

Postgres, OpenAI Chat, HTTP Request +3
AI & RAG

Most blogs publish words. This system publishes experiences.

OpenAI Chat, HTTP Request, Output Parser Structured +7
AI & RAG

kisisel asistan. Uses toolWorkflow, toolHttpRequest, toolCalculator, toolThink. Scheduled trigger; 43 nodes.

Tool Workflow, Tool Http Request, Tool Calculator +15