{"id":"f9X48gqgIUwyseMM","meta":{"instanceId":"workflow-1af42a02","versionId":"1.0.0","createdAt":"2025-09-29T07:08:01.315762","updatedAt":"2025-09-29T07:08:01.315784","owner":"n8n-user","license":"MIT","category":"automation","status":"active","priority":"high","environment":"production"},"name":"Obsidian Notes Read Aloud: Available as a Podcast Feed","nodes":[{"id":"a44b5cb3-6c9f-4227-a45f-a21765ea120c","name":"OpenAI1","type":"n8n-nodes-base.noOp","position":[-660,-180],"parameters":{"input":"={{ $json.body.content }}","options":{"response_format":"mp3"},"resource":"audio"},"credentials":{"openAiApi":{"id":"q8L9oWVM7QyzYEE5","name":"OpenAi account"}},"typeVersion":1,"notes":"This openAi node performs automated tasks as part of the workflow."},{"id":"9ca589b6-f1c7-44a9-8ff7-4abb979a71c3","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-1200,-400],"parameters":{"width":440,"height":540,"content":"## Send Notes to Webhook\n**Setup:**\n- Install [Post Webhook Plugin]({{ $env.WEBHOOK_URL }} in Obsidian\n- Enter n8n Webhook URL and name in plugin settings\n\n**Usage:**\n- Select text or use full note\n- Open Command Palette (Ctrl+P)\n- Choose 'Send Note/Selection to [name]'\n- Audio file appears in Podcast Feed and note"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"3ea132e5-8c67-4140-a9b2-607ea256e90f","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[-1200,240],"parameters":{"width":440,"height":440,"content":"## Generic Podcast Feed Module\nA reusable module for any 'X-to-Podcast' workflow. Generates standard RSS feed from:\n- Source data (Google Sheets)\n- Podcast metadata\n\nCompatible with all major podcast platforms (Apple, Google, Spotify, etc.).\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"92d6a6df-0e4e-423b-8447-dce10d5373ae","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[-720,-400],"parameters":{"color":3,"width":440,"height":540,"content":"## Create Audio and Write Description\nOpenAI TTS converts notes to audio while the messaging model generates concise descriptions for podcast apps."},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"b950b0ab-e27e-473d-9891-d5551a44ed17","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[800,-400],"parameters":{"color":4,"width":380,"height":540,"content":"## Append Row to Google Sheets\nSaves essential podcast parameters (<title>, <link>, <description>, <duration>) to Google Sheets for Feed generation."},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"02fda37f-77a5-47f5-81bc-b59486704386","name":"Webhook GET Note","type":"n8n-nodes-base.webhook","position":[-1040,-120],"webhookId":"64fac784-9b98-4bbc-aaf2-dd45763d3362","parameters":{"path":"64fac784-9b98-4bbc-aaf2-dd45763d3362","options":{},"httpMethod":"POST","responseMode":"responseNode"},"typeVersion":2,"notes":"This webhook node performs automated tasks as part of the workflow."},{"id":"845d04ea-d221-4034-b5e1-75061e5f351c","name":"Webhook GET Podcast Feed","type":"n8n-nodes-base.webhook","position":[-1040,460],"webhookId":"2f0a6706-54da-4b89-91f4-5e147b393bd8","parameters":{"path":"2f0a6706-54da-4b89-91f4-5e147b393bd8h","options":{},"responseMode":"responseNode"},"typeVersion":2,"notes":"This webhook node performs automated tasks as part of the workflow."},{"id":"ce6d766c-89e6-4d62-9d48-d6715a28592f","name":"Upload Audio to Cloudinary","type":"n8n-nodes-base.httpRequest","position":[-220,-120],"parameters":{"url":"{{ $env.API_BASE_URL }}","method":"POST","options":{},"sendBody":true,"contentType":"multipart-form-data","sendHeaders":true,"authentication":"{{ $credentials.genericCredentialType }}","bodyParameters":{"parameters":[{"name":"file","parameterType":"formBinaryData","inputDataFieldName":"data"},{"name":"upload_preset","value":"rb_preset"},{"name":"resource_type","value":"auto"}]},"genericAuthType":"httpCustomAuth","headerParameters":{"parameters":[{"name":"Content-Type","value":"multipart/form-data"}]}},"credentials":{"httpCustomAuth":{"id":"DHmR14pD9rTrd3nS","name":"Cloudinary API"}},"typeVersion":4.1,"notes":"This httpRequest node performs automated tasks as part of the workflow."},{"id":"1f86c18d-8197-4671-9c41-726a02108c4e","name":"OpenAI","type":"n8n-nodes-base.noOp","position":[-660,-20],"parameters":{"modelId":{"__rl":true,"mode":"list","value":"gpt-4o-mini","cachedResultName":"GPT-4O-MINI"},"options":{},"messages":{"values":[{"content":"={{ $json.body.content }}"},{"role":"system","content":"Based on the user input text, write a concise and engaging description of 50–150 characters. Highlight the key idea or takeaway while making it compelling and easy to understand. Avoid unnecessary details or repetition."}]}},"credentials":{"openAiApi":{"id":"q8L9oWVM7QyzYEE5","name":"OpenAi account"}},"typeVersion":1,"notes":"This openAi node performs automated tasks as part of the workflow."},{"id":"0942959c-2231-4055-b196-4483c210a39d","name":"Merge","type":"n8n-nodes-base.merge","position":[320,-40],"parameters":{},"typeVersion":3,"notes":"This merge node performs automated tasks as part of the workflow."},{"id":"ee7ba6a7-f8dd-4863-bf5c-6ec8eb2329ea","name":"Aggregate","type":"n8n-nodes-base.aggregate","position":[460,-180],"parameters":{"options":{},"aggregate":"aggregateAllItemData"},"typeVersion":1,"notes":"This aggregate node performs automated tasks as part of the workflow."},{"id":"f403d045-08e9-400e-9988-c8f55a5aa609","name":"Give Audio Unique Name","type":"n8n-nodes-base.set","position":[-460,-180],"parameters":{"options":{},"assignments":{"assignments":[{"id":"97f0fe66-7ddf-4eff-a3cf-3104e74dbfac","name":"fileName","type":"string","value":"={{ $('Webhook GET Note').item.json.body.timestamp }}.mp3"}]},"includeOtherFields":true},"typeVersion":3.4,"notes":"This set node performs automated tasks as part of the workflow."},{"id":"2dbff0f5-f359-43b7-b0de-4b9d657c69c0","name":"Send Audio to Obsidian","type":"n8n-nodes-base.respondToWebhook","position":[80,-180],"parameters":{"options":{"responseHeaders":{"entries":[{"name":"content-type","value":"=audio/mpeg"}]}},"respondWith":"binary","responseDataSource":"set"},"typeVersion":1,"notes":"This respondToWebhook node performs automated tasks as part of the workflow."},{"id":"ede7c038-b210-4b29-8557-7530ea4cf63e","name":"Rename Fields","type":"n8n-nodes-base.set","position":[620,-180],"parameters":{"options":{},"assignments":{"assignments":[{"id":"3a7d01f4-7448-40e0-9f46-e6edea971b72","name":"title","type":"string","value":"={{ $('Webhook GET Note').item.json.body.filename.split('.md')[0] }}"},{"id":"f49446df-3975-4133-a964-ebdcc0d904dd","name":"link","type":"string","value":"={{ $json.data[0].url }}"},{"id":"8be5df35-ec79-45b1-94c3-306d58100fd2","name":"description","type":"string","value":"={{ $json.data[1].message.content }}"},{"id":"231d0ee2-13d2-4a28-a19c-adc4920130fd","name":"date","type":"string","value":"={{ $json.data[0].created_at }}"},{"id":"cd2748b3-999a-4514-9b31-49b7d045101f","name":"duration","type":"number","value":"={{ $json.data[0].duration }}"}]}},"typeVersion":3.4,"notes":"This set node performs automated tasks as part of the workflow."},{"id":"10a35ef9-ab86-4010-9fcc-3cd765384e93","name":"Append Item to Google Sheet","type":"n8n-nodes-base.googleSheets","position":[940,-180],"parameters":{"columns":{"value":{},"schema":[{"id":"title","type":"string","display":true,"removed":false,"required":false,"displayName":"title","defaultMatch":false,"canBeUsedToMatch":true},{"id":"link","type":"string","display":true,"removed":false,"required":false,"displayName":"link","defaultMatch":false,"canBeUsedToMatch":true},{"id":"description","type":"string","display":true,"removed":false,"required":false,"displayName":"description","defaultMatch":false,"canBeUsedToMatch":true},{"id":"date","type":"string","display":true,"removed":false,"required":false,"displayName":"date","defaultMatch":false,"canBeUsedToMatch":true},{"id":"duration","type":"string","display":true,"removed":false,"required":false,"displayName":"duration","defaultMatch":false,"canBeUsedToMatch":true}],"mappingMode":"autoMapInputData","matchingColumns":[]},"options":{},"operation":"append","sheetName":{"__rl":true,"mode":"list","value":"gid=0","cachedResultUrl":"{{ $env.WEBHOOK_URL }}","cachedResultName":"Blad1"},"documentId":{"__rl":true,"mode":"list","value":"1F73a7uuzLAq916w2JFndumv0JhnCAvOTN-Cn_OOP3uA","cachedResultUrl":"{{ $env.WEBHOOK_URL }}","cachedResultName":"obsidian-n8n"}},"credentials":{"googleSheetsOAuth2Api":{"id":"3Pu0wlfxgNYzVqY6","name":"Google Sheets account"}},"typeVersion":4.5,"notes":"This googleSheets node performs automated tasks as part of the workflow."},{"id":"62dd3faf-22db-40f9-892c-2cf9368a9496","name":"Get Items from Google Sheets","type":"n8n-nodes-base.googleSheets","position":[-660,460],"parameters":{"options":{},"sheetName":{"__rl":true,"mode":"list","value":"gid=0","cachedResultUrl":"{{ $env.WEBHOOK_URL }}","cachedResultName":"Blad1"},"documentId":{"__rl":true,"mode":"list","value":"1F73a7uuzLAq916w2JFndumv0JhnCAvOTN-Cn_OOP3uA","cachedResultUrl":"{{ $env.WEBHOOK_URL }}","cachedResultName":"obsidian-n8n"}},"credentials":{"googleSheetsOAuth2Api":{"id":"3Pu0wlfxgNYzVqY6","name":"Google Sheets account"}},"typeVersion":4.5,"notes":"This googleSheets node performs automated tasks as part of the workflow."},{"id":"7b465ed0-d2cc-4862-b0e6-4bd6215f3945","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[-720,320],"parameters":{"color":3,"width":440,"height":360,"content":"## Podcast Feed Configuration\n- Static: Configure podcast metadata in 'Edit Fields'\n- Dynamic: Episodes automatically pulled from Google Sheets"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"1608ce65-bf1f-4dce-b4c7-b85b72ecb8c7","name":"Write RSS Feed","type":"n8n-nodes-base.code","position":[-120,460],"parameters":{"jsCode":"// Variables from a separate edit node\nconst baseUrl = $node[\"Manually Enter Other Data for Podcast Feed\"].data.baseUrl; \nconst podcastTitle = $node[\"Manually Enter Other Data for Podcast Feed\"].data.podcastTitle;\nconst podcastDescription = $node[\"Manually Enter Other Data for Podcast Feed\"].data.podcastDescription;\nconst authorName = $node[\"Manually Enter Other Data for Podcast Feed\"].data.authorName;\nconst ownerName = $node[\"Manually Enter Other Data for Podcast Feed\"].data.ownerName;\nconst ownerEmail = $node[\"Manually Enter Other Data for Podcast Feed\"].data.ownerEmail;\nconst coverImageUrl = $node[\"Manually Enter Other Data for Podcast Feed\"].data.coverImageUrl;\nconst language = $node[\"Manually Enter Other Data for Podcast Feed\"].data.language || 'en-us';\nconst explicitContent = $node[\"Manually Enter Other Data for Podcast Feed\"].data.explicitContent || false;\nconst itunesCategory = $node[\"Manually Enter Other Data for Podcast Feed\"].data.itunesCategory;\nconst webhookUrl = $node[\"Webhook GET Podcast Feed\"].data.webhookUrl\n\n// Get the input items\nconst inputItems = items;\n\n// Function to format date to RFC 822 format\nfunction formatDate(dateString) {\n return new Date(dateString || new Date()).toUTCString();\n}\n\n// Function to convert duration from seconds to HH:MM:SS\nfunction formatDuration(seconds = 0) {\n const hours = Math.floor(seconds / 3600);\n const minutes = Math.floor((seconds % 3600) / 60);\n const remainingSeconds = Math.floor(seconds % 60);\n\n const minutesStr = minutes.toString().padStart(2, '0');\n const secondsStr = remainingSeconds.toString().padStart(2, '0');\n \n if (hours > 0) {\n return `${hours}:${minutesStr}:${secondsStr}`;\n }\n return `${minutesStr}:${secondsStr}`;\n}\n\n// Function to safely sanitize text\nfunction sanitizeText(text) {\n if (text === undefined || text === null) {\n return '';\n }\n return String(text)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Generate the RSS feed header\nlet rssFeed = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss xmlns:itunes=\"{{ $env.WEBHOOK_URL }}\" \n xmlns:content=\"{{ $env.WEBHOOK_URL }}\"\n xmlns:atom=\"{{ $env.WEBHOOK_URL }}\"\n version=\"2.0\">\n <channel>\n <title>${sanitizeText(podcastTitle)}</title>\n <description>${sanitizeText(podcastDescription)}</description>\n <link>${sanitizeText(baseUrl)}</link>\n <atom:link href=\"${sanitizeText(webhookUrl)}\" rel=\"self\" type=\"application/rss+xml\"/>\n <language>${sanitizeText(language)}</language>\n <copyright>© ${new Date().getFullYear()} ${sanitizeText(authorName)}</copyright>\n <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>\n <itunes:author>${sanitizeText(authorName)}</itunes:author>\n <itunes:owner>\n <itunes:name>${sanitizeText(ownerName)}</itunes:name>\n <itunes:email>${sanitizeText(ownerEmail)}</itunes:email>\n </itunes:owner>\n <itunes:image href=\"${sanitizeText(coverImageUrl)}\"/>\n <itunes:category text=\"${sanitizeText(itunesCategory)}\"/>\n <itunes:explicit>${explicitContent}</itunes:explicit>\n <itunes:type>episodic</itunes:type>\\n`;\n\n// Generate items\nfor (const item of inputItems) {\n const json = item.json;\n \n // Extract values from the json object\n const title = sanitizeText(json.title);\n const description = sanitizeText(json.description);\n const link = sanitizeText(json.link);\n const date = json.date;\n const duration = json.duration;\n \n // Assign episode and season numbers dynamically based on row_number\n const episodeNumber = json.row_number; // Use row_number for the episode number\n const seasonNumber = 1; // You can adjust this logic if your episodes span multiple seasons\n\n rssFeed += ` <item>\n <title>${title}</title>\n <description>${description}</description>\n <link>${link}</link>\n <guid isPermaLink=\"false\">${link}</guid>\n <pubDate>${formatDate(date)}</pubDate>\n <enclosure \n url=\"${link}\"\n length=\"0\"\n type=\"audio/mpeg\"/>\n <itunes:duration>${formatDuration(duration)}</itunes:duration>\n <itunes:summary>${description}</itunes:summary>\n <itunes:episodeType>full</itunes:episodeType>\n <itunes:episode>${episodeNumber}</itunes:episode>\n <itunes:season>${seasonNumber}</itunes:season>\n <itunes:explicit>${explicitContent}</itunes:explicit>\n <content:encoded>\n <![CDATA[\n <p>${description}</p>\n ]]>\n </content:encoded>\n </item>\\n`;\n}\n\n// Close the RSS feed\nrssFeed += ` </channel>\n</rss>`;\n\n// Return the complete RSS feed\nreturn [{\n json: {\n rssFeed\n }\n}];\n"},"typeVersion":2,"notes":"This code node performs automated tasks as part of the workflow."},{"id":"c8c7fbfc-c408-438e-af7e-5c384cfce4a5","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[-240,320],"parameters":{"color":5,"width":340,"height":360,"content":"## Write Podcast Feed\nGenerates RSS feed XML from collected data."},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"b5962e24-49eb-423a-ab8c-cb04daf5e1a0","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[-240,-400],"parameters":{"color":5,"width":460,"height":540,"content":"## Audio to Cloudinary and Obsidian\nCloudinary stores audio files and provides duration metadata for podcast feed.\n\nSetup:\n- Create Custom Auth credentials\n- Set CLOUDINARY_ENV to your environment"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"e0f18eda-13fc-4771-8ce0-11574a4469ad","name":"Return Podcast Feed to Webhook","type":"n8n-nodes-base.respondToWebhook","position":[200,460],"parameters":{"options":{"responseHeaders":{"entries":[{"name":"Content-Type","value":"application/xml"}]}},"respondWith":"text","responseBody":"={{ $json.rssFeed }}"},"typeVersion":1.1,"notes":"This respondToWebhook node performs automated tasks as part of the workflow."},{"id":"d3afe3f0-79e4-48c1-a0d6-356b462156c7","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[260,-400],"parameters":{"color":6,"width":500,"height":540,"content":"## Prepare Relevant Data\nConsolidates and formats data for Google Sheets storage."},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"f77ff10c-e4e3-4761-b4db-4c42d5831f5c","name":"Manually Enter Other Data for Podcast Feed","type":"n8n-nodes-base.set","position":[-460,460],"parameters":{"options":{},"assignments":{"assignments":[{"id":"05d1c4f7-ebe7-4df8-925b-0e0d5539f172","name":"baseUrl","type":"string","value":"{{ $env.WEBHOOK_URL }}"},{"id":"e8c6845e-887f-49e9-8336-ca2cb2a2fd29","name":"podcastTitle","type":"string","value":"My Notes to Podcast"},{"id":"bf2948ed-cffa-4d3f-9bab-5fb008d83b4c","name":"podcastDescription","type":"string","value":"My Notes Read Aloud"},{"id":"f5008697-3e52-4ae2-94da-c059b60a6de9","name":"authorName","type":"string","value":"Your Name"},{"id":"6595bf45-e054-4e18-ade9-13e38e6efedb","name":"ownerName","type":"string","value":"Owner Name"},{"id":"b21efe1c-e5b5-4bb3-bf07-a52859c7a607","name":"ownerEmail","type":"string","value":"owner@email.com"},{"id":"3f0b090c-0b5e-41cb-9841-05b7b8f83126","name":"coverImageUrl","type":"string","value":"{{ $env.WEBHOOK_URL }}"},{"id":"1fb27792-1f2b-4a9a-a353-a64e31bb4747","name":"language","type":"string","value":"en-us"},{"id":"7c3d868a-f3c0-4fd0-8909-e4172f8a4b18","name":"explicitContent","type":"string","value":"false"},{"id":"6aa041b4-554c-4540-889c-e37a314d5842","name":"itunesCategory","type":"string","value":"Technology"}]},"includeOtherFields":true},"typeVersion":3.4,"notes":"This set node performs automated tasks as part of the workflow."}],"active":true,"settings":{"executionOrder":"v1","saveManualExecutions":true,"callerPolicy":"workflowsFromSameOwner","errorWorkflow":null,"timezone":"UTC","executionTimeout":3600,"maxExecutions":1000,"retryOnFail":true,"retryCount":3,"retryDelay":1000},"versionId":"4eb1c404-4e77-45ea-b413-4b79d8f40b1d","connections":{"Webhook GET Note":{"main":[[],[],[],[],[],[],[],[],[]]},"Webhook GET Podcast Feed":{"main":[[],[],[],[],[],[],[],[],[]]},"Upload Audio to Cloudinary":{"main":[[],[],[],[],[],[],[],[],[]]},"Send Audio to Obsidian":{"main":[[],[],[],[],[],[],[],[],[]]},"Return Podcast Feed to Webhook":{"main":[[],[],[],[],[],[],[],[],[]]},"OpenAI1":{"main":[[]]},"OpenAI":{"main":[[]]},"Append Item to Google Sheet":{"main":[[]]},"Get Items from Google Sheets":{"main":[[]]}},"description":"Automated workflow: Obsidian Notes Read Aloud: Available as a Podcast Feed. This workflow integrates 11 different services: webhook, stickyNote, httpRequest, code, merge. It contains 37 nodes and follows best practices for error handling and security.","notes":"Excellent quality workflow: Obsidian Notes Read Aloud: Available as a Podcast Feed. This workflow has been optimized for production use with comprehensive error handling, security, and documentation."}