AutomationFlowsWeb Scraping › Generate Daily AI Podcast from RSS Feeds

Generate Daily AI Podcast from RSS Feeds

Original n8n title: AI Digest Daily Podcast

AI Digest Daily Podcast. Uses rssFeedRead, httpRequest. Event-driven trigger; 11 nodes.

Event trigger★★★★☆ complexity11 nodesRSS Feed ReadHTTP Request
Web Scraping Trigger: Event Nodes: 11 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → RSS Feed Read 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
{
  "name": "AI Digest Daily Podcast",
  "nodes": [
    {
      "parameters": {},
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        256,
        400
      ],
      "id": "e379bcc7-63f8-4515-a348-5391b4cbe08c"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "name": "Schedule: 6am Daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        256,
        200
      ],
      "id": "af5337d4-cc78-4080-a09c-b1630eae8ed9"
    },
    {
      "parameters": {
        "jsCode": "const feeds = [\n  'https://jack-clark.net/feed/',\n  'https://www.latent.space/feed',\n  'https://datamachina.substack.com/feed',\n  'https://www.interconnects.ai/feed',\n  'https://magazine.sebastianraschka.com/feed',\n  'https://www.aisnakeoil.com/feed',\n  'https://www.supervised.news/feed',\n  'https://nextword.substack.com/feed',\n  'https://www.aitidbits.ai/feed',\n  'https://thesequence.substack.com/feed'\n];\nreturn feeds.map(url => ({ json: { url } }));"
      },
      "name": "Load Feeds",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        464,
        304
      ],
      "id": "fa798565-09be-4a3f-9367-9e213913f34e"
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {}
      },
      "name": "RSS Read",
      "type": "n8n-nodes-base.rssFeedRead",
      "typeVersion": 1,
      "position": [
        656,
        304
      ],
      "id": "e7af0240-ccc0-44bd-90f6-c4648cc1500b",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "function getDomain(url) {\n  try {\n    const match = (url || '').match(/https?:\\/\\/([^\\/]+)/);\n    return match ? match[1].replace('www.', '') : 'unknown';\n  } catch { return 'unknown'; }\n}\n\nconst items = $input.all();\nconst now = new Date();\nconst cutoff24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);\nconst cutoff72h = new Date(now.getTime() - 72 * 60 * 60 * 1000);\n\n// Extract articles from RSS items\nlet articles = items\n  .filter(item => item.json.title && !item.json.error)\n  .map(item => ({\n    title: item.json.title || '',\n    link: item.json.link || '',\n    snippet: (item.json.contentSnippet || '').substring(0, 300),\n    pubDate: item.json.isoDate || item.json.pubDate || '',\n    source: getDomain(item.json.link)\n  }));\n\nif (articles.length === 0) {\n  throw new Error('No articles fetched from any feed');\n}\n\n// Filter to last 24h, fallback to 72h\nlet recent = articles.filter(a => {\n  if (!a.pubDate) return true;\n  return new Date(a.pubDate) >= cutoff24h;\n});\nif (recent.length < 5) {\n  recent = articles.filter(a => {\n    if (!a.pubDate) return true;\n    return new Date(a.pubDate) >= cutoff72h;\n  });\n}\n\n// Dedup by normalized title\nconst seen = new Set();\nconst unique = recent.filter(a => {\n  const key = a.title.toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 50);\n  if (seen.has(key)) return false;\n  seen.add(key);\n  return true;\n});\n\n// Sort newest first\nconst sorted = unique.sort((a, b) => new Date(b.pubDate || 0) - new Date(a.pubDate || 0));\n\n// Diversify: max 2 per source\nconst counts = {};\nconst diverse = sorted.filter(a => {\n  counts[a.source] = (counts[a.source] || 0) + 1;\n  return counts[a.source] <= 2;\n}).slice(0, 8);\n\nconst today = now.toLocaleDateString('en-US', {\n  weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n});\n\nconst articlesText = diverse.map((a, i) =>\n  `${i + 1}. ${a.title} (${a.source})\\n${a.snippet}`\n).join('\\n\\n');\n\nconst systemPrompt = `You are the host of \"AI Digest Daily\", a daily podcast covering the most important AI news. Your style is conversational but informed \u2014 like a knowledgeable friend catching someone up over coffee.\n\nRules:\n- Cover 4-6 of the most interesting stories from these articles\n- Spend roughly 30-60 seconds per story\n- Use natural transitions (\"Speaking of...\", \"Meanwhile...\", \"On a related note...\")\n- Short, punchy sentences \u2014 this will be read by text-to-speech\n- NO jargon without brief explanation\n- NO stage directions, sound effects, or markdown formatting\n- Write EXACTLY as it should be spoken aloud\n- Start with \"Welcome to AI Digest Daily for ${today}\"\n- End with a brief sign-off like \"That's your AI digest for today. Stay curious.\"\n- Total: 600-800 words (about 5 min spoken)\n- CRITICAL: Stay under 3500 characters total\n- Do NOT use asterisks, bullets, or any formatting \u2014 pure spoken text only`;\n\nconst userMessage = `Here are today's top AI stories:\n\n${articlesText}\n\nWrite the podcast script now.`;\n\nreturn [{ json: { systemPrompt, userMessage, date: today, articleCount: diverse.length } }];"
      },
      "name": "Build Script Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        304
      ],
      "id": "3600ad3b-3b55-4ef7-ab87-3846a20c4d4e"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-5-mini', messages: [{ role: 'system', content: $json.systemPrompt }, { role: 'user', content: $json.userMessage }] }) }}",
        "options": {}
      },
      "name": "Generate Script",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1056,
        304
      ],
      "id": "91fa9223-bf95-4d42-885e-a71566af220f",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nlet script = '';\nif (input.choices) {\n  script = input.choices[0].message.content || '';\n} else {\n  script = input.text || input.content || '';\n}\n\nif (!script) {\n  throw new Error('No script text generated');\n}\n\n// Safety: truncate at sentence boundary if over 4000 chars\nif (script.length > 4000) {\n  const truncated = script.substring(0, 4000);\n  const lastPeriod = truncated.lastIndexOf('.');\n  if (lastPeriod > 3000) {\n    script = truncated.substring(0, lastPeriod + 1);\n  } else {\n    script = truncated;\n  }\n  script += \" That's your AI digest for today. Stay curious.\";\n}\n\nreturn [{ json: { text: script } }];"
      },
      "name": "Trim Script",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1256,
        304
      ],
      "id": "b1c5a3d2-4e6f-7890-abcd-ef0123456789"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/audio/speech",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'tts-1', input: $json.text, voice: 'onyx', response_format: 'mp3' }) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "name": "Generate Audio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1456,
        304
      ],
      "id": "f1bfbdcf-1b6d-4bbb-b364-3ed7633fb3f6",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "jsCode": "const enc = new TextEncoder();\n\nasync function sha256Hex(data) {\n  const buf = typeof data === 'string' ? enc.encode(data) : data;\n  const hash = await globalThis.crypto.subtle.digest('SHA-256', buf);\n  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function hmac(key, data) {\n  const keyBuf = typeof key === 'string' ? enc.encode(key) : key;\n  const msgBuf = typeof data === 'string' ? enc.encode(data) : data;\n  const cryptoKey = await globalThis.crypto.subtle.importKey('raw', keyBuf, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n  return new Uint8Array(await globalThis.crypto.subtle.sign('HMAC', cryptoKey, msgBuf));\n}\n\nasync function hmacHex(key, data) {\n  const sig = await hmac(key, data);\n  return Array.from(sig).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\nconst accessKey = $env.R2_ACCESS_KEY_ID;\nconst secretKey = $env.R2_SECRET_ACCESS_KEY;\nconst accountId = $env.R2_ACCOUNT_ID;\nif (!accessKey || !secretKey || !accountId) {\n  throw new Error('Set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID in n8n env vars');\n}\n\nconst binaryItem = $input.first().binary;\nif (!binaryItem || !binaryItem.data) {\n  throw new Error('No binary data in property \"data\"');\n}\nconst bodyBuffer = Buffer.from(binaryItem.data.data, 'base64');\n\nconst bucket = 'ai-digest';\nconst date = new Date().toISOString().split('T')[0];\nconst objectKey = `episodes/ai-digest-${date}.mp3`;\nconst host = `${accountId}.r2.cloudflarestorage.com`;\n\nconst now = new Date();\nconst amzDate = now.toISOString().replace(/[:-]|\\..*/g, '') + 'Z';\nconst dateStamp = amzDate.substring(0, 8);\nconst contentSha256 = 'UNSIGNED-PAYLOAD';\nconst canonicalUri = `/${bucket}/${objectKey}`;\n\nconst canonicalHeaders = `content-type:audio/mpeg\\nhost:${host}\\nx-amz-content-sha256:${contentSha256}\\nx-amz-date:${amzDate}\\n`;\nconst signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date';\nconst canonicalRequest = `PUT\\n${canonicalUri}\\n\\n${canonicalHeaders}\\n${signedHeaders}\\n${contentSha256}`;\n\nconst credentialScope = `${dateStamp}/auto/s3/aws4_request`;\nconst canonicalRequestHash = await sha256Hex(canonicalRequest);\nconst stringToSign = `AWS4-HMAC-SHA256\\n${amzDate}\\n${credentialScope}\\n${canonicalRequestHash}`;\n\nlet sigKey = await hmac(`AWS4${secretKey}`, dateStamp);\nsigKey = await hmac(sigKey, 'auto');\nsigKey = await hmac(sigKey, 's3');\nsigKey = await hmac(sigKey, 'aws4_request');\nconst signature = await hmacHex(sigKey, stringToSign);\n\nconst authorization = `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;\n\nconst response = await fetch(`https://${host}${canonicalUri}`, {\n  method: 'PUT',\n  headers: {\n    'Authorization': authorization,\n    'x-amz-date': amzDate,\n    'x-amz-content-sha256': contentSha256,\n    'Content-Type': 'audio/mpeg',\n    'Content-Length': bodyBuffer.length.toString(),\n  },\n  body: bodyBuffer,\n});\n\nif (!response.ok) {\n  const errText = await response.text();\n  throw new Error(`R2 upload failed (${response.status}): ${errText}`);\n}\n\nreturn [{ json: { uploaded: true, key: objectKey, status: response.status } }];"
      },
      "name": "Upload to R2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1656,
        304
      ],
      "id": "3187408a-69a3-44a6-9513-2133c2e40f16",
      "retryOnFail": true,
      "maxTries": 4,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "const date = new Date().toISOString().split('T')[0];\nconst filename = `episodes/ai-digest-${date}.mp3`;\nconst publicUrl = `https://pub-9b797e802d6047af9f356783bf25a650.r2.dev/${filename}`;\n\nlet articleCount = 0;\ntry {\n  articleCount = $('Build Script Prompt').first().json.articleCount;\n} catch {}\n\nconst content = `\ud83c\udf99\ufe0f **AI Digest Daily \u2014 ${date}**\\n\\n\ud83d\udd0a Listen: ${publicUrl}\\n\\n\ud83d\udcca Generated from ${articleCount} articles\\n\u2705 New episode is live!`;\n\nreturn [{ json: { content } }];"
      },
      "name": "Format Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1856,
        304
      ],
      "id": "9d4dc90e-fe8e-44fc-bef7-083a870ca20b"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.DISCORD_WEBHOOK_URL }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ content: $json.content }) }}",
        "options": {}
      },
      "name": "Discord",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2056,
        304
      ],
      "id": "ef0d4d70-d476-4752-b18e-e23bad8a9abb",
      "continueOnFail": true
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Load Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: 6am Daily": {
      "main": [
        [
          {
            "node": "Load Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Feeds": {
      "main": [
        [
          {
            "node": "RSS Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read": {
      "main": [
        [
          {
            "node": "Build Script Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Script Prompt": {
      "main": [
        [
          {
            "node": "Generate Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Script": {
      "main": [
        [
          {
            "node": "Trim Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trim Script": {
      "main": [
        [
          {
            "node": "Generate Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Audio": {
      "main": [
        [
          {
            "node": "Upload to R2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to R2": {
      "main": [
        [
          {
            "node": "Format Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Message": {
      "main": [
        [
          {
            "node": "Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner",
    "timezone": "UTC"
  },
  "staticData": null,
  "tags": []
}

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

AI Digest Daily Podcast. Uses rssFeedRead, httpRequest. Event-driven trigger; 11 nodes.

Source: https://github.com/planetaryescape/ai-digest/blob/ad409e8da621f3808a5785a6d7df949b788cbabe/n8n-workflows/ai-digest-podcast.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

GoldPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 33 nodes.

RSS Feed Read, HTTP Request
Web Scraping

AegisPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 31 nodes.

RSS Feed Read, HTTP Request
Web Scraping

QuorumPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 29 nodes.

RSS Feed Read, HTTP Request
Web Scraping

PulseMosaic-v2-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.

RSS Feed Read, HTTP Request
Web Scraping

PulseMosaic-v3.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.

RSS Feed Read, HTTP Request