{
  "nodes": [
    {
      "id": "0721f8d0-1413-40ce-a5c4-9cbe6fd5a8c7",
      "name": "\ud83d\udccb MAIN \u2014 Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        -912
      ],
      "parameters": {
        "width": 780,
        "height": 1028,
        "content": "## \ud83c\udf99\ufe0f Product Update Audio Announcements \u2014 Changelog to TTS Audio\n\n**What this workflow does:**\nEvery time a new release is pushed to your GitHub repository (or a Notion changelog page is updated), this workflow:\n1. Fetches the raw changelog/release notes from GitHub Releases API\n2. Pulls the matching structured release notes from a **Notion** database (rich formatted content)\n3. Uses **OpenAI** to rewrite the technical changelog into a natural, podcast-style spoken script\n4. Sends the script to **ElevenLabs TTS API** to generate a professional MP3 audio file\n5. Uploads the MP3 via **UploadToURL** to get a permanent public audio URL\n6. Uses the **HTML node** to compose a beautifully formatted email newsletter with the audio player embed, changelog summary, and a 'Listen Now' CTA button\n7. Sends the newsletter via **Gmail** to your subscriber list (loaded from Google Sheets)\n8. Posts a Slack message to your internal `#product-updates` channel with the audio URL and release summary\n9. **Writes back** to the Notion changelog page \u2014 marks it as 'Audio Published' with the hosted audio URL\n\n**Architecture (unique from all previous templates):**\n- \ud83d\udc19 GitHub Trigger (watches releases \u2014 NEW trigger type)\n- \ud83d\udcd3 Notion \u2014 fetch release notes page (NEW: database query)\n- \ud83e\udd16 OpenAI \u2014 rewrite to spoken script (NEW: different prompt purpose)\n- \ud83c\udf99\ufe0f ElevenLabs TTS API \u2014 audio generation (NEW: TTS node)\n- \u2601\ufe0f UploadToURL \u2014 host the MP3 file (mandatory)\n- \ud83d\udccb Aggregate Node \u2014 bundle subscriber emails (NEW node type)\n- \ud83c\udf10 HTML Node \u2014 compose rich email body (NEW node type)\n- \ud83d\udce7 Gmail \u2014 send newsletter to subscribers (NEW: email delivery)\n- \ud83d\udcac Slack \u2014 post to #product-updates channel (NEW: Slack node)\n- \ud83d\udd04 Notion Update \u2014 write-back audio URL to source page (NEW: bidirectional)\n\n**Setup Requirements:**\n1. GitHub credentials (personal access token with `repo` read scope)\n2. Notion Integration Token \u2014 share your Changelog database with the integration\n3. Notion Database ID for your changelog (from the database page URL)\n4. OpenAI API credentials\n5. ElevenLabs API Key + your preferred Voice ID (from elevenlabs.io/voices)\n6. UploadToURL endpoint configured in upload node\n7. Gmail OAuth2 credentials\n8. Google Sheets with subscriber emails \u2014 sheet named `Subscribers` with column `Email`\n9. Slack Bot Token with `chat:write` scope \u2014 set channel to `#product-updates`\n10. Notion credentials again for the write-back step"
      },
      "typeVersion": 1
    },
    {
      "id": "6eeaf206-5041-41fd-bd21-2e434d5e9389",
      "name": "\ud83d\udcdd Note \u2014 GitHub Trigger, Parse & Notion Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 616,
        "height": 616,
        "content": "### \ud83d\udc19 Step 1 \u2014 GitHub Release Trigger & Notion Fetch\n**GitHub Trigger:** Fires on every new GitHub Release published to your repo (uses GitHub webhooks \u2014 not polling). Captures tag name, release name, release body (raw markdown changelog), author, and release URL.\n**Code \u2014 Parse Release:** Cleans the raw markdown changelog into plain text. Extracts version number, release date, and categorises changes into `features`, `fixes`, and `improvements` arrays by scanning bullet prefixes.\n**Notion \u2014 Query Database:** Searches your Notion Changelog database for a page matching the version tag. Retrieves the richer formatted description, product area tags, and any manually added context \u2014 supplements the raw GitHub release body."
      },
      "typeVersion": 1
    },
    {
      "id": "aa818069-eff9-43f7-9f2d-143ba805908c",
      "name": "\ud83d\udcdd Note \u2014 Script, TTS & UploadToURL",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 1128,
        "height": 632,
        "content": "### \ud83c\udf99\ufe0f Step 2 \u2014 Script Writing, TTS Generation & Upload\n**OpenAI \u2014 Spoken Script:** Rewrites the combined GitHub + Notion content into a warm, conversational podcast-style spoken announcement (60-90 seconds when read aloud). Avoids markdown, bullet points, and jargon.\n**ElevenLabs TTS:** Sends the script to ElevenLabs `/v1/text-to-speech/{voice_id}` endpoint. Returns raw MP3 binary audio. Voice, stability, and similarity boost are all configurable.\n**UploadToURL:** Uploads the MP3 binary and returns a permanent public URL \u2014 this URL is used in the email embed, Slack message, and Notion write-back."
      },
      "typeVersion": 1
    },
    {
      "id": "bb19ab3c-6d68-4732-82fa-ca35a56ddecf",
      "name": "\ud83d\udcdd Note \u2014 Subscribers, HTML Build & Gmail Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 1032,
        "height": 632,
        "content": "### \ud83d\udce7 Step 3 \u2014 Subscriber Load, HTML Email & Gmail Send\n**Google Sheets \u2014 Load Subscribers:** Reads all rows from the `Subscribers` sheet. Each row contains at minimum an `Email` field. Returns one item per subscriber.\n**Aggregate Node:** Bundles all subscriber email addresses into a single comma-separated string \u2014 used as the Gmail BCC list so one send covers all subscribers.\n**HTML Node:** Composes a fully formatted HTML email with: audio player embed (using `<audio>` tag), release version header, changelog summary table (features/fixes), and a styled 'Listen Now' CTA button linking to the hosted MP3.\n**Gmail \u2014 Send Newsletter:** Sends the HTML email to all subscribers in BCC. Subject includes version number and release date."
      },
      "typeVersion": 1
    },
    {
      "id": "70b804a2-49a8-4c31-8805-4985f00b4f0f",
      "name": "\ud83d\udcdd Note \u2014 Slack Post & Notion Write-Back",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3552,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 632,
        "content": "### \ud83d\udcac Step 4 \u2014 Slack Announcement & Notion Write-Back\n**Slack \u2014 Post to #product-updates:** Posts a rich Slack Block Kit message to your internal channel. Includes: release version, one-line summary, 'Listen to audio update' link (hosted MP3), and a direct link to the GitHub release.\n**Notion \u2014 Update Page:** Writes back to the original Notion changelog page \u2014 sets a `AudioStatus` property to `Published`, stores the `AudioURL` (hosted MP3 link), and stamps the `AudioPublishedAt` timestamp. This prevents duplicate processing if the workflow fires again."
      },
      "typeVersion": 1
    },
    {
      "id": "760f31b3-035d-4f20-8361-b2ed1ee30618",
      "name": "GitHub \u2014 On New Release Published",
      "type": "n8n-nodes-base.githubTrigger",
      "position": [
        752,
        480
      ],
      "parameters": {
        "owner": "YOUR_GITHUB_ORG_OR_USERNAME",
        "events": [
          "release"
        ],
        "options": {},
        "repository": "YOUR_REPO_NAME"
      },
      "typeVersion": 1
    },
    {
      "id": "25cd24a1-6e9b-4718-a49c-c2757d2d3237",
      "name": "Code \u2014 Parse & Categorise Release Notes",
      "type": "n8n-nodes-base.code",
      "position": [
        976,
        480
      ],
      "parameters": {
        "jsCode": "const payload = $input.first().json;\n\n// Only process 'published' release events\nconst action = payload.action || '';\nif (action !== 'published') {\n  return [];\n}\n\nconst release = payload.release || {};\nconst repo    = payload.repository || {};\n\nconst rawBody   = release.body || '';\nconst version   = release.tag_name || 'v0.0.0';\nconst releaseName = release.name || version;\nconst releaseUrl  = release.html_url || '';\nconst author      = release.author?.login || 'Team';\nconst publishedAt = release.published_at || new Date().toISOString();\nconst repoName    = repo.full_name || repo.name || 'Product';\n\n// Parse changelog markdown into categories\nconst lines = rawBody.split('\\n').map(l => l.trim()).filter(Boolean);\nconst features    = [];\nconst fixes       = [];\nconst improvements = [];\nconst other       = [];\n\nlet currentSection = 'other';\nfor (const line of lines) {\n  const lower = line.toLowerCase();\n  if (lower.includes('## feat') || lower.includes('### feat') || lower.includes('## new'))     { currentSection = 'features'; continue; }\n  if (lower.includes('## fix')  || lower.includes('### fix')  || lower.includes('## bug'))     { currentSection = 'fixes'; continue; }\n  if (lower.includes('## impr') || lower.includes('### impr') || lower.includes('## change'))  { currentSection = 'improvements'; continue; }\n  if (line.startsWith('-') || line.startsWith('*') || line.startsWith('\u2022')) {\n    const clean = line.replace(/^[-*\u2022]\\s*/, '').trim();\n    if (!clean) continue;\n    if (currentSection === 'features')     features.push(clean);\n    else if (currentSection === 'fixes')   fixes.push(clean);\n    else if (currentSection === 'improvements') improvements.push(clean);\n    else other.push(clean);\n  }\n}\n\n// Plain text version for TTS script base\nconst plainChangelog = rawBody\n  .replace(/#{1,6}\\s*/g, '')\n  .replace(/[*_`]/g, '')\n  .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim()\n  .substring(0, 2000);\n\nconst releaseDate = new Date(publishedAt).toLocaleDateString('en-US', {\n  weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n});\n\nreturn [{\n  json: {\n    version,\n    releaseName,\n    releaseUrl,\n    author,\n    publishedAt,\n    releaseDate,\n    repoName,\n    plainChangelog,\n    features,\n    fixes,\n    improvements,\n    other,\n    totalChanges: features.length + fixes.length + improvements.length + other.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ad024d1f-d93e-4896-a479-bbbdfbece7e7",
      "name": "Notion \u2014 Query Changelog Database",
      "type": "n8n-nodes-base.notion",
      "position": [
        1184,
        480
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "conditions": [
            {
              "key": "Version",
              "condition": "equals"
            }
          ]
        },
        "options": {
          "downloadFiles": false
        },
        "resource": "databasePage",
        "operation": "getAll",
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_NOTION_DATABASE_ID"
        },
        "filterType": "manual"
      },
      "typeVersion": 2.2
    },
    {
      "id": "91bfee04-63e2-4a57-8fba-8adac177e209",
      "name": "Code \u2014 Merge Notion Context + Release Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        480
      ],
      "parameters": {
        "jsCode": "const notionResp  = $input.first().json;\nconst releaseData = $('Code \u2014 Parse & Categorise Release Notes').item.json;\n\n// Extract richer Notion content if page found\nlet notionSummary = '';\nlet notionPageId  = '';\nlet audioAlreadyPublished = false;\n\nif (notionResp && notionResp.id) {\n  notionPageId = notionResp.id;\n\n  // Extract plain text from Notion title property\n  const titleProp = notionResp.properties?.Name?.title ||\n                    notionResp.properties?.Title?.title || [];\n  const titleText = titleProp.map(t => t.plain_text || '').join('');\n\n  // Check if audio already published\n  const audioStatus = notionResp.properties?.AudioStatus?.select?.name || '';\n  if (audioStatus === 'Published') {\n    audioAlreadyPublished = true;\n  }\n\n  // Extract rich text summary\n  const summaryProp = notionResp.properties?.Summary?.rich_text || [];\n  notionSummary = summaryProp.map(t => t.plain_text || '').join('').trim();\n}\n\nif (audioAlreadyPublished) {\n  // Return empty to stop processing\n  return [];\n}\n\nreturn [{\n  json: {\n    ...releaseData,\n    notionPageId,\n    notionSummary: notionSummary || releaseData.plainChangelog.substring(0, 300)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cfb52b62-e69f-453a-a905-20f36695ff42",
      "name": "OpenAI \u2014 Write Podcast-Style Spoken Script",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1632,
        480
      ],
      "parameters": {
        "resource": "chat"
      },
      "typeVersion": 1.4
    },
    {
      "id": "927bf6f4-a0ed-47ef-ae19-ff7c2591949a",
      "name": "ElevenLabs \u2014 Generate MP3 Audio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1856,
        480
      ],
      "parameters": {
        "url": "=https://api.elevenlabs.io/v1/text-to-speech/YOUR_ELEVENLABS_VOICE_ID",
        "method": "POST",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "jsonBody": "={\n  \"text\": {{ JSON.stringify($json.choices[0].message.content) }},\n  \"model_id\": \"eleven_multilingual_v2\",\n  \"voice_settings\": {\n    \"stability\": 0.5,\n    \"similarity_boost\": 0.85,\n    \"style\": 0.2,\n    \"use_speaker_boost\": true\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "xi-api-key",
              "value": "=YOUR_ELEVENLABS_API_KEY"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "audio/mpeg"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3521601e-fe5e-4960-a1cd-3db15f417570",
      "name": "Code \u2014 Store Script Text Before Upload",
      "type": "n8n-nodes-base.code",
      "position": [
        2064,
        480
      ],
      "parameters": {
        "jsCode": "// Store the spoken script text for later use before binary overwrites context\nconst aiResp      = $('OpenAI \u2014 Write Podcast-Style Spoken Script').item.json;\nconst releaseData = $('Code \u2014 Merge Notion Context + Release Data').item.json;\n\nconst spokenScript = aiResp?.choices?.[0]?.message?.content || '';\n\n// Pass-through: binary data stays in item, we just annotate json\nreturn [{\n  json: {\n    ...releaseData,\n    spokenScript\n  },\n  binary: $input.first().binary\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c5e59b51-8d2d-4a3e-90c4-6f2efd7b80fc",
      "name": "Code \u2014 Store Audio URL + Full Release Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        480
      ],
      "parameters": {
        "jsCode": "const uploadResp  = $input.first().json;\nconst releaseData = $('Code \u2014 Store Script Text Before Upload').item.json;\n\nconst audioUrl =\n  uploadResp?.url ??\n  uploadResp?.data?.url ??\n  uploadResp?.file?.url ??\n  uploadResp?.link ??\n  '';\n\nreturn [{\n  json: {\n    ...releaseData,\n    audioUrl\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c63743e0-b669-4e1d-9e75-c48ca0be2e78",
      "name": "Google Sheets \u2014 Load Subscriber List",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2736,
        480
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Subscribers"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "b7b44d9a-fd60-4f29-9baf-ad0e39bc182f",
      "name": "Aggregate \u2014 Bundle All Subscriber Emails",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2944,
        480
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "subscribers"
      },
      "typeVersion": 1
    },
    {
      "id": "98a3ca90-bf97-4bed-a957-46aa22aca686",
      "name": "Code \u2014 Build HTML Email Newsletter",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        480
      ],
      "parameters": {
        "jsCode": "const aggregated  = $input.first().json.subscribers || [];\nconst releaseData = $('Code \u2014 Store Audio URL + Full Release Data').item.json;\n\n// Extract emails into BCC list\nconst emails = aggregated\n  .map(row => row.Email || row.email || '')\n  .filter(Boolean)\n  .join(',');\n\n// Build features/fixes HTML rows\nconst featureRows = (releaseData.features || []).map(f =>\n  `<tr><td style=\"padding:6px 0;color:#1a1a2e;\">\u2728 ${f}</td></tr>`\n).join('');\n\nconst fixRows = (releaseData.fixes || []).map(f =>\n  `<tr><td style=\"padding:6px 0;color:#1a1a2e;\">\ud83d\udc1b ${f}</td></tr>`\n).join('');\n\nconst improvRows = (releaseData.improvements || []).map(i =>\n  `<tr><td style=\"padding:6px 0;color:#1a1a2e;\">\u26a1 ${i}</td></tr>`\n).join('');\n\nconst changeTable = (featureRows || fixRows || improvRows)\n  ? `<table style=\"width:100%;border-collapse:collapse;\">${featureRows}${fixRows}${improvRows}</table>`\n  : `<p style=\"color:#555;\">${releaseData.plainChangelog.substring(0,300)}...</p>`;\n\nconst htmlBody = `\n<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head>\n<body style=\"margin:0;padding:0;background:#f4f6fb;font-family:Arial,sans-serif;\">\n  <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f4f6fb;\">\n    <tr><td align=\"center\" style=\"padding:40px 20px;\">\n      <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.08);\">\n\n        <!-- Header -->\n        <tr><td style=\"background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);padding:36px 40px;\">\n          <h1 style=\"margin:0;color:#fff;font-size:26px;font-weight:700;\">\ud83c\udf99\ufe0f Product Update</h1>\n          <p style=\"margin:8px 0 0;color:rgba(255,255,255,0.85);font-size:15px;\">${releaseData.releaseName} \u00b7 ${releaseData.releaseDate}</p>\n        </td></tr>\n\n        <!-- Audio Player Section -->\n        <tr><td style=\"padding:32px 40px 24px;background:#f9f6ff;text-align:center;\">\n          <p style=\"margin:0 0 12px;color:#764ba2;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:1px;\">\ud83c\udfa7 Listen to this update</p>\n          <audio controls style=\"width:100%;max-width:480px;\">\n            <source src=\"${releaseData.audioUrl}\" type=\"audio/mpeg\">\n            Your email client doesn't support audio playback.\n          </audio>\n          <br><br>\n          <a href=\"${releaseData.audioUrl}\" style=\"display:inline-block;background:#667eea;color:#fff;text-decoration:none;padding:12px 32px;border-radius:8px;font-size:15px;font-weight:600;\">\u25b6 Listen Now (MP3)</a>\n        </td></tr>\n\n        <!-- Changelog Summary -->\n        <tr><td style=\"padding:32px 40px;\">\n          <h2 style=\"margin:0 0 18px;color:#1a1a2e;font-size:19px;\">What's new in ${releaseData.version}</h2>\n          ${changeTable}\n        </td></tr>\n\n        <!-- CTA -->\n        <tr><td style=\"padding:0 40px 32px;text-align:center;\">\n          <a href=\"${releaseData.releaseUrl}\" style=\"display:inline-block;border:2px solid #667eea;color:#667eea;text-decoration:none;padding:11px 28px;border-radius:8px;font-size:14px;font-weight:600;\">View Full Release Notes on GitHub \u2192</a>\n        </td></tr>\n\n        <!-- Footer -->\n        <tr><td style=\"background:#f4f6fb;padding:20px 40px;text-align:center;border-top:1px solid #eee;\">\n          <p style=\"margin:0;color:#999;font-size:12px;\">You're receiving this because you subscribed to ${releaseData.repoName} product updates.</p>\n        </td></tr>\n\n      </table>\n    </td></tr>\n  </table>\n</body>\n</html>`;\n\nreturn [{\n  json: {\n    ...releaseData,\n    htmlBody,\n    bccList: emails,\n    emailSubject: `\ud83c\udf99\ufe0f [${releaseData.version}] ${releaseData.releaseName} \u2014 Listen to the Update`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "91ec0fd2-ba43-4797-b45c-86c059919820",
      "name": "Gmail \u2014 Send Audio Newsletter to Subscribers",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3392,
        480
      ],
      "parameters": {
        "sendTo": "=newsletter@yourdomain.com",
        "message": "={{ $json.htmlBody }}",
        "options": {
          "bccList": "={{ $json.bccList }}",
          "replyTo": "user@example.com",
          "senderName": "Product Team",
          "appendAttribution": false
        },
        "subject": "={{ $json.emailSubject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "faad1606-961e-4218-93e4-f9300d32e3d5",
      "name": "Slack \u2014 Post to #product-updates Channel",
      "type": "n8n-nodes-base.slack",
      "position": [
        3616,
        480
      ],
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "#product-updates"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "acda4293-1b81-4985-9dae-6170f6ab5995",
      "name": "Notion \u2014 Write Back Audio URL to Changelog Page",
      "type": "n8n-nodes-base.notion",
      "position": [
        3824,
        480
      ],
      "parameters": {
        "operation": "update"
      },
      "typeVersion": 2.2
    },
    {
      "id": "7d211080-0815-419b-8a29-8f49a9d5c64e",
      "name": "Upload a File",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        2272,
        480
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Upload a File": {
      "main": [
        [
          {
            "node": "Code \u2014 Store Audio URL + Full Release Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ElevenLabs \u2014 Generate MP3 Audio": {
      "main": [
        [
          {
            "node": "Code \u2014 Store Script Text Before Upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub \u2014 On New Release Published": {
      "main": [
        [
          {
            "node": "Code \u2014 Parse & Categorise Release Notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notion \u2014 Query Changelog Database": {
      "main": [
        [
          {
            "node": "Code \u2014 Merge Notion Context + Release Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Build HTML Email Newsletter": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Send Audio Newsletter to Subscribers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2014 Load Subscriber List": {
      "main": [
        [
          {
            "node": "Aggregate \u2014 Bundle All Subscriber Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Store Script Text Before Upload": {
      "main": [
        [
          {
            "node": "Upload a File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Parse & Categorise Release Notes": {
      "main": [
        [
          {
            "node": "Notion \u2014 Query Changelog Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate \u2014 Bundle All Subscriber Emails": {
      "main": [
        [
          {
            "node": "Code \u2014 Build HTML Email Newsletter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack \u2014 Post to #product-updates Channel": {
      "main": [
        [
          {
            "node": "Notion \u2014 Write Back Audio URL to Changelog Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Merge Notion Context + Release Data": {
      "main": [
        [
          {
            "node": "OpenAI \u2014 Write Podcast-Style Spoken Script",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Store Audio URL + Full Release Data": {
      "main": [
        [
          {
            "node": "Google Sheets \u2014 Load Subscriber List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI \u2014 Write Podcast-Style Spoken Script": {
      "main": [
        [
          {
            "node": "ElevenLabs \u2014 Generate MP3 Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail \u2014 Send Audio Newsletter to Subscribers": {
      "main": [
        [
          {
            "node": "Slack \u2014 Post to #product-updates Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}