{
  "id": "L6ZTFOllD9qQVYlu",
  "name": "Transcript YouTube Video (Transcript API)",
  "tags": [],
  "nodes": [
    {
      "id": "fd8ce10b-25d3-455b-9121-2c5bf6192b1a",
      "name": "Set Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        1248,
        736
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a6f0f3c4-2d89-4c4e-9b43-1c8a0e22f001",
              "name": "youtubeVideoId",
              "type": "string",
              "value": "={{ $json.youtubeVideoId }}"
            },
            {
              "id": "3dc94d21-257f-430c-b087-95dad39bf829",
              "name": "apiToken",
              "type": "string",
              "value": "={{ $json.apiToken }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "1b30c4f2-09a2-41b2-a681-b96c5fd1cae4",
      "name": "Get Transcript (YouTube Transcript API)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Calls youtube-transcript.io API. Response is set to Full Response + text, so we parse body safely in the next node.",
      "position": [
        1456,
        736
      ],
      "parameters": {
        "url": "https://www.youtube-transcript.io/api/transcripts",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "text"
            }
          }
        },
        "jsonBody": "={\n  \"ids\": [\n    \"{{ $('Set Variables').item.json.youtubeVideoId }}\"\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Basic {{ $json.apiToken }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "waitBetweenTries": 5000
    },
    {
      "id": "ab4d562f-1c9e-4d88-bdd2-485e94dadc31",
      "name": "Parse API Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1904,
        640
      ],
      "parameters": {
        "jsCode": "const videoId = $('Set Variables').item.json.youtubeVideoId;\n\n\n// n8n HTTP Request (fullResponse + text) usually returns: { statusCode, headers, body }\nconst raw = ($json && ($json.body ?? $json.data)) ?? null;\n\nlet parsed;\ntry {\n  parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n} catch (e) {\n  return [{ json: { videoId, status: 'error', error: 'Invalid JSON response from transcript API', source: 'youtube_transcript_api' } }];\n}\n\n// Support multiple possible shapes\nconst firstItem = Array.isArray(parsed)\n  ? parsed[0]\n  : (Array.isArray(parsed?.data) ? parsed.data[0] : parsed?.data?.[0]);\n\n// Common fields seen in transcript APIs: text / transcript / tracks[].transcript[]\nlet text = firstItem?.text ?? firstItem?.transcript ?? null;\n\nif (!text && firstItem?.tracks?.[0]?.transcript?.length) {\n  text = firstItem.tracks[0].transcript.map(t => t.text).join(' ');\n}\n\nconst language = firstItem?.language ?? firstItem?.lang ?? firstItem?.tracks?.[0]?.language\n\nreturn [{\n  json: {\n    videoId,\n    language,\n    text,\n    status: text ? 'found' : 'no_transcript',\n    source: 'youtube_transcript_api'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ba9d71c7-a461-46d5-9220-559e56868480",
      "name": "IF Has Transcript?",
      "type": "n8n-nodes-base.if",
      "position": [
        2128,
        640
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-transcript",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.text !== null && $json.text !== undefined && $json.text !== '' }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ac4bf08b-396b-4b7d-bf0e-bf2e5e426a1d",
      "name": "Clean Transcript",
      "type": "n8n-nodes-base.code",
      "position": [
        2352,
        544
      ],
      "parameters": {
        "jsCode": "const videoId = $json.videoId;\nconst language = $json.language;\n\n\nlet text = $json.text;\n\n// Normalize to string\ntext = typeof text === 'string' ? text : JSON.stringify(text);\n\n// Basic cleaning (safe for most transcript sources)\ntext = text\n  .replace(/\\r?\\n/g, ' ')\n  .replace(/\\s+/g, ' ')\n  .replace(/\\[(music|m\u00fasica|musica)\\]/gi, '')\n  .trim();\n\nreturn [{\n  json: {\n    videoId,\n    language,\n    text,\n    wordCount: text ? text.split(' ').filter(Boolean).length : 0,\n    charCount: text ? text.length : 0,\n    status: 'success',\n    source: 'youtube_transcript_api'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bbfe49d2-777c-4b3a-8aa9-acbe41414131",
      "name": "No Transcript Fallback",
      "type": "n8n-nodes-base.code",
      "position": [
        2352,
        736
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    videoId: $('Set Variables').item.json.youtubeVideoId,\n    language: $('Set Variables').item.json.preferredLanguage || 'es',\n    error: 'No transcript available for this video (or API returned empty).',\n    status: 'no_transcript',\n    suggestion: 'Try another language, or use a Whisper/ASR fallback.',\n    source: 'youtube_transcript_api'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f2c51f08-d4a6-417c-bea6-e862dbc1457b",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        32
      ],
      "parameters": {
        "width": 760,
        "height": 1320,
        "content": "## \ud83d\ude80 Try It Out!\n\n### **YouTube Transcript API Extractor** *(Any Public Video)*\nExtracts a clean transcript from a **videoId** using **youtube-transcript.io**.\n\n---\n\n### \ud83c\udfaf **Use Cases**\n- AI summaries, sentiment analysis, keyword extraction\n- Internal indexing/SEO\n- Content pipelines (blog/newsletter)\n- Batch transcript processing\n\n---\n\n### \ud83d\udd04 **How It Works** *(5 Steps)*\n1. **\ud83d\udce5 Input**: `youtubeVideoId`, `apiToken`\n2. **\ud83c\udf10 API**: POST to youtube-transcript.io\n3. **\ud83e\udde9 Parse**: Normalizes the response format\n4. **\ud83e\uddf9 Clean**: Normalizes text and whitespace\n5. **\u2705 Output**: Transcript + metrics (`wordCount`/`charCount`)\n\n---\n\n### \ud83d\ude80 **How to Use**\nPayload:  \n`{\"youtubeVideoId\":\"xObjAdhDxBE\", \"apiToken\": \"xxxxxxxxxx\"}`\n\n\n**\u2699\ufe0f Setup**:\n- This sub-workflow is intended to be called from another workflow (Execute Workflow Trigger)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a3052424-f30d-4e55-927e-c28bc032c336",
      "name": "Sticky Note - Input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 420,
        "content": "## 1. Input Processing\n- Receives execution as a sub-workflow\n- Defines `youtubeVideoId` and `preferredLanguage`\n\nExample:\n{\n  \"youtubeVideoId\": \"xObjAdhDxBE\",\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "c4fd578a-b9af-4aab-bc36-1f2781804826",
      "name": "Sticky Note - API",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1376,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 420,
        "content": "## 2. Transcript API Request\n- Calls **youtube-transcript.io**\n- Does not require YouTube OAuth\n- Returns the response (Full Response + text)\n- The next node performs robust parsing"
      },
      "typeVersion": 1
    },
    {
      "id": "8700f14e-75cc-4382-b9e4-f4a23587f597",
      "name": "Sticky Note - Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2064,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 380,
        "content": "## 3. Parsing & Cleaning\n- Parses JSON even when the response arrives as a string (using safe parsing / try-catch) \n- Supports multiple response shapes (`text` / `transcript` / `tracks`)\n- Basic cleaning:\n  - `\\n` \u2192 space\n  - Collapses repeated whitespace\n  - Removes `[Music]` / `[M\u00fasica]`\n\nOutput: text ready for AI processing\n"
      },
      "typeVersion": 1
    },
    {
      "id": "1ebe6831-0b73-44f8-b7cf-6591109a293a",
      "name": "Sticky Note - Output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 380,
        "content": "## \u2705 Final Output Format\n\nSuccess:\n{\n  \"videoId\": \"xObjAdhDxBE\",\n  \"language\": \"es\",\n  \"text\": \"Clean transcript text...\",\n  \"wordCount\": 1234,\n  \"charCount\": 5678,\n  \"status\": \"success\",\n  \"source\": \"youtube_transcript_api\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "e22f8b25-b753-4507-97ba-ce8cd1e151a8",
      "name": "Sticky Note - Errors",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        736
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 200,
        "content": "## \u274c No Transcript\n\nWhen no transcript is available:\n- Returns a structured error (**No Transcript Fallback**)\n- Ends the execution with **Stop and Error**\n- Suggests an ASR/Whisper fallback\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0f2675a3-3ef7-4135-93f7-3dc1a2406f0b",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "position": [
        1680,
        736
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "OK",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "52d1c4eb-8389-41c7-b325-d2ffc7cbc0af",
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.statusCode }}",
                    "rightValue": 200
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "=Not enough credits",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "6aa821f2-f09a-46d2-baf2-358582ebd983",
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.statusCode }}",
                    "rightValue": 402
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Generic Error",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "96428dda-42b0-4a4a-8a17-f32b54738fe0",
                    "operator": {
                      "type": "number",
                      "operation": "notEmpty",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.statusCode }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "94b03a2f-6b1d-4a98-9a49-53060135dad4",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        1008,
        736
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "youtubeVideoId"
            },
            {
              "name": "apiToken"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "40f8b73d-fb7c-46ed-bd46-b9cca70cfca1",
      "name": "No Transcript",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        2576,
        736
      ],
      "parameters": {
        "errorMessage": "There's no transcript for this YouTube video (Transcript API)."
      },
      "typeVersion": 1
    },
    {
      "id": "366b70e0-b2c3-4857-92d5-f2ad844b46d7",
      "name": "Not Enough Credits",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1904,
        880
      ],
      "parameters": {
        "errorMessage": "Not enough credits to fetch these transcripts"
      },
      "typeVersion": 1
    },
    {
      "id": "ebdb722e-17bc-40c8-82d6-6d73802b142c",
      "name": "Sticky Note - Errors1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        864
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 184,
        "content": "## \u274c Not Enough Credits\n\nWhen no credits is available:\n- Returns a structured error\n- Ends the execution with **Stop and Error**\n"
      },
      "typeVersion": 1
    },
    {
      "id": "96510955-1afc-4456-aa7d-3a80e9c60fb1",
      "name": "Generic Error",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1904,
        1104
      ],
      "parameters": {
        "errorMessage": "Generic Error"
      },
      "typeVersion": 1
    },
    {
      "id": "07c4168c-2053-4c24-8359-c38ef676cd01",
      "name": "Sticky Note - Errors2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        1072
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 184,
        "content": "## \u274c Generic Error\n\nOther generic error\n- Ends the execution with **Stop and Error**\n"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "I8euEHwLoUMOskgW",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1",
    "saveExecutionProgress": true,
    "timeSavedPerExecution": 10
  },
  "versionId": "67a1be7b-4dd8-44ba-9f05-bcd12ff30b07",
  "connections": {
    "Switch": {
      "main": [
        [
          {
            "node": "Parse API Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Not Enough Credits",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generic Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Variables": {
      "main": [
        [
          {
            "node": "Get Transcript (YouTube Transcript API)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Has Transcript?": {
      "main": [
        [
          {
            "node": "Clean Transcript",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Transcript Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse API Response": {
      "main": [
        [
          {
            "node": "IF Has Transcript?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Transcript Fallback": {
      "main": [
        [
          {
            "node": "No Transcript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Set Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Transcript (YouTube Transcript API)": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}