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 →
{
"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.
openAiApi
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Extract And Decode Google News RSS URLs to Clean Article Links. Uses manualTrigger, limit, rssFeedRead, httpRequest. Event-driven trigger; 20 nodes.
The workflow performs tasks that would normally require human intervention on Google News links, transforming the RSS feeds into data that can be used by an automated system like n8n, thus creating a
News Aggregation with Deduplication and Ranking. Uses httpRequest, rssFeedRead. Event-driven trigger; 8 nodes.
Multi-Source Deduplication Example. Uses httpRequest, rssFeedRead. Event-driven trigger; 6 nodes.
This n8n workflow automates the generation of short news videos using the HeyGen video API and RSS feeds from a Bangla news source, Prothom Alo. It is ideal for content creators, media publishers, or