AutomationFlowsAI & RAG › Convert Newsletters Into AI Podcasts with Gpt-4o Mini and Elevenlabs

Convert Newsletters Into AI Podcasts with Gpt-4o Mini and Elevenlabs

ByLuis Acosta @podcast-tools on n8n.io

Turn email overload into audio insights — automatically.

Event trigger★★★★☆ complexityAI-powered23 nodesGmail TriggerOpenAIHTTP RequestRead Write FileExecute CommandGmail
AI & RAG Trigger: Event Nodes: 23 Complexity: ★★★★☆ AI nodes: yes Added:
Convert Newsletters Into AI Podcasts with Gpt-4o Mini and Elevenlabs — n8n workflow card showing Gmail Trigger, OpenAI, HTTP Request integration

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

This workflow follows the Executecommand → HTTP Request 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": "kmIE2vOxAQCnkYZl",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Newsletter to Audio Template v3",
  "tags": [],
  "nodes": [
    {
      "id": "efdedc12-d60f-441d-ba3a-5e331f3bb776",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -640,
        -96
      ],
      "parameters": {
        "width": 432,
        "height": 1120,
        "content": "## \ud83c\udfa7 Newsletter-to-Audio Conversation Flow\n\nThis workflow turns unread newsletters from your email inbox into dynamic audio conversations between two AI voices \u2014 inspired by **Google's NotebookLM** ability to summarize and humanize dense content.\n\n### \u2728 How it works:\n1. \ud83d\udce8 **Fetch Unread Newsletters** using Gmail (e.g., sender: \"your_favorite@newsletter.com\").\n2. \ud83e\udde0 **Summarize and Reformat** the content into a dialogue using an LLM (like OpenAI or Gemini).\n3. \ud83d\udde3\ufe0f **Generate Voices** for each part of the conversation using a TTS service (e.g., ElevenLabs, Google TTS).\n4. \ud83c\udf9b\ufe0f **Merge Audio Segments** into a natural back-and-forth flow using FFmpeg or audio nodes.\n5. \ud83d\udce4 **Send the Final Audio** file back to your email inbox (or deliver to another channel like Telegram or Drive).\n\n### \ud83d\udca1 Inspired by:\nNotebookLM's approach to making long-form text more digestible by turning it into personalized, conversational summaries.\n\nYou can modify:\n- \ud83c\udf99\ufe0f The tone and voice of the personas\n- \ud83d\udd75\ufe0f Email filters (e.g., subject or sender)\n- \ud83d\udceb The delivery method (email, storage, etc.)\n\n> Ideal for turning passive subscriptions into engaging, hands-free content.\n\n---\n\n### \ud83d\udcec Need help or want to collaborate?\n\nIf you have any questions, need help setting this up, or want to share feedback \u2014 feel free to reach out:  \n\ud83d\udce9 **Luis.acosta@news2podcast.com**\n\nIf you're looking to build something more advanced with audio and AI \u2014 such as automatically updating podcasts to Spotify or other audio platforms \u2014 let me know and I\u2019ll figure out how I can help you!\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0d7b28ee-2163-4e5f-b552-1e85174a1a00",
      "name": "Get Newsletter",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "from:demandcurve.com"
        },
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "37149829-5c91-4654-9173-e7c2617f0db3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 1104,
        "content": "## \ud83d\udce8 Step 1: Get Newsletter Content (Gmail or Webhook)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis is the entry point of the workflow.\n\nBy default, we use the **Gmail node** to fetch unread newsletter emails from your inbox \u2014 perfect for personal automation. You can filter by sender with search \"from:\" (like Substack, Medium, or Beehiiv) to capture only relevant messages.\n\n### \ud83d\udd01 Alternative: Webhook for Product Integration\n\nIf you're integrating this workflow into a larger product or service, you can **swap this Gmail node for a Webhook node** to receive newsletter or any other content via API from your app or users.\n\nThis makes it easy to embed the experience in platforms where users submit newsletter URLs, HTML, or pasted content manually.\n\n### \u2705 Expected Output\n\n- `Body (HTML or plain text)`: Main content to be summarized and converted into a dialogue.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "54688146-cc4f-4499-9596-9fa11dac491e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 1104,
        "content": "## \ud83e\udde0 Step 2: Generate Dialogue Script\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis node uses the **OpenAI Chat model (GPT-4o Mini)** to convert newsletter content into a natural, spoken-style conversation between two AI personas: `men1` and `men2`.\n\n### \ud83c\udfaf Purpose\nTurn dense, static newsletter content into an engaging, human-like dialogue \u2014 similar to how **NotebookLM** rephrases documents as friendly, flowing discussions.\n\n### \ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1 Voice Personalities\n- `voice1`: Curious, expressive, informal \u2014 brings humor, reacts with emotion, and keeps it conversational.\n- `voice2`: Calm, reflective, slightly ironic \u2014 adds context, simplifies, and balances the tone.\n\n### \ud83e\udde9 Prompt Logic\nThe custom prompt:\n- Sets a clear structure: intro, content breakdown, and closing\n- Encourages a spoken, spontaneous tone (not read-from-a-script)\n- Uses `<break time=\"1.5s\" />` tags to simulate realistic pauses\n- Demands **at least 10,000 characters** (~10 minutes of audio)\n- Output is formatted only as:\n   - voice1: \u2026\n   - voice2: \u2026\n\n### \ud83d\udce5 Input Source\n- Pulls the newsletter content from the Gmail node:\n`{{$('Get Newsletter').first().json.text}}`\n\n### \u2705 Model Choice: GPT-4o Mini\nWe use **GPT-4o Mini** for its **excellent cost/performance ratio**, enabling fast generation of long-form dialogue at scale.\n\n> This is the core creative step of the workflow \u2014 translating static information into an immersive audio script, ready for voice synthesis.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d1cf8808-1af6-46d1-88c7-40cacd361cc1",
      "name": "Generate Dialogue Script",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        416,
        0
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are the scriptwriter of a podcast that transforms dense written content into a lively, natural conversation between two AI speakers, `voice1` and `voice2`.\n\nYour task is to turn the following newsletter content into a **realistic audio dialogue**. The conversation should be fluid, informal, and engaging \u2014 similar in tone and structure to how NotebookLM rewrites long documents as discussions. It must sound like two well-informed people exchanging ideas, not like a text being read aloud.\n\n### Roles\n\n- **voice1**: Curious, expressive, casual, often injects humor or everyday references. Tends to ask questions, react with surprise or amusement, and bring lightness to the discussion.\n- **voice2**: Analytical, composed, insightful. Adds perspective, context, and a slightly ironic or dry sense of humor. Offers clarity without sounding robotic.\n\nUse realistic, human-like phrasing with brief interjections (`\"Right?\"`, `\"Let me stop you there\"`, `\"That's exactly it\"`). Use `<break time=\"1.5s\" />` tags occasionally to simulate natural pauses.\n\n### Structure\n\n1. **Introduction**: Set the scene naturally. Briefly introduce what the episode is about based on the content, without listing or labeling sections. Present `voice1` and `voice2` through dialogue, not narration.\n2. **Content Breakdown**: For each key idea or section from the newsletter:\n   - Paraphrase the content in spoken language.\n   - Embed the headline or theme organically in the conversation.\n   - Include personal reactions, examples, and small tangents to make it relatable.\n   - Open loops by teasing questions or ideas that will be answered later in the conversation.\n   - Maintain curiosity and variety in tone and rhythm.\n3. **Closing**: End warmly and casually, with a brief comment on what stood out or what\u2019s coming next (no need for formal farewells).\n\n### Requirements\n\n- The script must be at least **ten thousand characters** (about 15 minutes of speech).\n- Use **commas** to separate items in a list, not periods.\n- Format the output as a single uninterrupted block of text with clear speaker tags:\n  \nvoice1: \u2026\nvoice2: \u2026\n\nYou will be given a newsletter input under this key:\n\n{{$('Get Newsletter').first().json.text}}\n\nGenerate only the final dialogue script \u2014 no explanations, bullet points, or headings. Just the conversation in English.\n\n\n"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "3254c9ca-c0f3-4ca2-bfb0-da126df9970c",
      "name": "Split script",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        0
      ],
      "parameters": {
        "jsCode": "/**\n * This Function node takes the script from the previous node\n * and splits it using \"voice1:\" and \"voice2:\" as delimiters.\n * Each resulting segment retains the respective identifier.\n */\n\nconst script = $input.first().json.message.content || \"\";\n\n// Ensure consistent line breaks\nconst normalizedScript = script.replace(/\\r\\n/g, \"\\n\");\n\n// Split the script while keeping \"voice1:\" and \"voice2:\" in the result\nconst segments = normalizedScript.split(/(?=(?:voice1:|voice2:))/g).map(s => s.trim()).filter(Boolean);\n\n// Return one item per segment\nreturn segments.map(segment => {\n  return {\n    json: {\n      segment\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "c0953943-c607-456f-aeab-09544e40ba60",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 1104,
        "content": "## \u2702\ufe0f Step 3: Split Script into Speaker Segments\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis Function node takes the full dialogue script generated by OpenAI and **splits it into individual segments** based on speaker turns (`men1:` and `men2:`).\n\n### \ud83d\udd27 What it does:\n- Reads the script from the previous node (`message.content`)\n- Normalizes line breaks (`\\r\\n` \u2192 `\\n`) for consistency\n- Uses a regex to split the text while **retaining speaker labels**\n- Filters out empty results\n- Returns each intervention as a separate item with:\n  ```json\n  {\n    \"segment\": \"men1: \u2026\" \n  }\n\n### \ud83d\udccc Why this is important\n\nEach segment will later be passed to the TTS system as a **standalone voice generation request**, allowing different voices to be applied for `men1` and `men2`.\n\nThis step transforms a long script into a list of **atomic, voice-ready dialogue chunks**.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "121a604c-2d64-4c02-9801-7ec18c78dbaf",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1488,
        0
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d2ad4b04-d346-4f79-998b-c73b9008024a",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        1920,
        96
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "93609a28-55f5-439e-8238-a48375255f4f",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.cleanedText }}",
              "rightValue": "voice1:"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b7a6be7e-3d9d-4eb9-9e26-a9ed1e8a84f5",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 1184,
        "height": 2000,
        "content": "## \ud83d\udd01 Step 4\u20136: Loop Through Segments & Generate Voices with ElevenLabs\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis part of the workflow loops through each dialogue segment and sends it to **ElevenLabs** to generate realistic voice audio based on the speaker (`men1` or `men2`).\n\n\n### \ud83d\ude4c Support this Template\n\nIf you'd like to support my work and help me continue building free, high-quality templates for the n8n community, **you can use my affiliate link when signing up for ElevenLabs**.  \n\ud83d\udc49 It **doesn\u2019t cost you anything extra**, and helps keep this project alive.  \n**[Click here to support via ElevenLabs](https://try.elevenlabs.io/ds0cvdfiufax)**\n\n\n### \ud83d\udd27 How it works\n\n1. **Function Node \u2013 Clean Segment**  \n   - Removes problematic characters like quotation marks and line breaks\n   - Stores the cleaned result as `cleanedText`\n\n2. **IF Node \u2013 Detect Speaker**  \n   - Checks if `cleanedText` starts with `\"voice1:\"`  \n   - Branches into `voice1` or `voice2` to assign the correct voice\n\n3. **Function Node \u2013 Prepare Text for TTS**  \n   - Strips the `\"voice1:\"` or `\"voice2:\"` label using regex  \n   - Outputs a clean `modifiedString` to be sent to ElevenLabs\n\n4. **HTTP Request Node \u2013 ElevenLabs TTS**  \n   - Uses a **custom auth header**:\n     ```json\n     {\n       \"headers\": {\n         \"xi-api-key\": \"YOUR_API_KEY_FOR_ELEVENLABS\"\n       }\n     }\n     ```\n   - Endpoint:  \n     ```\n     https://api.elevenlabs.io/v1/text-to-speech/YOUR_VOICE_ID\n     ```\n    Replace `YOUR_VOICE_ID` with the voice you want to use from your ElevenLabs dashboard. After logging in, go to Voices, find the voice that best fits your needs, and copy the Voice ID by clicking on the three dots [...]\n\n   - Sample JSON body:\n     ```json\n     {\n       \"text\": \"Your cleaned and formatted dialogue text here.\",\n       \"model_id\": \"eleven_multilingual_v2\",\n       \"voice_settings\": {\n         \"stability\": 0.4,\n         \"similarity_boost\": 0.75\n       }\n     }\n     ```\n\n   - You can have one node for each speaker (`voice1`, `voice2`) with a different **voice ID** in the URL.\n\n---\n\n### \ud83d\udd01 Output\n- Each iteration sends one segment to ElevenLabs.\n- The result will be a **binary audio file** (MP3) for each speaker\u2019s line.\n- You can later merge them using FFmpeg or the Audio Merge node.\n\n---\n\n> This section is key to giving your newsletter content an actual voice \u2014 literally! And with ElevenLabs' high-quality synthesis and GPT-4o Mini's conversational structure, the result is natural, polished audio narration.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e9926fad-2ec7-429b-9cbc-e13e537fd2f9",
      "name": "Function Node \u2013 Clean Segment",
      "type": "n8n-nodes-base.code",
      "position": [
        1744,
        96
      ],
      "parameters": {
        "jsCode": "const paragraph = $input.first().json.segment; \nif (!paragraph) {\n    throw new Error(\"No se encontr\u00f3 contenido de texto en el correo.\");\n}\n\nlet cleanedText = paragraph\n  .replace(/\"/g, \"\")\n  .replace(/\u201c/g, \"\")\n  .replace(/\u201d/g, \"\");\n\ncleanedText = cleanedText.replace(/\\n/g, \"\");\n\nconsole.log(\"Texto limpio sin comillas ni saltos de l\u00ednea:\", cleanedText);\n\nreturn [{ json: { cleanedText } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2cab07f6-fb5c-4242-a0e9-702256e8708d",
      "name": "Function Node \u2013 Prepare Text for TTS",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        160
      ],
      "parameters": {
        "jsCode": "const cleanedText = $input.first().json.cleanedText;\n\nif (typeof cleanedText !== \"string\") {\n    throw new Error(\"cleanedText debe ser un string.\");\n}\n\nconsole.log(\"Texto original:\", JSON.stringify(cleanedText, null, 2));\n\nif (cleanedText.includes(\"men1:\")) {\n    console.log(\"\u2705 'voice1:' detectado en el texto original.\");\n} else {\n    console.log(\"\u274c 'voice1:' NO encontrado en el texto original. \u00a1Revisar input!\");\n}\n\n\nconst modifiedString = cleanedText.replace(/\\bvoice1:\\s*/gi, \"\").trim();\n\n\nconsole.log(\"Texto modificado:\", JSON.stringify(modifiedString, null, 2));\n\n\nif (modifiedString.includes(\"voice1:\")) {\n    console.log(\"\u274c 'voice1:' sigue presente en el texto modificado. \u00a1El regex debe ajustarse!\");\n} else {\n    console.log(\"\u2705 'voice1:' eliminado correctamente.\");\n}\n\n\nreturn [\n    {\n        json: {\n            modifiedString\n        }\n    }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "8b891a0d-9a6a-4b16-a459-02cf8f153c0a",
      "name": "Function Node \u2013 Prepare Text for TTS1",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        368
      ],
      "parameters": {
        "jsCode": "const cleanedText = $input.first().json.cleanedText;\n\nif (typeof cleanedText !== \"string\") {\n    throw new Error(\"cleanedText debe ser un string.\");\n}\n\nconsole.log(\"Texto original:\", JSON.stringify(cleanedText, null, 2));\n\nif (cleanedText.includes(\"voice2:\")) {\n    console.log(\"\u2705 'voice2:' detectado en el texto original.\");\n} else {\n    console.log(\"\u274c 'voice2:' NO encontrado en el texto original. \u00a1Revisar input!\");\n}\n\nconst modifiedString = cleanedText.replace(/\\bvoice2:\\s*/gi, \"\").trim();\n\nconsole.log(\"Texto modificado:\", JSON.stringify(modifiedString, null, 2));\n\nif (modifiedString.includes(\"voice2:\")) {\n    console.log(\"\u274c 'voice2:' sigue presente en el texto modificado. \u00a1El regex debe ajustarse!\");\n} else {\n    console.log(\"\u2705 'voice2:' eliminado correctamente.\");\n}\n\nreturn [\n    {\n        json: {\n            modifiedString\n        }\n    }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "e12b7916-44b5-4ed8-8b99-1485e8ecf36d",
      "name": "Function Node \u2013 Prepare Text for TTS - Voice 1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2384,
        160
      ],
      "parameters": {
        "url": "=https://api.elevenlabs.io/v1/text-to-speech/uYXf8XasLslADfZ2MB4u",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"text\": \"{{ $json.modifiedString }}\",\n  \"model_id\": \"eleven_multilingual_v2\",\n  \"voice_settings\": {\n    \"stability\": 0.5,\n    \"similarity_boost\": 0.75\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpCustomAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpCustomAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "9894a491-669b-493e-bd29-00281fe79150",
      "name": "Function Node \u2013 Prepare Text for TTS - Voice 2",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2384,
        368
      ],
      "parameters": {
        "url": "=https://api.elevenlabs.io/v1/text-to-speech/UgBBYS2sOqTuMpoF3BR0",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"text\": \"{{ $json.modifiedString }}\",\n  \"model_id\": \"eleven_multilingual_v2\",\n  \"voice_settings\": {\n    \"stability\": 0.5,\n    \"similarity_boost\": 0.75\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpCustomAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpCustomAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "d46fbc80-a9ea-439a-84fc-e665f63170f4",
      "name": "Save Audio Chucks",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2384,
        -16
      ],
      "parameters": {
        "options": {},
        "fileName": "=/newsletter2podcast/tmp/audio_{{$itemIndex}}.mp3",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "4b7569cb-94d1-4a4a-9a24-4f1223c47be3",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2608,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 1792,
        "content": "## \ud83c\udfbc Step 7\u20138: Prepare FFmpeg List and Merge Audio\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis section merges all the individual voice segments into a single MP3 using FFmpeg, following the standard concat workflow via a `.txt` list file.\n\n\u26a0\ufe0f **To use this step, you must have FFmpeg installed and run n8n in a local or self-hosted environment.** This will not work on n8n Cloud or any environment that does not allow executing system commands.\n\n\n### \ud83e\uddfe Step 7 \u2013 Generate `concat_list.txt`\n\nA **Code node** performs the following:\n\n1. Loops through all items, assuming each item has:\n   - `fileName`\n   - `directory` (only for the first file)\n2. Builds a properly formatted FFmpeg list:\n   ```\n   file 'audio_1.mp3'\n   file 'audio_2.mp3'\n   file 'audio_3.mp3'\n   ```\n3. Converts the result to **Base64-encoded binary data** so it can be saved using the `Write Binary File` node.\n\n\ud83d\udcdd The result is written to:\n\n```\n/newsletter2podcast/tmp/concat_list.txt\n```\n\nCode logic highlights:\n- First item uses full path: `${directory}/${fileName}`\n- Others use just `fileName`\n- Converts the list to a `Buffer` and then to base64 for n8n binary handling\n\n---\n\n### \ud83c\udfac Step 8 \u2013 Merge Audio with FFmpeg\n\nAn `Execute Command` node runs:\n\n```bash\nffmpeg -y -f concat -safe 0 -i /newsletter2podcast/tmp/concat_list.txt -c copy /newsletter2podcast/tmp/final_merged.mp3\n```\n\nThis generates the final, merged audio file using **lossless concatenation**:\n\n- `-y`: Overwrites existing output\n- `-f concat`: Uses FFmpeg\u2019s concat demuxer\n- `-safe 0`: Allows absolute paths\n- `-c copy`: Copies streams without re-encoding\n\n\ud83c\udd95 Output location:\n\n```\n/newsletter2podcast/tmp/final_merged.mp3\n```\n\nDelete the audio chunks and the concat_list\n\n```bash\nfind /newsletter2podcast/tmp/ -type f ! -name \"final_merged.mp3\" -delete\n```\n---\n\n### \ud83d\udccc Notes\n\n- Ensure all audio files are encoded consistently (same format, codec, bitrate)\n- If ElevenLabs was used, they\u2019re already compatible\n- If sync issues occur, you can preprocess files with re-encoding (`-c:a libmp3lame`)\n- This process assumes FFmpeg is installed and accessible in your n8n environment\n\n---\n\n> \u2705 This step finalizes your audio transformation \u2014 from dialogue chunks to a smooth, podcast-ready episode.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "244a5ef9-14bd-4005-bba9-c224fea02c65",
      "name": "Generate `concat_list.txt`",
      "type": "n8n-nodes-base.code",
      "position": [
        2704,
        -16
      ],
      "parameters": {
        "jsCode": "/**\n * This Code node will:\n * 1. Gather all file paths from the incoming items (assuming each item has `item.json.filePath`).\n * 2. Build a single text string, each line in FFmpeg concat format: `file '/path/to/audio.mp3'`\n * 3. Convert that text to binary (Base64) so the next node (\"Write Binary File\") can save it as `concat_list.txt`.\n */\n\nconst items = $input.all();\n\n// Build the concat list\nlet concatListText = '';\n\nitems.forEach((item, index) => {\n  let filePath;\n\n\n  // Use only fileName for the rest\n    filePath = item.json.fileName;\n\n\n  if (filePath) {\n    concatListText += `file '${filePath}'\\n`;\n  }\n});\n\n// Convert the text to a Buffer, then to Base64\nconst buffer = Buffer.from(concatListText, 'utf-8');\nconst base64Data = buffer.toString('base64');\n\n// Return a single item containing the binary data\nreturn [\n  {\n    json: {},\n    binary: {\n      data: {\n        data: base64Data,\n        mimeType: 'text/plain',\n        fileName: 'concat_list.txt'\n      }\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "a53ecd13-10fb-4d5b-9149-f82447804f07",
      "name": "Save concat_list",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2912,
        -16
      ],
      "parameters": {
        "options": {},
        "fileName": "/newsletter2podcast/tmp/concat_list.txt",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "1259c775-2f7e-48be-b71a-8ace50bcf378",
      "name": "Join audio chucks and delete all files",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3120,
        -16
      ],
      "parameters": {
        "command": "ffmpeg -y -f concat -safe 0 -i /newsletter2podcast/tmp/concat_list.txt -c copy /newsletter2podcast/tmp/final_merged.mp3\n\nfind /newsletter2podcast/tmp/ -type f ! -name \"final_merged.mp3\" -delete\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5d5a1452-74be-40b4-901a-bf746581651a",
      "name": "read final_merged",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        3424,
        -16
      ],
      "parameters": {
        "options": {},
        "fileSelector": "/newsletter2podcast/tmp/final_merged.mp3"
      },
      "typeVersion": 1
    },
    {
      "id": "c1caeef4-199e-43ad-a4ed-e95bbb1b2385",
      "name": "Send audio",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3632,
        -16
      ],
      "parameters": {
        "sendTo": "={{$('Get Newsletter').first().json.to.text}}",
        "message": "=<h1>Hello! Here your newsletter in Audio Version </h1>\n",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "=[Audio Version] {{$('Get Newsletter').first().json.subject}}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b27f0c70-2c69-4338-aa9f-3a0fad785d2c",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3312,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 1184,
        "content": "## \u2709\ufe0f Step 9: Read Merged Audio & Send via Email\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThis final section completes the workflow by:\n\n1. **Reading the final merged MP3** using a `Read Binary File` node\n2. **Sending it as an email attachment** with a `Gmail` node\n\n---\n\n### \ud83d\udcc2 Read Binary File\n\n- Path:  \n/newsletter2podcast/tmp/final_merged.mp3\n\n---\n\n### \ud83d\udce4 Gmail Node \u2013 Send Audio\n\nUse the `Gmail` node to:\n\n- Set the **recipient email** dynamically or statically\n- Add a subject and message body \n- Attach the binary file:\n- `Binary Property`: `data`\n- Attachment filename: `newsletter_audio.mp3`\n\n---\n\n### \ud83d\udcdd Notes\n\n- You can personalize the subject with dynamic values (like the original newsletter subject).\n- Make sure Gmail authentication is correctly configured in your credentials.\n- You may add logic before this node to notify users via Telegram, Slack, or cloud storage.\n\n---\n\n> \u2705 This final step completes the journey from unread newsletter to hands-free audio experience, right in your inbox.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "61e1c74c-8506-4907-a433-56b50ff33151",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Function Node \u2013 Prepare Text for TTS",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Function Node \u2013 Prepare Text for TTS1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send audio": {
      "main": [
        []
      ]
    },
    "Split script": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Newsletter": {
      "main": [
        [
          {
            "node": "Generate Dialogue Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Save Audio Chucks",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Function Node \u2013 Clean Segment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save concat_list": {
      "main": [
        [
          {
            "node": "Join audio chucks and delete all files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Audio Chucks": {
      "main": [
        [
          {
            "node": "Generate `concat_list.txt`",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "read final_merged": {
      "main": [
        [
          {
            "node": "Send audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Dialogue Script": {
      "main": [
        [
          {
            "node": "Split script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate `concat_list.txt`": {
      "main": [
        [
          {
            "node": "Save concat_list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function Node \u2013 Clean Segment": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function Node \u2013 Prepare Text for TTS": {
      "main": [
        [
          {
            "node": "Function Node \u2013 Prepare Text for TTS - Voice 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Join audio chucks and delete all files": {
      "main": [
        [
          {
            "node": "read final_merged",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function Node \u2013 Prepare Text for TTS1": {
      "main": [
        [
          {
            "node": "Function Node \u2013 Prepare Text for TTS - Voice 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function Node \u2013 Prepare Text for TTS - Voice 1": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function Node \u2013 Prepare Text for TTS - Voice 2": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "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

Turn email overload into audio insights — automatically.

Source: https://n8n.io/workflows/6523/ — 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 — HRMate — streamlines your entire recruitment process by automatically parsing incoming job applications, evaluating candidate fit using AI, and sending personalized acceptance or r

HTTP Request, Gmail Trigger, OpenAI +2
AI & RAG

Overview

Gmail Trigger, Google Drive, OpenAI +4
AI & RAG

Small teams, solo operators, and security-conscious individuals who receive email attachments from external senders. Useful for freelancers, agencies, HR teams, and anyone handling CVs, invoices, or d

Gmail Trigger, HTTP Request, OpenAI +4
AI & RAG

This intelligent email automation workflow helps you maximize engagement through domain-based outreach. It utilizes AI-powered personalization and strategic follow-ups to increase response rates. The

Gmail, HTTP Request, Google Sheets +1
AI & RAG

This workflow converts emailed timesheets into structured invoice rows in Google Sheets and stores them in the correct Google Drive folder structure.

Gmail Trigger, OpenAI, Google Sheets +2