AutomationFlowsAI & RAG › Summarize Youtube Video Transcripts in Discord with Gemini and Supabase

Summarize Youtube Video Transcripts in Discord with Gemini and Supabase

ByTristan V @tristanv on n8n.io

> Paste a YouTube URL into a Discord channel and this workflow automatically extracts the transcript, uses an LLM to generate a concise summary, and stores everything in a database — all in seconds.

Event trigger★★★★☆ complexityAI-powered29 nodesN8N Nodes Discord TriggerExecute CommandGoogle GeminiSupabaseDiscordError Trigger
AI & RAG Trigger: Event Nodes: 29 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #13749 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "1273ffea-39d3-42cc-bcb1-abd403ea3bd7",
      "name": "Extract YouTube URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -576,
        336
      ],
      "parameters": {
        "jsCode": "// Extract YouTube URL from Discord message\nconst message = $input.first().json;\nconst messageContent = message.content;\n\nif (!messageContent) {\n  return [];\n}\n\n// Match youtube.com/watch?v= and youtu.be/ formats\nconst ytRegex = /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com\\/(?:watch\\?v=|live\\/|shorts\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})(?:[&?][^\\s]*)*/;\nconst match = messageContent.match(ytRegex);\n\nif (!match) {\n  return [{\n    json: {\n      is_youtube: false,\n      channel_id: message.channelId\n    }\n  }];\n}\n\nconst videoId = match[1];\nconst videoUrl = `https://www.youtube.com/watch?v=${videoId}`;\n\nreturn [{\n  json: {\n    is_youtube: true,\n    video_id: videoId,\n    video_url: videoUrl,\n    discord_message: messageContent,\n    discord_shared_at: new Date().toISOString(),\n    channel_id: message.channelId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "610f37f3-d1c9-491f-9fa9-eda1af30e9d6",
      "name": "Discord Trigger",
      "type": "n8n-nodes-discord-trigger.discordTrigger",
      "position": [
        -800,
        336
      ],
      "parameters": {
        "pattern": "every",
        "guildIds": [
          "YOUR_GUILD_ID_HERE"
        ],
        "channelIds": [
          "YOUR_CHANNEL_ID_HERE"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "discordBotTriggerApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9e31fe2d-41e3-4405-8340-4a63650ea96d",
      "name": "Is YouTube URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        -32,
        336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-yt-check",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.is_youtube }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3a55d9c6-c8b5-4170-be43-5d9b19c82b4d",
      "name": "yt-dlp Get Metadata",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        192,
        240
      ],
      "parameters": {
        "command": "=yt-dlp --js-runtime node --remote-components ejs:github --cookies /home/node/.n8n/cookies.txt --write-auto-sub --sub-lang \"en.*,vi.*\" --skip-download -o \"/tmp/yt_%(id)s\" \"{{ $node[\"Extract YouTube URL\"].json[\"video_url\"] }}\" 2>/dev/null; yt-dlp --js-runtime node --remote-components ejs:github --cookies /home/node/.n8n/cookies.txt --print \"{\\\"id\\\":%(id)#j,\\\"title\\\":%(title)#j,\\\"description\\\":%(description)#j,\\\"view_count\\\":%(view_count)s,\\\"channel\\\":%(channel)#j,\\\"upload_date\\\":%(upload_date)#j,\\\"duration\\\":%(duration)s}\" --skip-download \"{{ $node[\"Extract YouTube URL\"].json[\"video_url\"] }}\""
      },
      "typeVersion": 1
    },
    {
      "id": "6121252c-8e1f-446b-9b00-48039ed9eaaa",
      "name": "Parse Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        416,
        240
      ],
      "parameters": {
        "jsCode": "const rawOutput = $input.first().json.stdout;\n\nif (!rawOutput) {\n  throw new Error('yt-dlp returned no output \u2014 video may be private, deleted, or geo-blocked');\n}\n\n// Decode Unicode escapes from JSON string values\nfunction decodeUnicode(str) {\n  try { return JSON.parse('\"' + str + '\"'); } catch (e) { return str; }\n}\n\n// Extract fields using regex since description may break JSON\nconst getId = rawOutput.match(/\"id\":\"([^\"]+)\"/);\nconst getTitle = rawOutput.match(/\"title\":\"([^\"]+)\"/);\nconst getChannel = rawOutput.match(/\"channel\":\"([^\"]+)\"/);\nconst getDate = rawOutput.match(/\"upload_date\":\"([^\"]+)\"/);\nconst getDuration = rawOutput.match(/\"duration\":(\\d+)/);\nconst getViews = rawOutput.match(/\"view_count\":(\\d+)/);\n\nif (!getId) throw new Error('MISSING: video id not found');\nif (!getTitle) throw new Error('MISSING: title not found');\nif (!getChannel) throw new Error('MISSING: channel not found');\nif (!getDate) throw new Error('MISSING: upload_date not found');\nif (!getDuration) throw new Error('MISSING: duration not found');\nif (!getViews) throw new Error('MISSING: view_count not found');\n\n// Extract description: match up to the next known key\nconst descMatch = rawOutput.match(/\"description\":\"([\\s\\S]*?)\",\"view_count\"/);\nconst description = descMatch ? decodeUnicode(descMatch[1]) : '';\n\nconst prevData = $('Extract YouTube URL').first().json;\n\nreturn [{\n  json: {\n    video_id: getId[1],\n    title: decodeUnicode(getTitle[1]),\n    channel: decodeUnicode(getChannel[1]),\n    upload_date: getDate[1],\n    duration: parseInt(getDuration[1]),\n    view_count: parseInt(getViews[1]),\n    description: description,\n    thumbnail_url: `https://img.youtube.com/vi/${getId[1]}/maxresdefault.jpg`,\n    video_url: prevData.video_url,\n    discord_shared_at: prevData.discord_shared_at,\n    channel_id: prevData.channel_id,\n    video_id_for_subs: getId[1]\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "fd999ffd-80b4-4248-bf9a-77247fe5d9f8",
      "name": "Read Subtitle File",
      "type": "n8n-nodes-base.executeCommand",
      "onError": "continueRegularOutput",
      "position": [
        640,
        240
      ],
      "parameters": {
        "command": "=FILE=$(ls /tmp/yt_{{ $json.video_id }}.*.vtt 2>/dev/null | head -1); [ -n \"$FILE\" ] && cat \"$FILE\" || echo \"\""
      },
      "typeVersion": 1
    },
    {
      "id": "f77139fe-ebad-4c6c-9d63-55561a004918",
      "name": "Parse Transcript",
      "type": "n8n-nodes-base.code",
      "position": [
        864,
        240
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst prevData = $('Parse Metadata').first().json;\n\n// Check if Read Subtitle File failed (continueOnFail)\nif (input.error || !input.stdout || input.stdout.trim().length === 0) {\n  throw new Error('No subtitles available for video: ' + prevData.video_id);\n}\n\nconst vttContent = input.stdout;\nconst lines = vttContent.split('\\n');\nconst textLines = [];\nlet lastLine = \"\";\n\nfor (let line of lines) {\n    let trimmed = line.trim();\n    \n    // Skip VTT metadata and timestamps\n    if (!trimmed || \n        trimmed === 'WEBVTT' || \n        trimmed.startsWith('Kind:') || \n        trimmed.startsWith('Language:') || \n        /^(\\d{2}:)?\\d{2}:\\d{2}\\.\\d{3}/.test(trimmed) || \n        trimmed.includes('-->')) {\n        continue;\n    }\n\n    // Remove HTML-like tags (styling)\n    const cleaned = trimmed.replace(/<[^>]+>/g, '').trim();\n    \n    // Prevent duplicate adjacent lines (common in VTT)\n    if (cleaned && cleaned !== lastLine) {\n        textLines.push(cleaned);\n        lastLine = cleaned;\n    }\n}\n\nconst transcript = textLines.join(' ');\n\nreturn [{\n    json: {\n        ...prevData,\n        transcript: transcript\n    }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "26965400-999e-4dea-bdf1-c8190da03f5f",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        1088,
        240
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash",
          "cachedResultName": "models/gemini-2.5-flash"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=Please summarize this YouTube video.\n\nVideo Title: {{ $json.title }}\nChannel: {{ $json.channel }}\n\nTranscript:\n{{ $json.transcript }}\n\nIMPORTANT: Write the summary in the SAME LANGUAGE as the transcript. Do not translate to English.\n\nProvide a concise summary of the video. Format your response exactly as follows:\n\n**TLDR**\n[Write a single paragraph, 3-4 sentence summary of the main idea here]\n\n**Summary**\n[Write a detailed 3-5 paragraph summary capturing key points, arguments, and conclusions here]\n\nWrite in clear, informative prose. No bullet points.\n"
            }
          ]
        }
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f0067574-707f-46b5-a725-8988539c20da",
      "name": "Prepare Insert Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        240
      ],
      "parameters": {
        "jsCode": "const currentData = $('Parse Transcript').first().json;\nconst geminiOutput = $input.first().json;\n\nconst summary = geminiOutput.content.parts[0].text;\n\nif (!summary) {\n  throw new Error('MISSING: Gemini returned no summary text');\n}\n\nreturn [{\n  json: {\n    video_id: currentData.video_id,\n    title: currentData.title,\n    channel: currentData.channel,\n    upload_date: currentData.upload_date,\n    duration: currentData.duration,\n    view_count: currentData.view_count,\n    description: currentData.description,\n    transcript: currentData.transcript,\n    ai_summary: summary,\n    thumbnail_url: currentData.thumbnail_url,\n    discord_shared_at: currentData.discord_shared_at,\n    channel_id: currentData.channel_id\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b9f7473c-2bc1-4337-8efb-4b555cc8439c",
      "name": "Save to Supabase",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1664,
        240
      ],
      "parameters": {
        "tableId": "videos",
        "dataToSend": "=autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5606a6df-0d30-4b1f-896f-5492ed37d048",
      "name": "Prepare Success Log",
      "type": "n8n-nodes-base.code",
      "position": [
        1888,
        240
      ],
      "parameters": {
        "jsCode": "const data = $('Prepare Insert Data').first().json;\n\nreturn [{\n  json: {\n    video_id: data.video_id,\n    process_status: 'success',\n    error_type: null,\n    notes: null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d94f1688-aed4-4cfa-8553-77519c45fa17",
      "name": "Log Run",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2112,
        240
      ],
      "parameters": {
        "tableId": "runs",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "e932de7a-bc59-4523-bd96-81b8855e4cc7",
      "name": "Discord Reply",
      "type": "n8n-nodes-base.discord",
      "position": [
        2528,
        240
      ],
      "parameters": {
        "content": "={{ $json.chunk }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GUILD_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "Your Server"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_CHANNEL_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "your-channel"
        }
      },
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ac5c0018-638c-4c15-9138-ce6989427dae",
      "name": "Discord Not YouTube Reply",
      "type": "n8n-nodes-base.discord",
      "position": [
        192,
        432
      ],
      "parameters": {
        "content": "That doesn't look like a YouTube link. Please share a valid YouTube URL.",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GUILD_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "Your Server"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_CHANNEL_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "your-channel"
        }
      },
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6516263a-56ab-4892-9a54-7699850fda00",
      "name": "Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -800,
        656
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "8fa011ac-baa2-43d0-9ed6-a731d5532d62",
      "name": "Prepare Error Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -576,
        656
      ],
      "parameters": {
        "jsCode": "const errorData = $input.first().json;\nconst errorMessage = errorData.execution?.error?.message || 'Unknown error';\nconst errorNode = errorData.execution?.error?.node?.name || 'Unknown node';\n\n// Determine error_type based on failing node\nlet errorType = 'unknown';\nif (errorNode.includes('yt-dlp') || errorNode === 'Parse Metadata') {\n  errorType = 'yt_dlp_failed';\n} else if (errorNode === 'Parse Transcript') {\n  errorType = errorMessage.includes('No subtitles') ? 'no_subtitles' : 'transcript_error';\n} else if (errorNode.includes('model') || errorNode.includes('Gemini')) {\n  errorType = 'gemini_error';\n} else if (errorNode.includes('Supabase') || errorNode.includes('Insert')) {\n  errorType = 'supabase_error';\n} else if (errorNode.includes('Prepare')) {\n  errorType = 'gemini_error';\n}\n\n// Try to extract video_id from error message\nconst vidMatch = errorMessage.match(/(?:video:\\s*|video_id:\\s*|yt_)([a-zA-Z0-9_-]{11})/);\nconst videoId = vidMatch ? vidMatch[1] : 'unknown';\n\nreturn [{\n  json: {\n    video_id: videoId,\n    process_status: 'error',\n    error_type: errorType,\n    notes: `[${errorNode}] ${errorMessage}`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1c208dab-6014-4835-9a68-00a9e721a6bd",
      "name": "Log Run Error",
      "type": "n8n-nodes-base.supabase",
      "position": [
        -32,
        656
      ],
      "parameters": {
        "tableId": "runs",
        "dataToSend": "autoMapInputData"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6ddf1a0a-ed65-45de-b758-3f8040cc026d",
      "name": "Discord Error Reply",
      "type": "n8n-nodes-base.discord",
      "position": [
        192,
        656
      ],
      "parameters": {
        "content": "=Error processing video: {{ $('Prepare Error Data').first().json.notes }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GUILD_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "Your Server"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_CHANNEL_ID_HERE",
          "cachedResultUrl": "",
          "cachedResultName": "your-channel"
        }
      },
      "credentials": {
        "discordBotApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6dcab5da-8423-482c-b6d9-ffee1fa6c9a4",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        -64
      ],
      "parameters": {
        "color": 4,
        "width": 496,
        "height": 892,
        "content": "# YouTube Video Summarizer \u2014 Discord Bot\n\n## Who is this for?\nTeams or creators who want to automatically summarize YouTube videos shared in a Discord channel.\n\n## What does it do?\n- Listens for YouTube links posted in a Discord channel\n- Downloads subtitles and metadata via yt-dlp\n- Summarizes the transcript using Gemini 2.5 Flash\n- Saves the full data (metadata + transcript + summary) to Supabase\n- Replies in Discord with a summary preview\n- Logs every run (success or error) to a `runs` table\n- Replies with helpful error messages when something goes wrong\n\n## Setup Requirements\n1. **Discord Bot** with message read + send permissions\n2. **yt-dlp** installed in the n8n container (with cookies.txt)\n3. **Google Gemini API Key** (Gemini 2.5 Flash)\n4. **Supabase** project with `videos` and `runs` tables\n\n## Quick Start\n1. Import the workflow into n8n\n2. Configure Discord Bot Trigger + Discord Bot credentials\n3. Configure Gemini API credential\n4. Configure Supabase credential\n5. Create `videos` and `runs` tables in Supabase\n6. Update Discord guild/channel IDs to match your server\n7. Activate the workflow\n8. Post a YouTube link in your Discord channel!"
      },
      "typeVersion": 1
    },
    {
      "id": "48bfa715-4fed-473f-b804-ba0f735ec43b",
      "name": "Sticky Note - Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -832,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 228,
        "content": "## 1. Trigger & URL Detection\n\nListens for every message in the configured Discord channel and checks if it contains a YouTube URL.\n\n**How it works:**\n1. Discord Trigger fires on every new message\n2. Code node extracts YouTube video ID via RegEx\n3. IF node routes: YouTube URL \u2192 processing, other messages \u2192 friendly reply"
      },
      "typeVersion": 1
    },
    {
      "id": "f6446181-cbbe-470a-b654-fc8274e71379",
      "name": "Sticky Note - Video Data Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 256,
        "content": "## 2. Video Data Extraction\n\nRuns yt-dlp to download subtitles and fetch video metadata in a single shell command.\n\n**How it works:**\n1. yt-dlp downloads auto-generated English subtitles (.vtt) to /tmp\n2. yt-dlp prints metadata JSON (title, channel, views, duration)\n3. Parse Metadata extracts fields via RegEx (handles broken JSON from descriptions)"
      },
      "typeVersion": 1
    },
    {
      "id": "dfb5a5b4-921a-44b8-863a-f0a34ef284f6",
      "name": "Sticky Note - Transcript Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 240,
        "content": "## 3. Transcript Processing\n\nReads the .vtt subtitle file and cleans it into plain text.\n\n**How it works:**\n1. `cat` reads the VTT file (continueOnFail enabled)\n2. Parse Transcript strips timestamps, HTML tags, and deduplicates lines\n3. If no subtitles exist, throws a descriptive error caught by Error Trigger"
      },
      "typeVersion": 1
    },
    {
      "id": "116e832f-4c11-451c-a3bb-66680d3b7f03",
      "name": "Sticky Note - AI Summarization",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 536,
        "height": 240,
        "content": "## 4. AI Summarization\n\nSends the clean transcript to Gemini 2.5 Flash for a concise summary.\n\n**How it works:**\n1. Gemini receives title, channel, and full transcript\n2. Returns 3-5 paragraphs of prose (no bullet points)\n3. Prepare Insert Data merges summary with all metadata fields"
      },
      "typeVersion": 1
    },
    {
      "id": "25b52b6c-db1c-4439-9e23-9481690c3837",
      "name": "Sticky Note - Save & Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 900,
        "height": 240,
        "content": "## 5. Save & Notify\n\nPersists data to Supabase and replies in Discord.\n\n**How it works:**\n1. Save to Supabase inserts all fields into the `videos` table\n2. Prepare Success Log builds a run record (status: success)\n3. Log Run inserts into the `runs` table\n4. Discord Reply posts a summary preview (title, duration, views, first 500 chars)"
      },
      "typeVersion": 1
    },
    {
      "id": "646554d2-c672-415b-a33a-1da62f3ed27d",
      "name": "Sticky Note - Error Handling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -816,
        832
      ],
      "parameters": {
        "color": 7,
        "width": 740,
        "height": 256,
        "content": "## 6. Error Handling\n\nCatches any workflow crash and replies in Discord with the error details.\n\n**How it works:**\n1. Error Trigger fires on any unhandled node failure\n2. Prepare Error Data classifies the error type (yt_dlp_failed, no_subtitles, gemini_error, supabase_error)\n3. Log Run Error saves the error to the `runs` table\n4. Discord Error Reply posts the error message to the channel"
      },
      "typeVersion": 1
    },
    {
      "id": "eb2d6b48-a92c-4b0e-a778-e16fbe698b57",
      "name": "Sticky Note - Discord Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        1120
      ],
      "parameters": {
        "color": 3,
        "width": 440,
        "height": 300,
        "content": "### Discord Bot Setup\n\n1. Go to [Discord Developer Portal](https://discord.com/developers/applications)\n2. Create a new Application \u2192 Bot\n3. Enable **Message Content Intent** under Privileged Intents\n4. Copy the Bot Token\n5. Invite bot to your server with Send Messages + Read Messages permissions\n6. In n8n: Create **Discord Bot Trigger** credential (for listening)\n7. Create **Discord Bot** credential (for sending replies)\n8. Update guild ID and channel ID in Trigger + Reply nodes"
      },
      "typeVersion": 1
    },
    {
      "id": "4c21a5a4-694b-4ca8-bbb1-83c9cf42915f",
      "name": "Sticky Note - Gemini Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        496
      ],
      "parameters": {
        "color": 3,
        "width": 400,
        "height": 240,
        "content": "### Gemini API Setup\n\n1. Go to [Google AI Studio](https://aistudio.google.com/apikey)\n2. Click **Create API Key**\n3. Copy the key\n4. In n8n: Click the Gemini node \u2192 Credential \u2192 Create New\n5. Paste your API key and save\n6. Model: `gemini-2.5-flash` (default)"
      },
      "typeVersion": 1
    },
    {
      "id": "447e0689-b5bc-4f93-aea4-e6a27a977d3c",
      "name": "Sticky Note - Supabase Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        496
      ],
      "parameters": {
        "color": 3,
        "width": 440,
        "height": 480,
        "content": "### Supabase Setup\n\n1. Create a project at [supabase.com](https://supabase.com)\n2. Go to Settings \u2192 API \u2192 copy the **URL** and **anon key**\n3. In n8n: Create Supabase credential with URL + API key\n4. Run the SQL below to create required tables:\n\n```sql\nCREATE TABLE videos (\n  video_id TEXT PRIMARY KEY,\n  title TEXT, channel TEXT,\n  upload_date TEXT, duration INT,\n  view_count INT, description TEXT,\n  transcript TEXT, ai_summary TEXT,\n  thumbnail_url TEXT,\n  discord_shared_at TIMESTAMPTZ,\n  channel_id TEXT\n);\n\nCREATE TABLE runs (\n  video_id TEXT PRIMARY KEY,\n  process_status TEXT NOT NULL,\n  error_type TEXT, notes TEXT,\n  date_added TIMESTAMPTZ DEFAULT now()\n);\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "b32a9d85-67a9-4217-984c-603702b292c0",
      "name": "Prepare Messages for Discord",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        240
      ],
      "parameters": {
        "jsCode": "// Get the full summary and title from the previous node\nconst summary = $('Prepare Insert Data').first().json.ai_summary || \"No summary available\";\nconst data = $('Prepare Insert Data').first().json;\nconst discordLimit = 2000; // Discord max message length\nconst safeLimit = 1900; // Leave some buffer\nconst chunks = [];\n\n// Create the header for the very first message\nconst header = `Video saved! **${data.title}** by **${data.channel}** | Duration: ${Math.floor(data.duration / 60)}m ${data.duration % 60}s | ${data.view_count} views ---\\n\\n`;\n\nlet remainingText = summary;\n\n// Loop through the text and break it into chunks intelligently\nwhile (remainingText.length > 0) {\n    // For the first chunk, reduce max length by the header size\n    const isFirstChunk = chunks.length === 0;\n    const maxLength = isFirstChunk ? safeLimit - header.length : safeLimit;\n\n    if (remainingText.length <= maxLength) {\n        // If the remaining text fits, add it and break\n        chunks.push({ json: { chunk: (isFirstChunk ? header : \"\") + remainingText } });\n        break;\n    }\n\n    // Find the last space within the limit to avoid breaking a word\n    let splitIndex = remainingText.lastIndexOf(\" \", maxLength);\n    \n    // If no space is found, split strictly at maxLength (fallback for long words)\n    if (splitIndex === -1) {\n        splitIndex = maxLength;\n    }\n\n    let chunkText = remainingText.substring(0, splitIndex);\n    \n    // Prepend the header only to the first chunk\n    if (isFirstChunk) {\n        chunkText = header + chunkText;\n    }\n\n    chunks.push({ json: { chunk: chunkText } });\n    \n    // Update remaining text for the next iteration\n    remainingText = remainingText.substring(splitIndex).trim();\n}\n\nreturn chunks;"
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "Log Run": {
      "main": [
        [
          {
            "node": "Prepare Messages for Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Prepare Error Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Run Error": {
      "main": [
        [
          {
            "node": "Discord Error Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Metadata": {
      "main": [
        [
          {
            "node": "Read Subtitle File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord Trigger": {
      "main": [
        [
          {
            "node": "Extract YouTube URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is YouTube URL?": {
      "main": [
        [
          {
            "node": "yt-dlp Get Metadata",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Discord Not YouTube Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Prepare Insert Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Transcript": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Supabase": {
      "main": [
        [
          {
            "node": "Prepare Success Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Error Data": {
      "main": [
        [
          {
            "node": "Log Run Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Subtitle File": {
      "main": [
        [
          {
            "node": "Parse Transcript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract YouTube URL": {
      "main": [
        [
          {
            "node": "Is YouTube URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Insert Data": {
      "main": [
        [
          {
            "node": "Save to Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Success Log": {
      "main": [
        [
          {
            "node": "Log Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "yt-dlp Get Metadata": {
      "main": [
        [
          {
            "node": "Parse Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Messages for Discord": {
      "main": [
        [
          {
            "node": "Discord Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

&gt; Paste a YouTube URL into a Discord channel and this workflow automatically extracts the transcript, uses an LLM to generate a concise summary, and stores everything in a database — all in seconds.

Source: https://n8n.io/workflows/13749/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow automatically collects financial news from multiple trusted sources, filters and cleans the data and uses AI (Google Gemini) to generate a concise daily wealth briefing. The final summar

RSS Feed Read, Google Gemini, Discord
AI & RAG

This workflow is designed for e-commerce, marketing teams, or creators who want to automate the production of high-quality, AI-generated product visuals and ad creatives.

HTTP Request, Error Trigger, Discord +7
AI & RAG

This workflow helps you repurpose your YouTube videos across multiple social media platforms with zero manual effort. It’s designed for creators, businesses, and marketers who want to maximize reach w

HTTP Request, RSS Feed Read, Discord +4
AI & RAG

This workflow automatically fetches news articles from NewsAPI, processes them using an AI model to generate summaries and key regulatory changes and stores both raw and processed data into Google She

HTTP Request, Google Gemini, Google Sheets +1
AI & RAG

This workflow will run on a weekly schedule and retrieve your Notion Daily Journal pages for the past week and aggregate them into a ChatGPT generated concise summary. It will save that weekly summary

Notion, Discord, OpenAI