{
  "id": "KYEhLDlUB9vVQcsu",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Sift- Youtube Video Summarizer",
  "tags": [],
  "nodes": [
    {
      "id": "80eab980-95ed-4ca7-8aa7-85723625454c",
      "name": "Validate Duration",
      "type": "n8n-nodes-base.code",
      "position": [
        176,
        -1408
      ],
      "parameters": {
        "jsCode": "// Get the duration from the API response\nconst duration = $input.item.json.items[0].contentDetails.duration;\n\n// Parse ISO 8601 duration format (e.g., PT10M30S, PT1H5M, PT45S)\nconst match = duration.match(/PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?/);\n\nconst hours = parseInt(match[1] || 0);\nconst minutes = parseInt(match[2] || 0);\nconst seconds = parseInt(match[3] || 0);\n\n// Calculate total minutes\nconst totalMinutes = hours * 60 + minutes + seconds / 60;\n\n// Get data from Extract YouTube node\nconst extractYouTubeNode = $('Extract YouTube video ID').first();\n\n// Check if video is too long\nif (totalMinutes > 10) {\n  // Return error flag instead of throwing\n  return {\n    json: {\n      error: true,\n      errorMessage: `Video is too long: ${Math.round(totalMinutes)} minutes. Maximum allowed is 10 minutes.`,\n      videoId: extractYouTubeNode.json.videoId,\n      videoUrl: extractYouTubeNode.json.videoUrl\n    }\n  };\n}\n\n// Pass through for valid videos\nreturn {\n  json: {\n    error: false,\n    videoId: extractYouTubeNode.json.videoId,\n    videoUrl: extractYouTubeNode.json.videoUrl,\n    durationMinutes: Math.round(totalMinutes * 10) / 10\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "a8add185-39be-4621-acde-7949fa79020b",
      "name": "Check Video Duration",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -32,
        -1408
      ],
      "parameters": {
        "url": "=https://youtube-v31.p.rapidapi.com/videos?part=contentDetails&id={{$('Extract YouTube video ID').item.json.videoId}}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-rapidapi-host",
              "value": "youtube-v31.p.rapidapi.com"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "d74f8fbd-92d9-4093-a2aa-fc2e4a089f8b",
      "name": "Receive Slack command",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -656,
        -1296
      ],
      "parameters": {
        "path": "sift",
        "options": {
          "responseData": "Filtering the fluff\u2026 give me about 2\u20134 minutes. I\u2019ll post the summary here. Sifting... \ud83d\udc68\u200d\ud83c\udf73"
        },
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "344e86a2-b00e-4807-9d0d-49d2b4e93d89",
      "name": "Normalize Slack payload",
      "type": "n8n-nodes-base.set",
      "position": [
        -432,
        -1296
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a2c85cf9-c94b-4eff-903b-0f3f331e951f",
              "name": "videoUrl",
              "type": "string",
              "value": "={{$json[\"body\"][\"text\"] || \"\"}}"
            },
            {
              "id": "f38dc534-9251-46c8-b343-7569f8929a4a",
              "name": "mode",
              "type": "string",
              "value": "={{$json.body.command === '/siftf' ? 'transcript' : 'summary'}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "4ff81a9d-fb8e-4769-ac03-bd9eaf575c59",
      "name": "Extract YouTube video ID",
      "type": "n8n-nodes-base.code",
      "position": [
        -224,
        -1200
      ],
      "parameters": {
        "jsCode": "// Accepts Slack payloads (body.text). Extracts full URL + 11-char YouTube ID.\nconst YT_RE = /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|shorts\\/|embed\\/)([A-Za-z0-9_-]{11})/i;\n\nfunction toItem(inputJson = {}) {\n  const raw = (inputJson?.body?.text ?? inputJson?.text ?? '').toString();\n  const text = raw.replace(/^\\/\\w+\\s*/, '').trim(); // drop \"/sift \"\n  const urlMatch = text.match(/https?:\\/\\/\\S+/);\n  const url = urlMatch ? urlMatch[0] : text;\n\n  const idMatch = url.match(YT_RE);\n  const videoId = idMatch ? idMatch[1] : null;\n\n  return { json: { text, videoUrl: url, videoId } };\n}\n\nconst itemsIn = $input.all();\nreturn itemsIn.length ? itemsIn.map(i => toItem(i.json)) : [toItem($json)];"
      },
      "typeVersion": 2
    },
    {
      "id": "f7802741-e91e-4fc3-bc45-f46ab69688c1",
      "name": "Is video longer than limit?",
      "type": "n8n-nodes-base.if",
      "position": [
        384,
        -1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f65a63ce-3d56-4db2-8fcb-49efb4eec1c5",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{$json.error}}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1e4eb4ad-7655-4019-801b-020e19987831",
      "name": "Send duration error to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        608,
        -1552
      ],
      "parameters": {
        "url": "={{$('Receive Slack command').item.json.body.response_url}}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "=\u274c {{$json.errorMessage}}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "567176b0-ade8-4002-9142-201e6df651ec",
      "name": "Convert YouTube video to MP3",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        592,
        -1296
      ],
      "parameters": {
        "url": "={{'https://youtube-mp3-audio-video-downloader.p.rapidapi.com/download-mp3/' + $json.videoId + '?quality=low'}}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "file",
              "outputPropertyName": "mp3"
            }
          }
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-rapidapi-host",
              "value": "youtube-mp3-audio-video-downloader.p.rapidapi.com"
            },
            {
              "name": "accept",
              "value": "application/octet-stream"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "414a5058-591f-46c1-8c92-177925539edb",
      "name": "Start transcription job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        816,
        -1296
      ],
      "parameters": {
        "url": "=https://api.assemblyai.com/v2/upload",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "binaryData",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/octet-stream"
            }
          ]
        },
        "inputDataFieldName": "mp3"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "07636da5-dfd4-45d4-9ab7-423c4c4d2aa8",
      "name": "Submit audio for transcription",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1056,
        -1296
      ],
      "parameters": {
        "url": "https://api.assemblyai.com/v2/transcript",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"audio_url\": \"{{$json['upload_url']}}\"\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ef806972-735d-4037-a9d3-8aac803ece57",
      "name": "Wait for transcription processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        1312,
        -1296
      ],
      "parameters": {
        "amount": "=20"
      },
      "typeVersion": 1.1
    },
    {
      "id": "5a5cbca2-37e5-4c70-bba2-bff401eadb99",
      "name": "Check transcription status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1504,
        -1296
      ],
      "parameters": {
        "url": "=https://api.assemblyai.com/v2/transcript/{{$node[\"Submit audio for transcription\"].json[\"id\"]}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "801a3b5c-984b-412a-bcce-f64105cf5926",
      "name": "Extract transcript text",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        -1296
      ],
      "parameters": {
        "jsCode": "// Read the mode that Edit Fields already decided\nconst mode = ($items('Normalize Slack payload', 0, 0)?.json?.mode) || 'summary';\n\n// Pass the transcript text from the AssemblyAI HTTP Request node ($json.text)\nreturn [{\n  json: {\n    transcript: $json.text ?? '',\n    mode\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "946d3c8b-3bb0-4530-99c2-9da77e5d0a46",
      "name": "Is transcription complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        1904,
        -1296
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b+1234567890ff-47fc-9460-d33980c7a4ee",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{$node[\"Check transcription status\"].json[\"status\"].toLowerCase()}}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "226a2079-8b1a-4df6-bf69-359826e4bb2b",
      "name": "Generate AI summary",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2096,
        -1312
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4-turbo",
          "cachedResultName": "GPT-4-TURBO"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "=You are a professional long-form content summarizer with 25 years of experience. Your job: produce concise, high-signal summaries of video transcripts across any topic. Be neutral, factual, and skimmable for Slack.\n\nRules\n\u2022 Extract, don\u2019t infer: Include only facts stated in the transcript. No moralizing, no opinions, no external context.  \n\u2022 Evidence-first bullets: Start bullets with a *bold* entity/date/metric, then the fact.  \n\u2022 Specifics over adjectives: Keep names, figures, dates, examples. Prefer numbers to descriptors.  \n\u2022 Clarity > coverage: Drop details that don\u2019t affect the core message.  \n\u2022 Language: Respond in the transcript\u2019s language (use the majority language if mixed).  \n\u2022 Formatting (Slack Markdown only):  \n  \u2013 Use *bold* and _italics_ (single asterisks/underscores).  \n  \u2013 Use bullets (\u2022) for lists; one blank line between sections.  \n  \u2013 No double-asterisk style (**\u2026**), no triple backticks, no code blocks, no emojis (unless provided in transcript).  \n  \u2013 Keep line lengths skimmable in Slack.  \n\u2022 Length budgets (hard caps):  \n  \u2013 TL;DR: \u2264 25 words  \n  \u2013 Key Takeaways: 5\u20137 bullets, each \u2264 22 words  \n  \u2013 Speaker\u2019s Points (only if explicitly enumerated): each \u2264 20 words  \n  \u2013 Notable Quotes: \u2264 2 short quotes or timestamps (optional)  \n\u2022 Enumerated Points rule: If (and only if) the speaker explicitly lists points/steps/reasons (e.g., \u201cthree points\u2026 first/second/third\u201d), include a *Speaker\u2019s Points* section that mirrors the numbering exactly.  \n\u2022 Gaps: If portions are unclear or missing, end with: _Note: Some portions unclear / missing._\n\nOutput format (exactly)\n\n*TL;DR*  \n{one sentence, \u2264 25 words}\n\n*Key Takeaways*  \n\u2022 *[Entity/Date/Metric]* brief fact statement.  \n\u2022 *[\u2026]* \u2026  \n\u2022 *[\u2026]* \u2026\n\n*Speaker\u2019s Points* (include only if the speaker explicitly enumerated them)  \n1. First: \u2026  \n2. Second: \u2026  \n3. Third: \u2026\n\n*Notable Quotes / Moments* (optional)  \n\u2022 \u201c\u2026\u201d (timestamp if stated)  \n\u2022 \u201c\u2026\u201d (timestamp if stated)\n\nHard guardrails\nDo not add opinions, moral framing, recommendations, \u201cmy take,\u201d or external context. Do not speculate about motives or guilt. If a claim is ambiguous or not supported by the transcript, omit it or mark it unclear. Maintain a neutral, factual tone at all times."
            },
            {
              "content": "={{$node[\"Extract transcript text\"].json[\"transcript\"]}}"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "21771809-513d-427d-bc64-8d98091b21d9",
      "name": "Post result to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2384,
        -1312
      ],
      "parameters": {
        "url": "={{$node[\"Receive Slack command\"].json[\"body\"][\"response_url\"]}}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "response_type",
              "value": "in_channel"
            },
            {
              "name": "text",
              "value": "=*SIFT Summary*\n<{{$node[\"Extract YouTube video ID\"].json[\"videoUrl\"]}}|:film_projector: Original Video>\n\n{{$node[\"Generate AI summary\"].json[\"message\"][\"content\"] ?? ($json[\"message\"] ? $json[\"message\"][0][\"content\"] : \"\")}}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": " Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "64e4806a-efd3-49c6-9927-3f64584e6dbd",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        -1760
      ],
      "parameters": {
        "width": 896,
        "height": 560,
        "content": "## How it works\n\nThis workflow lets users submit a YouTube link from Slack and receive a clean, AI-generated summary directly in the same Slack channel.\n\nWhen a user invokes the Slack command, the workflow extracts the YouTube video URL and checks the video duration using the YouTube Data API. Videos longer than the supported limit are politely rejected with a clear message sent back to Slack.\n\nIf the video is within the allowed length, the workflow converts the video into an audio file and sends it to AssemblyAI for transcription. The workflow waits while the transcription is processed and periodically checks its status until the transcript is complete.\n\nOnce the transcript is ready, the workflow uses an AI model to generate a concise summary including key takeaways and notable points. The final result is formatted for readability and posted back to Slack using the original response URL.\n\nThis approach ensures efficient processing, avoids unnecessary API usage, and provides users with fast, readable insights from YouTube content without leaving Slack.\n\n## Setup steps\n\n1. Create a Slack slash command and configure it to send requests to the Webhook node.\n2. Add API credentials for YouTube Data API, RapidAPI, AssemblyAI, and OpenAI.\n3. Adjust the maximum allowed video duration if needed.\n4. Activate the workflow and test it by submitting a YouTube link from Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "5a70f943-d6b4-455a-8afe-bf7c1c0e7aa8",
      "name": "Sticky Note18",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        -1584
      ],
      "parameters": {
        "color": 7,
        "height": 112,
        "content": "Extracts the YouTube video ID and validates video duration before processing."
      },
      "typeVersion": 1
    },
    {
      "id": "fec7dfca-e023-4ed8-945f-faaee8bec7b7",
      "name": "Sticky Note19",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -1488
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Converts the video to audio and submits it for transcription."
      },
      "typeVersion": 1
    },
    {
      "id": "ec8e5fe0-4f64-453b-8fa4-f74c81f66407",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1296,
        -1488
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Waits for transcription completion and retrieves the final transcript."
      },
      "typeVersion": 1
    },
    {
      "id": "ea933c45-82c8-4618-8b1b-d4156bb08b61",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        -1472
      ],
      "parameters": {
        "color": 7,
        "height": 80,
        "content": "Generates an AI summary and sends the result back to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "c30ff2c3-f052-427a-a429-b26deccf8389",
      "name": "Sticky Note17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        -1440
      ],
      "parameters": {
        "color": 7,
        "height": 96,
        "content": "Slack command input and payload normalization.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3b89df69-635c-444c-ad76-c96b79e02569",
  "connections": {
    "Validate Duration": {
      "main": [
        [
          {
            "node": "Is video longer than limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI summary": {
      "main": [
        [
          {
            "node": "Post result to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Video Duration": {
      "main": [
        [
          {
            "node": "Validate Duration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Slack command": {
      "main": [
        [
          {
            "node": "Normalize Slack payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract transcript text": {
      "main": [
        [
          {
            "node": "Is transcription complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Slack payload": {
      "main": [
        [
          {
            "node": "Extract YouTube video ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start transcription job": {
      "main": [
        [
          {
            "node": "Submit audio for transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract YouTube video ID": {
      "main": [
        [
          {
            "node": "Check Video Duration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check transcription status": {
      "main": [
        [
          {
            "node": "Extract transcript text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is transcription complete?": {
      "main": [
        [
          {
            "node": "Generate AI summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait for transcription processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is video longer than limit?": {
      "main": [
        [
          {
            "node": "Send duration error to Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Convert YouTube video to MP3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert YouTube video to MP3": {
      "main": [
        [
          {
            "node": "Start transcription job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send duration error to Slack": {
      "main": [
        []
      ]
    },
    "Submit audio for transcription": {
      "main": [
        [
          {
            "node": "Wait for transcription processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for transcription processing": {
      "main": [
        [
          {
            "node": "Check transcription status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}