AutomationFlowsSocial Media › Youtube Outlier Detector (find Trending Content Based on Your Competitors)

Youtube Outlier Detector (find Trending Content Based on Your Competitors)

ByLeonardo Grigorio @leonardogrig on n8n.io

Video explanation

Event trigger★★★★☆ complexity21 nodesHTTP RequestExecute Workflow TriggerPostgresYouTube
Social Media Trigger: Event Nodes: 21 Complexity: ★★★★☆ Added:

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

This workflow follows the Execute Workflow Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.

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
{
  "id": "Zrd98BnbmN1Px9an",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Youtube Searcher",
  "tags": [],
  "nodes": [
    {
      "id": "5cb8757a-d8f0-49fa-803d-7f04b514f9f8",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        80,
        220
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "28964bd5-dc53-4dfa-bbb1-4eb80b952063",
      "name": "find_video_data1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1440,
        320
      ],
      "parameters": {
        "url": "https://www.googleapis.com/youtube/v3/videos?",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "key",
              "value": "={{ $env[\"GOOGLE_API_KEY\"] }}"
            },
            {
              "name": "id",
              "value": "={{ $json.id.videoId }}"
            },
            {
              "name": "part",
              "value": "contentDetails, statistics"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "5e8b9441-4b91-4460-a9ac-4a0a02aa57ad",
      "name": "When clicking \u2018Test workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "disabled": true,
      "position": [
        -180,
        220
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "793ef651-ea56-41bc-a0a9-feeaddf999c0",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -160,
        -180
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "64e331ff-2cda-4ba0-94f9-03fa6c3d6590",
      "name": "fetch_last_registered",
      "type": "n8n-nodes-base.postgres",
      "position": [
        360,
        360
      ],
      "parameters": {
        "query": "SELECT MAX(publish_time) AS latest_publish_time\nFROM video_statistics\nWHERE channel_id = '{{ $json.id }}';",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "fb0a8208-c920-4344-8816-ef6509f07abc",
      "name": "get_videos",
      "type": "n8n-nodes-base.youTube",
      "onError": "continueRegularOutput",
      "position": [
        640,
        360
      ],
      "parameters": {
        "limit": 50,
        "filters": {
          "channelId": "={{ $('Loop Over Items').item.json.id }}",
          "regionCode": "US",
          "publishedAfter": "={{ $json.latest_publish_time ? new Date(new Date($json.latest_publish_time).getTime() + 60 * 60 * 1000).toISOString() : new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000).toISOString() }}"
        },
        "options": {
          "order": "relevance",
          "safeSearch": "moderate"
        },
        "resource": "video"
      },
      "credentials": {
        "youTubeOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "ea358d3c-9a83-49c9-a02e-745cf5b29097",
      "name": "if_is_empty",
      "type": "n8n-nodes-base.if",
      "onError": "continueRegularOutput",
      "position": [
        940,
        540
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "7591deae-4626-4b2e-af26-d02042573a13",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $input.item.json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "142e5c5e-f488-4667-a759-ef4494f2a194",
      "name": "Postgres",
      "type": "n8n-nodes-base.postgres",
      "position": [
        80,
        -180
      ],
      "parameters": {
        "query": "WITH RankedVideos AS (\n    SELECT \n        channel_id,\n        id,\n        view_count,\n        like_count,\n        comment_count,\n        publish_time,\n        ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY view_count DESC) AS rank_desc,\n        ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY view_count ASC) AS rank_asc\n    FROM video_statistics\n),\nFilteredVideos AS (\n    SELECT \n        channel_id,\n        id,\n        view_count,\n        like_count,\n        comment_count,\n        publish_time\n    FROM RankedVideos\n    WHERE NOT (\n        rank_desc <= 2 OR rank_asc <= 2  -- Exclude top 2 and bottom 2 videos\n    )\n    OR (\n        (SELECT COUNT(*) FROM video_statistics WHERE video_statistics.channel_id = RankedVideos.channel_id) <= 10  -- Include all videos if 10 or fewer exist\n    )\n),\nChannelStats AS (\n    SELECT \n        channel_id,\n        ROUND(AVG(view_count)::NUMERIC, 0) AS average_views -- Round to 0 decimal places\n    FROM FilteredVideos\n    GROUP BY channel_id\n)\nSELECT \n    v.channel_id,\n    c.average_views,\n    JSON_AGG(\n        JSON_BUILD_OBJECT(\n            'id', v.id,\n            'view_count', v.view_count,\n            'like_count', v.like_count,\n            'comment_count', v.comment_count,\n            'publish_time', v.publish_time\n        )\n    ) AS channel_videos\nFROM video_statistics v\nLEFT JOIN ChannelStats c\nON v.channel_id = c.channel_id\nGROUP BY v.channel_id, c.average_views;\n",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "a542b55e-bab4-476d-8333-692f5b3a5dcb",
      "name": "insert_items",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2980,
        320
      ],
      "parameters": {
        "query": "{{$json.query}}",
        "options": {
          "queryReplacement": "={{$json.parameters}}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "6680728a-805e-4a45-8720-56726ad9e582",
      "name": "create_table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        620,
        -180
      ],
      "parameters": {
        "query": "CREATE TABLE video_statistics (\n    id VARCHAR(255) PRIMARY KEY, -- Unique identifier for the video\n    view_count INT NOT NULL, -- Number of views\n    like_count INT NOT NULL, -- Number of likes\n    comment_count INT NOT NULL, -- Number of comments\n    publish_time TIMESTAMP NOT NULL, -- Timestamp of publishing\n    channel_id VARCHAR(255) NOT NULL -- Channel ID\n);\n",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "4e345df5-bdd6-4a93-9096-367bd911dbd4",
      "name": "remove_shorts",
      "type": "n8n-nodes-base.code",
      "position": [
        1720,
        320
      ],
      "parameters": {
        "jsCode": "const input = $input.all();\n\nconst iso8601ToSeconds = iso8601 => {\n  const match = iso8601 ? iso8601.match(/PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?/) : null;\n  if (!match) {\n    console.warn(`Invalid ISO8601 duration: ${iso8601}`);\n    return 0; \n  }\n  const hours = parseInt(match[1] || 0, 10);\n  const minutes = parseInt(match[2] || 0, 10);\n  const seconds = parseInt(match[3] || 0, 10);\n  return hours * 3600 + minutes * 60 + seconds;\n};\n\nconst filteredResponses = input.filter(response => {\n  if (response.json && response.json.items) {\n    const validItems = response.json.items.filter(item => {\n      const duration = item.contentDetails?.duration;\n      if (!duration) {\n        console.warn(`Missing duration for item: ${JSON.stringify(item)}`);\n        return false; \n      }\n      const durationInSeconds = iso8601ToSeconds(duration);\n\n      return durationInSeconds > 210;\n    });\n\n    response.json.items = validItems;\n\n    return validItems.length > 0; \n  }\n\n  return false;\n});\n\nreturn filteredResponses;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "aadac7e3-8114-4c43-b0bf-d1a7de7c3e0c",
      "name": "create_query",
      "type": "n8n-nodes-base.code",
      "position": [
        2780,
        320
      ],
      "parameters": {
        "jsCode": "const input = $input.all();\n\nlet tableName = \"video_statistics\"; \n\nconst rows = input;\n\nconst formattedRows = rows.map(elements => {\n  const row = elements.json;\n  const formattedRow = {\n    id: row.id,\n    view_count: parseInt(row.viewCount, 10) || 0, \n    like_count: parseInt(row.likeCount, 10) || 0,\n    comment_count: parseInt(row.commentCount, 10) || 0,\n    publish_time: row.publishTime ? new Date(row.publishTime).toISOString() : null,\n    channel_id: $('Loop Over Items').first().json.id || \"unknown\"\n  };\n  return formattedRow;\n});\n\nconst columns = [\"id\", \"view_count\", \"like_count\", \"comment_count\", \"publish_time\", \"channel_id\"];\n\nconst valuePlaceholders = formattedRows.map((_, rowIndex) =>\n  `(${columns.map((_, colIndex) => `$${rowIndex * columns.length + colIndex + 1}`).join(\", \")})`\n).join(\", \");\n\nconst insertQuery = `INSERT INTO ${tableName} (${columns.map(col => `\\\"${col}\\\"`).join(\", \")}) VALUES ${valuePlaceholders};`;\n\nconst parameters = formattedRows.flatMap(row => \n  columns.map(col => row[col])\n);\n\nreturn [\n  {\n    query: insertQuery,\n    parameters: parameters\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "46376f7c-1ce1-4f8a-8392-7281aacfd1c5",
      "name": "structure_data",
      "type": "n8n-nodes-base.code",
      "position": [
        2560,
        320
      ],
      "parameters": {
        "jsCode": "const input = $input.all(); \n\nconst filteredInput = input.filter(item => item.json.viewCount !== null);\n\nconst updatedInput = filteredInput.map(item => {\n    return {\n        ...item,\n        json: {\n            ...item.json,\n            likeCount: item.json.likeCount === null ? \"0\" : item.json.likeCount,\n            commentCount: item.json.commentCount === null ? \"0\" : item.json.commentCount\n        }\n    };\n});\n\nreturn updatedInput;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f66597ef-1324-45e0-b3e8-bc8a588315e4",
      "name": "if_empty",
      "type": "n8n-nodes-base.if",
      "position": [
        2020,
        500
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "dacc5370-f54c-4b90-a2aa-65efff196d3b",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1176b08f-79bb-4f8f-8c83-25a7c2cee9e7",
      "name": "already_populated",
      "type": "n8n-nodes-base.set",
      "position": [
        1200,
        600
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "7579fbc3-d702-4c36-b539-11b7db6c07fa",
              "name": "report",
              "type": "string",
              "value": "={{ $('Loop Over Items').item.json.url }} already populated. Latest was: {{ $('fetch_last_registered').item.json.latest_publish_time }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "265b3062-ee60-4de0-8ee0-3973e653aa7d",
      "name": "map_data",
      "type": "n8n-nodes-base.set",
      "position": [
        2340,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1a76e4e8-cd56-4d55-bcbf-ed24708e1464",
              "name": "id",
              "type": "string",
              "value": "={{ $json.items[0].id }}"
            },
            {
              "id": "0b6d93ba-89fb-4781-809f-6c7bd887f9e2",
              "name": "viewCount",
              "type": "string",
              "value": "={{ $json.items[0].statistics.viewCount }}"
            },
            {
              "id": "9526b059-661a-49a2-81d3-3623d677ddd1",
              "name": "likeCount",
              "type": "string",
              "value": "={{ $json.items[0].statistics.likeCount }}"
            },
            {
              "id": "ca4adf8b-d74f-4dda-a96e-0a2ca3e864e3",
              "name": "commentCount",
              "type": "string",
              "value": "={{ $json.items[0].statistics.commentCount }}"
            },
            {
              "id": "8129ff1c-87c6-489b-83f8-88bdbf426b0f",
              "name": "=publishTime",
              "type": "string",
              "value": "={{ $('get_videos').item.json.snippet.publishedAt }}"
            },
            {
              "id": "16fc88dc-4772-4380-873d-2aa9642b31ac",
              "name": "channelId",
              "type": "string",
              "value": "={{ $('if_is_empty').item.json.snippet.channelId }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "173ac548-89be-4e94-a0e3-e90c45489a0c",
      "name": "sanitize_data",
      "type": "n8n-nodes-base.code",
      "position": [
        300,
        -180
      ],
      "parameters": {
        "jsCode": "const now = new Date();\nconst twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);\n\nconst bestPerformingVideos = [];\n\n$input.all().forEach(channel => {\n  \n  const averageViews = parseInt(channel.json.average_views, 10);\n  \n  channel.json.channel_videos.forEach(video => {\n    const publishDate = new Date(video.publish_time);\n    const isWithinTwoWeeks = publishDate >= twoWeeksAgo && publishDate <= now;\n    const isAboveThreshold = video.view_count >= 2 * averageViews;\n\n  \n    if (isWithinTwoWeeks && isAboveThreshold) {\n      const score = (video.like_count / video.view_count) * 100;\n      bestPerformingVideos.push({\n        id: video.id,\n        videoUrl: `https://www.youtube.com/watch?v=${video.id}`,\n        viewCount: video.view_count,\n        likeCount: video.like_count,\n        score: parseFloat(score.toFixed(2)),\n        commentCount: video.comment_count,\n        channelId: `https://www.youtube.com/channel/${channel.json.channel_id}` \n      });\n    }\n  });\n});\n\nreturn bestPerformingVideos;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "48e729ac-985c-47f5-8895-d2e52581e849",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -260,
        140
      ],
      "parameters": {
        "color": 7,
        "width": 3440,
        "height": 720,
        "content": "### Save Videos To Database"
      },
      "typeVersion": 1
    },
    {
      "id": "11c51123-27f7-4de7-9215-49d89679c2f6",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -260,
        -260
      ],
      "parameters": {
        "color": 6,
        "width": 780,
        "height": 280,
        "content": "### Fetch best performing videos from last 2 weeks"
      },
      "typeVersion": 1
    },
    {
      "id": "7ef37f94-9283-4b51-a127-98c94542429a",
      "name": "see table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        920,
        -180
      ],
      "parameters": {
        "query": "SELECT * FROM video_statistics;",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "e66af542-ea16-4c3c-9f6e-b5401bbd41da",
      "name": "drop table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1200,
        -180
      ],
      "parameters": {
        "query": "DROP TABLE video_statistics;",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "8ee4a252-a795-4931-951f-024d1f0d801a",
  "connections": {
    "Postgres": {
      "main": [
        [
          {
            "node": "sanitize_data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "if_empty": {
      "main": [
        [
          {
            "node": "map_data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "map_data": {
      "main": [
        [
          {
            "node": "structure_data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_videos": {
      "main": [
        [
          {
            "node": "if_is_empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "if_is_empty": {
      "main": [
        [
          {
            "node": "find_video_data1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "already_populated",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create_query": {
      "main": [
        [
          {
            "node": "insert_items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "insert_items": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "remove_shorts": {
      "main": [
        [
          {
            "node": "if_empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "structure_data": {
      "main": [
        [
          {
            "node": "create_query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "fetch_last_registered",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "find_video_data1": {
      "main": [
        [
          {
            "node": "remove_shorts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "already_populated": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fetch_last_registered": {
      "main": [
        [
          {
            "node": "get_videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Test workflow\u2019": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "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

Video explanation

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

More Social Media workflows → · Browse all categories →

Related workflows

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

Social Media

Wait Dropbox. Uses manualTrigger, httpRequest, executeWorkflowTrigger, stickyNote. Event-driven trigger; 20 nodes.

HTTP Request, Execute Workflow Trigger, Dropbox
Social Media

Extracts a clean transcript from a videoId using youtube-transcript.io. AI summaries, sentiment analysis, keyword extraction Internal indexing/SEO Content pipelines (blog/newsletter) Batch transcript

HTTP Request, Execute Workflow Trigger, Stop And Error
Social Media

YouTube Caption Extractor (Your Channel Only) Extracts clean transcripts from YOUR CHANNEL YouTube video captions using YouTube Data API v3.

Execute Workflow Trigger, HTTP Request, Stop And Error
Social Media

Purpose: Uploads videos to Facebook Pages or Groups automatically, either via a Form Trigger or triggered by another workflow.

Form Trigger, HTTP Request, Execute Workflow Trigger
Social Media

Extracts the auto-generated transcript from any YouTube video automatically. 📥 Input: only 🌐 Fetch: GET YouTube page HTML to extract INNERTUBEAPIKEY 🔑 Extract: Parse API key and video ID 🎥 Metadata: G

Execute Workflow Trigger, HTTP Request, XML +1