{
  "name": "Content signal: new YouTube videos that mention your company or competitors, to Slack every Monday",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "836e1a22-532c-5403-92df-7dec12ddaea6",
      "name": "Every Monday at 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "1bd65704-af7e-5d81-838f-8c8c993402d8",
              "name": "channels",
              "value": "@30MPC\n@Saastr\n@salesgravy\n@JoinPavilion\n@WinningByDesign\n@closecrm",
              "type": "string"
            },
            {
              "id": "a0a834a2-a160-507b-a432-efddd4901e2e",
              "name": "terms",
              "value": "Gong\nClay\nHubSpot\nSalesforce\nApollo\nOutreach",
              "type": "string"
            },
            {
              "id": "bb08a391-15dd-524d-afea-18e57582e2de",
              "name": "maxVideosPerChannel",
              "value": 10,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "de1f57ce-16b9-58c8-b51d-f4493a949ba4",
      "name": "Channels and terms",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "resource": "Actors",
        "operation": "Run actor and get dataset",
        "actorId": {
          "__rl": true,
          "value": "t836MIomOqhDM5qUw",
          "mode": "id"
        },
        "customBody": "={{ JSON.stringify({\n  urls: String($json.channels).split('\\n').map((s) => s.trim()).filter(Boolean),\n  maxVideosPerChannel: Number($json.maxVideosPerChannel ?? 10),\n  includeShorts: false,\n  languages: ['en'],\n  includeSegments: false,\n  chunkSize: 120,\n  chunkOverlap: 0,\n  onlyNew: true,\n  maxVideos: 200\n}) }}",
        "maxTotalChargeUsd": 1
      },
      "id": "4e40a685-55ed-5149-8acf-f37110994fd0",
      "name": "Read new videos",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 2,
      "position": [
        480,
        0
      ],
      "credentials": {
        "apifyApi": {
          "id": "REPLACE_WITH_YOUR_APIFY_CREDENTIAL",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// One Slack message a week: every new video from the channels you follow that mentions one of your terms, with how\n// often, links that open the video where each mention is, and a few words around the first one. Terms match as you\n// type them, case included, so 'Clay' finds the company and not the word clay. A week with no mentions sends nothing.\nconst terms = String($('Channels and terms').first().json.terms ?? '').split('\\n').map((s) => s.trim()).filter(Boolean);\nconst escRe = (s) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst rx = terms.map((t) => [t, new RegExp(`(?<![\\\\w])${escRe(t)}(?![\\\\w])`, 'g')]);\nconst esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst clock = (s) => { s = Math.floor(s); const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), x = String(s % 60).padStart(2, '0'); return h ? `${h}:${String(m).padStart(2, '0')}:${x}` : `${m}:${x}`; };\nconst videos = $input.all().map((i) => i.json).filter((v) => v.hasTranscript);\nconst lines = [];\nlet hits = 0;\nfor (const v of videos) {\n  const found = [];\n  for (const [term, re] of rx) {\n    const at = [];\n    let snippet = '';\n    for (const c of v.chunks ?? []) {\n      const m = [...c.text.matchAll(re)];\n      if (!m.length) continue;\n      at.push(...m.map(() => c.start));\n      if (!snippet) {\n        // About six words either side of the first mention.\n        const i = m[0].index;\n        const before = c.text.slice(0, i).split(/\\s+/).slice(-7).join(' ');\n        const after = c.text.slice(i + m[0][0].length).split(/\\s+/).slice(0, 7).join(' ');\n        snippet = `${before}${m[0][0]}${after}`.trim();\n      }\n    }\n    if (at.length) found.push({ term, n: at.length, at: [...new Set(at)].slice(0, 3), snippet });\n  }\n  if (!found.length) continue;\n  hits += 1;\n  lines.push(`*${esc(v.channelName)}* · <${v.url}|${esc(v.title.slice(0, 100))}> · ${String(v.publishedAt).slice(0, 10)}`);\n  for (const f of found) {\n    const links = f.at.map((s) => `<https://www.youtube.com/watch?v=${v.videoId}&t=${Math.floor(s)}s|${clock(s)}>`).join(', ');\n    lines.push(`   ${esc(f.term)} ×${f.n} (${links}) _…${esc(f.snippet)}…_`);\n  }\n}\nif (!hits) return [];\nconst title = `${hits} new video${hits === 1 ? '' : 's'} mention your terms (${videos.length} new video${videos.length === 1 ? '' : 's'} checked)`;\nreturn [{ json: { text: `*${title}*\\n\\n${lines.slice(0, 60).join('\\n')}${lines.length > 60 ? '\\n…and more in the Apify dataset.' : ''}` } }];"
      },
      "id": "ed34e949-38d3-5ab9-a383-7a0c5df3a9df",
      "name": "Find mentions",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        0
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#market-watch",
          "mode": "name"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "8abc3400-3bc1-56dc-b4f0-d3919c4bec4a",
      "name": "Post to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        960,
        0
      ],
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_YOUR_SLACK_CREDENTIAL",
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "content": "## Channels and terms\nOne channel per line (@handle or link) and one term per line. Terms match as typed, case included.",
        "height": 280,
        "width": 660,
        "color": 7
      },
      "id": "81e7898e-fc6a-5c79-afae-a3888be48d68",
      "name": "Companies",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -130
      ]
    },
    {
      "parameters": {
        "content": "## Find and send\nOne message per week: each video that mentions a term, with timestamped links.",
        "height": 290,
        "width": 480,
        "color": 7
      },
      "id": "61d35200-90c1-5774-a883-37315b680e21",
      "name": "Send",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        670,
        -130
      ]
    },
    {
      "parameters": {
        "content": "## Who mentions your company (or a competitor) on YouTube this week\n\nThe workflow from locaihost.org/blog/content-signal-b2b-sales-youtube/ . Every Monday it reads the new videos from the channels you follow and posts one Slack message with every video that mentions one of your terms: how often, links that open the video at each mention, and a few words of context.\n\n### How it works\n1. **Channels and terms** sends your channels to the **youtube-transcripts** Actor on Apify, with `onlyNew` on, so each video is read and paid for once.\n2. The Actor returns each new video's captions in 120-word chunks with start times.\n3. A Code node finds your terms in the chunks (as typed, case included) and builds the message. A week with no mentions sends nothing.\n\n### Setup\n1. Add your Apify API token as an *Apify API* credential.\n2. Pick your Slack credential and channel in **Post to Slack**.\n3. Edit **Channels and terms**: channel handles or links, one per line, and the names to watch for, one per line. It comes with the six B2B sales channels and six sales tools from the article.\n\n### Cost per run\n$3 per 1,000 transcripts; videos without captions are free. The first run reads up to 10 videos per channel (60 here, $0.18). After that it only reads new uploads: these six channels post about 7 regular videos a week between them, about $0.02. The node caps each run at $1.\n\n**Ads count too:** sponsor reads repeat word for word across a channel's episodes, and an ad for one tool often names another. If one term shows up in the same place in every episode, it is an ad.\n\n**Apify node:** this uses the verified Apify node (`@apify/n8n-nodes-apify`). n8n Cloud installs it from the canvas; on self-hosted n8n add it under Settings > Community nodes.\n\nMade by locaihost data (locaihost.org).",
        "height": 900,
        "width": 580
      },
      "id": "52965cb1-0028-5a34-9ef5-1a6774f510e5",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -680,
        -240
      ]
    }
  ],
  "connections": {
    "Every Monday at 08:00": {
      "main": [
        [
          {
            "node": "Channels and terms",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Channels and terms": {
      "main": [
        [
          {
            "node": "Read new videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read new videos": {
      "main": [
        [
          {
            "node": "Find mentions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find mentions": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
