{
  "id": "2hi0dwPfJyNF39nP",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Summarize Apple Podcast Episodes with ElevenLabs and GPT-5-MINI",
  "tags": [],
  "nodes": [
    {
      "id": "sticky-overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3776,
        -128
      ],
      "parameters": {
        "color": 5,
        "width": 556,
        "height": 1000,
        "content": "## \ud83c\udf99\ufe0f Summarize Apple Podcast Episodes with ElevenLabs and GPT-5-MINI\n\n**Paste one or more Apple Podcast episode URLs into a form (one per line) and receive a structured AI-generated summary by email - powered by ElevenLabs speech-to-text and GPT-5-MINI.**\n\n---\n\n### Who is this for\n- Podcast listeners who want fast episode digests\n- Content creators researching competitor podcasts\n- Teams that share podcast insights via email\n\n---\n\n### How it works\n1. **Submit URLs** - Paste one or more Apple Podcast episode URLs into the trigger form\n2. **Discover RSS feed** - The iTunes API is queried to find the podcast's public RSS feed URL\n3. **Find episode MP3** - The RSS XML is parsed to locate the matching episode audio file\n4. **Transcribe audio** - ElevenLabs Scribe transcribes the full episode via direct URL\n5. **Generate summary** - GPT-5-MINI produces a structured summary: title, key points, useful info, and bottom line\n6. **Email results** - All summaries are combined into a formatted HTML email and sent to your inbox\n\n---\n\n### Setup\n1. **ElevenLabs** - Add HTTP Header Auth credential with your ElevenLabs API key; connect to \"Transcribe Episode with ElevenLabs\"\n2. **OpenAI** - Connect your OpenAI API credential to \"Generate Episode Summary\"\n3. **Gmail** - Connect your Gmail OAuth2 credential to \"Send Summary Email\"\n4. **Recipient email** - Update the \"To\" address in \"Send Summary Email\"\n5. Activate the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-s1",
      "name": "Section: User Input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3200,
        208
      ],
      "parameters": {
        "color": 5,
        "width": 238,
        "height": 298,
        "content": "## \u2460 User Input\nUser submits Apple Podcast episode URLs via the n8n form."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-s2",
      "name": "Section: Podcast Discovery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2928,
        208
      ],
      "parameters": {
        "color": 3,
        "width": 962,
        "height": 298,
        "content": "## \u2461 Podcast Discovery\nLooks up the RSS feed via iTunes API and extracts the matching episode MP3 URL(s)."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-s3",
      "name": "Section: Transcription & Summary",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        208
      ],
      "parameters": {
        "color": 3,
        "width": 490,
        "height": 298,
        "content": "## \u2462 Transcription & AI Summary\nTranscribes the episode audio and generates a structured summary with GPT-5-MINI."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-s4",
      "name": "Section: Email Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        208
      ],
      "parameters": {
        "color": 0,
        "width": 442,
        "height": 298,
        "content": "## \u2463 Email Delivery\nCombines all summaries into an HTML email and sends it to the recipient."
      },
      "typeVersion": 1
    },
    {
      "id": "elevenlabs-stt",
      "name": "Transcribe Episode with ElevenLabs",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1872,
        320
      ],
      "parameters": {
        "url": "https://api.elevenlabs.io/v1/speech-to-text",
        "method": "POST",
        "options": {
          "timeout": 600000
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "cloud_storage_url",
              "value": "={{ $json.mp3Url }}"
            },
            {
              "name": "model_id",
              "value": "scribe_v2"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "fca607d8-ec3a-434f-96ea-b9f3151f9ba9",
      "name": "Submit Podcast URLs",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -3104,
        320
      ],
      "parameters": {
        "options": {
          "appendAttribution": false
        },
        "formTitle": "Apple Podcasts Summarizer",
        "formFields": {
          "values": [
            {
              "fieldType": "textarea",
              "fieldLabel": "Input Apple Podcast links",
              "placeholder": "https://podcasts.apple.com/mn/podcast/seasonality-isnt-a-problem-its-a-profit-opportunity-ep-952/id1254720112?i=1+1234567890",
              "requiredField": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "b263c0b1-797d-4197-b6aa-4b967d9af20e",
      "name": "Parse Input URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -2880,
        320
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json['Input Apple Podcast links'];\nconst urls = input.split('\\n').filter(u => u.trim() !== '');\nreturn urls.map(url => {\n  const match = url.match(/id(\\d+)/);\n  return {\n    json: {\n      apple_url: url.trim(),\n      podcastId: match ? match[1] : ''\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "cd36a002-1b68-49b6-a2f9-f56c0bbf675d",
      "name": "Look Up RSS Feed via iTunes",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2688,
        320
      ],
      "parameters": {
        "url": "=https://itunes.apple.com/lookup?id={{ $json.podcastId }}&entity=podcast",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "970c1467-b612-4460-8c86-d3643f617bcf",
      "name": "Extract RSS Feed URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -2496,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const rawData = $input.item.json;\nlet data;\nif (rawData.data !== undefined) {\n    data = typeof rawData.data === 'string' ? JSON.parse(rawData.data) : rawData.data;\n} else {\n    data = rawData;\n}\nlet rssUrl = '';\nif (data.results && data.results.length > 0) { rssUrl = data.results[0].feedUrl; }\nreturn { json: { rssUrl: rssUrl } };"
      },
      "typeVersion": 2
    },
    {
      "id": "376cb838-552b-4e44-bfd4-99b4285f6918",
      "name": "Find Episode MP3 URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -2112,
        320
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const xml = $input.item.json.data;\nconst xmlString = typeof xml === 'string' ? xml : JSON.stringify(xml);\nconst originalUrl = $('Parse Input URLs').item.json.apple_url;\nconst urlParts = originalUrl.split('/');\nconst urlSlug = urlParts.find(part => part.includes('-') && part.length > 10) || '';\nconst searchTerms = urlSlug.toLowerCase().replace(/-/g, ' ');\nconst items = xmlString.split('<item>');\nlet mp3Url = '';\nlet episodeTitle = '';\nlet bestMatch = null;\nlet bestScore = 0;\nfor (let item of items) {\n  const titleMatch = item.match(/<title><!\\[CDATA\\[([^\\]]+)\\]\\]><\\/title>/) || item.match(/<title>([^<]+)<\\/title>/);\n  if (titleMatch) {\n    const itemTitle = titleMatch[1].toLowerCase();\n    const words = searchTerms.split(' ').filter(w => w.length > 2);\n    let score = 0;\n    for (let word of words) { if (itemTitle.includes(word)) score++; }\n    if (score > bestScore) { bestScore = score; bestMatch = item; episodeTitle = titleMatch[1]; }\n  }\n}\nif (bestMatch) {\n  const urlMatch = bestMatch.match(/<enclosure[^>]*url=\"([^\"]+)\"[^>]*type=\"audio/);\n  if (urlMatch) { mp3Url = urlMatch[1]; }\n}\nif (!mp3Url) {\n  const firstMatch = xmlString.match(/<enclosure[^>]*url=\"([^\"]+)\"[^>]*type=\"audio/);\n  mp3Url = firstMatch ? firstMatch[1] : '';\n  episodeTitle = 'First episode (no match found)';\n}\nreturn { json: { mp3Url: mp3Url, episodeTitle: episodeTitle, matchScore: bestScore, success: mp3Url ? true : false } };"
      },
      "typeVersion": 2
    },
    {
      "id": "934ac04a-c9da-4ddc-ba9a-882f7b8bef0a",
      "name": "Fetch RSS Feed",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2304,
        320
      ],
      "parameters": {
        "url": "={{ $json.rssUrl }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "c2d71e27-2920-4399-afae-fe22db9eb32f",
      "name": "Build HTML Email",
      "type": "n8n-nodes-base.code",
      "position": [
        -1360,
        320
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfunction mdToHtml(md) {\n  const lines = md.split('\\n');\n  const out = [];\n  let inList = false;\n  for (const line of lines) {\n    if (line.startsWith('- ')) {\n      if (!inList) { out.push('<ul style=\"margin:8px 0;padding-left:20px\">'); inList = true; }\n      const content = line.slice(2)\n        .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n        .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" style=\"color:#0066cc\">$1</a>');\n      out.push(`<li style=\"margin:4px 0\">${content}</li>`);\n    } else {\n      if (inList) { out.push('</ul>'); inList = false; }\n      if (line.trim() === '') continue;\n      const content = line\n        .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n        .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" style=\"color:#0066cc\">$1</a>');\n      out.push(`<p style=\"margin:6px 0\">${content}</p>`);\n    }\n  }\n  if (inList) out.push('</ul>');\n  return out.join('\\n');\n}\n\nlet html = `<!DOCTYPE html>\n<html><head><meta charset=\"UTF-8\"></head>\n<body style=\"font-family:Arial,sans-serif;line-height:1.6;color:#333;max-width:700px;margin:0 auto;padding:20px\">\n<h2 style=\"color:#1a1a1a;border-bottom:2px solid #eee;padding-bottom:10px\">Podcast Summaries</h2>\n<p style=\"color:#888;font-size:13px\">Generated: ${new Date().toLocaleDateString()} &middot; ${items.length} episode(s)</p>\n`;\n\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i].json;\n  let summary = '';\n  if (item.message && item.message.content) { summary = item.message.content; }\n  else if (item.choices && item.choices[0] && item.choices[0].message) { summary = item.choices[0].message.content; }\n  else if (item.content) { summary = item.content; }\n  else { summary = 'Error: Could not extract summary'; }\n  html += `<div style=\"border:1px solid #e0e0e0;border-radius:8px;padding:20px;margin:20px 0\">${mdToHtml(summary)}</div>`;\n}\n\nhtml += `</body></html>`;\n\nreturn { json: { html: html } };"
      },
      "typeVersion": 2
    },
    {
      "id": "9fb5b75e-c772-4c14-87dc-2dcd9dd8c0de",
      "name": "Generate Episode Summary",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -1696,
        320
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "GPT-5-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are a precise podcast summarization engine. Your only job is to fill in the template below using only information found in the transcript. Do not invent, infer, or embellish.\n\n# Formatting Rules (follow strictly):\n- Output ONLY the filled template. No preamble, no closing remarks, no explanations.\n- Keep every pair of ** exactly as shown. Do not remove or alter any markdown characters.\n- Do not change headings, labels, or section order. Do not add new sections.\n- The **Title** and **URL** lines are pre-filled \u2014 copy them exactly as given, do not modify them.\n- Bullets are plain text only: no bold, no nesting, no sub-points. Each must start with \"- \" with no blank lines between them.\n- Do not wrap the URL in any format other than: [URL](URL)\n- If the transcript lacks enough information for a section, write [Insufficient data].\n- The output must be valid Markdown.\n\n# Word Limits:\n- Main Topic: 1 sentence, max 20 words\n- Key Points: 3\u20135 bullets, each 15\u201325 words\n- Useful Info: 2\u20133 sentences, 40\u201360 words\n- Bottom Line: 1 sentence, max 20 words\n\n# Template:\n**Title**: {{ $('Find Episode MP3 URL').item.json.episodeTitle }}\n**URL**: [{{ $('Parse Input URLs').item.json.apple_url }}]({{ $('Parse Input URLs').item.json.apple_url }})\n**Main Topic:** \n**Key Points:**\n- \n**Useful Info:** \n**Bottom Line:** \n\n# Transcript:\n{{ $json.text }}"
            }
          ]
        },
        "simplify": false
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "db9a8c06-c22f-4ddc-9d13-f3fa37c985d6",
      "name": "Send Summary Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -1168,
        320
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.html }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "Podcast Summary"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "Nin8EYMkR9vuyKPp",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1",
    "timeSavedPerExecution": 100
  },
  "versionId": "1f8b627d-0552-45ea-9652-1ae9cd97973c",
  "connections": {
    "Fetch RSS Feed": {
      "main": [
        [
          {
            "node": "Find Episode MP3 URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build HTML Email": {
      "main": [
        [
          {
            "node": "Send Summary Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Input URLs": {
      "main": [
        [
          {
            "node": "Look Up RSS Feed via iTunes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit Podcast URLs": {
      "main": [
        [
          {
            "node": "Parse Input URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract RSS Feed URL": {
      "main": [
        [
          {
            "node": "Fetch RSS Feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Episode MP3 URL": {
      "main": [
        [
          {
            "node": "Transcribe Episode with ElevenLabs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Episode Summary": {
      "main": [
        [
          {
            "node": "Build HTML Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look Up RSS Feed via iTunes": {
      "main": [
        [
          {
            "node": "Extract RSS Feed URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe Episode with ElevenLabs": {
      "main": [
        [
          {
            "node": "Generate Episode Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}