This workflow corresponds to n8n.io template #12161 — we link there as the canonical source.
This workflow follows the Googlegemini → 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 →
{
"id": "kuGonIAboDQKn5h3",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Analyze competitor strategy from YouTube and RSS to Notion & Telegram",
"tags": [],
"nodes": [
{
"id": "871f2e3f-b311-4fb0-8c0f-430365aa3259",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-144,
64
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.3
},
{
"id": "94f99a23-6074-4a45-ac2f-7c345f4b6ea0",
"name": "YouTube (Competitor A): Search Video",
"type": "n8n-nodes-base.youTube",
"position": [
144,
-224
],
"parameters": {
"limit": 3,
"filters": {
"channelId": "input your competitor channel ID here",
"publishedAfter": "={{ new Date(Date.now() - 24*60*60*1000).toISOString() }}"
},
"options": {
"order": "date"
},
"resource": "video"
},
"credentials": {
"youTubeOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "8f8876ba-5810-4a51-af01-d961546fc9bb",
"name": "Apify - Get Dataset Items",
"type": "@apify/n8n-nodes-apify.apify",
"position": [
896,
-128
],
"parameters": {
"resource": "Datasets",
"datasetId": "={{ $json.defaultDatasetId }}",
"authentication": "apifyOAuth2Api"
},
"credentials": {
"apifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ef9f0fb5-710b-4de5-9a52-4d623bda4cd4",
"name": "Code - Normalize Apify Items",
"type": "n8n-nodes-base.code",
"position": [
1120,
-128
],
"parameters": {
"jsCode": "const ytNodeName = \"Merge (YouTube) (Mode Append)\";\n\nconst ytItems = $items(ytNodeName).map(i => i.json);\nconst apifyItems = $input.all();\n\nfunction getVideoIdFromUrl(url = \"\") {\n try {\n const u = new URL(url);\n if (u.hostname.includes(\"youtube.com\")) return u.searchParams.get(\"v\");\n if (u.hostname === \"youtu.be\") return u.pathname.replace(\"/\", \"\") || null;\n } catch (e) {}\n return null;\n}\n\nfunction pickYtVideoId(yt) {\n return (\n yt?.id?.videoId ||\n yt?.videoId ||\n yt?.snippet?.resourceId?.videoId ||\n getVideoIdFromUrl(yt?.url) ||\n null\n );\n}\n\n// Build lookup from YT by videoId\nconst ytById = {};\nfor (const yt of ytItems) {\n const vid = pickYtVideoId(yt);\n if (vid) ytById[vid] = yt;\n}\n\n// Normalize Apify items, match YT by videoId (not by idx)\nreturn apifyItems.map(({ json: ap }) => {\n const apVideoId =\n ap?.id ||\n getVideoIdFromUrl(ap?.url) ||\n null;\n\n const yt = apVideoId ? (ytById[apVideoId] || {}) : {};\n\n const transcriptOnly = (ap?.transcript_only_text || \"\").toString().trim();\n const fallbackDesc = (yt?.snippet?.description || yt?.description || \"\").toString().trim();\n const content = (transcriptOnly || fallbackDesc).slice(0, 12000);\n\n const title = ap?.title || yt?.snippet?.title || yt?.title || \"\";\n const url = ap?.url || (apVideoId ? `https://www.youtube.com/watch?v=${apVideoId}` : \"\");\n const publishedAt = yt?.snippet?.publishedAt || yt?.publishedAt || \"\";\n\n return {\n json: {\n type: \"YT_TRANSCRIPT\",\n videoId: apVideoId,\n title,\n url,\n publishedAt,\n content,\n source: \"apify_best_youtube_transcripts_scraper\",\n hasTranscript: Boolean(transcriptOnly),\n // debug fields (optional, can be deleted)\n _match: {\n matchedBy: \"videoId\",\n ytFound: Boolean(apVideoId && ytById[apVideoId]),\n }\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "a34e7448-f0ed-4a78-b649-7d9bea1e810b",
"name": "Merge (YouTube) (Mode Append)",
"type": "n8n-nodes-base.merge",
"position": [
448,
-128
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "c9ff9afd-a5de-4154-bf47-dcf2256d69d3",
"name": "YouTube (Competitor B): Search Video",
"type": "n8n-nodes-base.youTube",
"position": [
144,
-32
],
"parameters": {
"limit": 3,
"filters": {
"channelId": "input your competitor channel ID here",
"publishedAfter": "={{ new Date(Date.now() - 24*60*60*1000).toISOString() }}"
},
"options": {
"order": "date"
},
"resource": "video"
},
"credentials": {
"youTubeOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "74a46e76-53b4-416a-9023-6a1e0bb7849a",
"name": "Apify - Run an Actor",
"type": "@apify/n8n-nodes-apify.apify",
"position": [
672,
-128
],
"parameters": {
"actorId": {
"__rl": true,
"mode": "list",
"value": "L57jETyu9qT6J7bs5",
"cachedResultUrl": "https://console.apify.com/actors/L57jETyu9qT6J7bs5/input",
"cachedResultName": "Best Youtube Transcripts Scraper (scrape-creators/best-youtube-transcripts-scraper)"
},
"customBody": "={\n \"videoUrls\": [\"{{'https://www.youtube.com/watch?v=' + $json.id.videoId}}\"]\n}",
"authentication": "apifyOAuth2Api"
},
"credentials": {
"apifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "9dc7cbf4-32a5-495e-9136-ed2cc967931f",
"name": "RSS Feed (Competitor A): TechCrunch",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
144,
160
],
"parameters": {
"url": "input your competitor RSS here",
"options": {
"ignoreSSL": true
}
},
"typeVersion": 1.2
},
{
"id": "c7cb3520-3d62-4676-b0cd-31115a13b4f7",
"name": "RSS Feed (Competitor B): n8n Blog",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
144,
352
],
"parameters": {
"url": "input your competitor RSS here",
"options": {
"ignoreSSL": true
}
},
"typeVersion": 1.2
},
{
"id": "8227ba9d-9f2f-48df-a3a7-72d64a13db8a",
"name": "Merge (RSS): Mode Append",
"type": "n8n-nodes-base.merge",
"position": [
448,
256
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "a4ea4a59-d8aa-4fc9-b80b-6dae596ddc93",
"name": "Merge (All Data): Mode Append",
"type": "n8n-nodes-base.merge",
"position": [
1344,
64
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "b1936f26-0828-4bb5-b88a-6c1d328a7d99",
"name": "Code (Data Prep)",
"type": "n8n-nodes-base.code",
"position": [
1568,
64
],
"parameters": {
"jsCode": "function stripHtml(html = \"\") {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \"\")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n .replace(/<\\/?[^>]+(>|$)/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction clip(text = \"\", max = 6000) {\n const t = (text || \"\").toString().trim();\n return t.length > max ? t.slice(0, max) + \"\u2026\" : t;\n}\n\nfunction toMs(x) {\n const ms = Date.parse(x || \"\");\n return Number.isNaN(ms) ? 0 : ms;\n}\n\n// FIX: The Node runtime code is missing this.getWorkflowStaticData()\nconst staticData =\n (typeof getWorkflowStaticData === \"function\" && getWorkflowStaticData(\"global\")) ||\n (typeof $getWorkflowStaticData === \"function\" && $getWorkflowStaticData(\"global\"));\n\nif (!staticData) throw new Error(\"Static Data API not available in this Code node runtime\");\n\nstaticData.seen = staticData.seen || {};\n\nconst now = new Date();\nconst nowIso = now.toISOString();\nconst SEEN_TTL_DAYS = 7;\nconst TTL_MS = SEEN_TTL_DAYS * 24 * 60 * 60 * 1000;\n\n// ===== Token control =====\nconst MAX_ITEMS_TOTAL = 8; // Gemini limits purchases to a maximum of 8 items per day.\nconst MAX_YT_ITEMS = 2; // Prioritize the two most recent videos\nconst RSS_HOT_POOL = 10; // Top 10 hottest RSS posts\nconst MAX_CONTENT_RSS = 1400; // A sharp cut to reduce the token price\nconst MAX_CONTENT_YT = 1800; // transcript also cuts strongly\n\nfunction isRss(json) {\n return Boolean(\n json.link &&\n (json.pubDate ||\n json.isoDate ||\n json.content ||\n json.contentSnippet ||\n json.description ||\n json[\"content:encoded\"])\n );\n}\n\nfunction competitorTagFromUrl(url = \"\") {\n if (url.includes(\"techcrunch.com\")) return \"techcrunch\";\n if (url.includes(\"n8n.io\")) return \"n8n\";\n return \"unknown\";\n}\n\n// \u201cHot score\u201d heuristic for RSS: recency + keyword + source boost\nfunction rssHotScore(item) {\n const ageMs = now.getTime() - toMs(item.publishedAt);\n const ageHours = ageMs > 0 ? ageMs / (1000 * 60 * 60) : 9999;\n\n // Recency score: 0..1 (highest new)\n const recency = Math.max(0, 1 - ageHours / 48); // trong 48h \u0111\u1ea7u l\u00e0 quan tr\u1ecdng\n\n const title = (item.title || \"\").toLowerCase();\n const content = (item.content || \"\").toLowerCase();\n\n // Keyword weight (depending on your domain, you can adjust it further)\n const kw = [\n { re: /\\b(ai|agent|llm|genai|rag)\\b/i, w: 0.30 },\n { re: /\\b(robot|humanoid|automation)\\b/i, w: 0.25 },\n { re: /\\b(hack|breach|cyber|security|ransom)\\b/i, w: 0.25 },\n { re: /\\b(acquire|acquisition|merge|funding|raise|valuation|ipo)\\b/i, w: 0.20 },\n { re: /\\b(nvidia|google|microsoft|meta|apple|openai|anthropic)\\b/i, w: 0.15 },\n { re: /\\b(regulation|ban|lawsuit|court|policy)\\b/i, w: 0.15 },\n ];\n\n let kwScore = 0;\n for (const k of kw) {\n if (k.re.test(title) || k.re.test(content)) kwScore += k.w;\n }\n\n // Source boost: TechCrunch tends to be more \"market-moving\".\n const sourceBoost = item.competitorTag === \"techcrunch\" ? 0.10 : 0.03;\n\n // Penalty for the song \"stale / evergreen\" in the style of \"how to track santa ... 2022\"\n const stalePenalty = /\\b(2020|2021|2022|2023)\\b/i.test(title) ? 0.25 : 0;\n\n return recency + kwScore + sourceBoost - stalePenalty;\n}\n\nconst inputItems = $input.all();\n\n// --- Normalize each item into a common schema ---\nconst normalized = inputItems.map(({ json }) => {\n if (!isRss(json)) {\n const competitorTag = json.competitorTag || \"unknown\";\n const videoId = json.videoId || json.id;\n const url = json.url || (videoId ? `https://www.youtube.com/watch?v=${videoId}` : \"\");\n\n return {\n id: `yt:${videoId || url}`,\n competitorTag,\n sourceType: \"youtube\",\n title: json.title || \"\",\n url,\n publishedAt: json.publishedAt || \"\",\n content: clip(json.content || \"\", MAX_CONTENT_YT),\n };\n }\n\n const title = json.title || \"\";\n const url = json.link || \"\";\n const publishedAt = json.isoDate || json.pubDate || \"\";\n const rawContent =\n json[\"content:encoded\"] ||\n json.content ||\n json.contentSnippet ||\n json.description ||\n \"\";\n\n const competitorTag = competitorTagFromUrl(url);\n\n return {\n id: `rss:${url || title}`,\n competitorTag,\n sourceType: \"rss\",\n title,\n url,\n publishedAt,\n content: clip(stripHtml(rawContent), MAX_CONTENT_RSS),\n };\n});\n\n// --- Dedup using staticData.seen with TTL ---\nconst fresh = [];\nfor (const item of normalized) {\n const key = item.id;\n const seenAt = staticData.seen[key];\n\n if (seenAt) {\n const seenMs = Date.parse(seenAt);\n if (!Number.isNaN(seenMs) && Date.now() - seenMs < TTL_MS) continue;\n }\n\n staticData.seen[key] = nowIso;\n fresh.push(item);\n}\n\n// --- Prune old keys ---\nfor (const [key, ts] of Object.entries(staticData.seen)) {\n const ms = Date.parse(ts);\n if (Number.isNaN(ms) || Date.now() - ms > TTL_MS) delete staticData.seen[key];\n}\n\nif (fresh.length === 0) {\n return [{\n json: {\n competitorContext: `DATE=${nowIso.slice(0, 10)} | ITEMS=0\\nTASK=No new items in last 24h.`,\n sources: [],\n stats: { input: inputItems.length, normalized: normalized.length, fresh: 0 },\n competitorTag: \"mixed\",\n date: nowIso.slice(0, 10),\n }\n }];\n}\n\n// ===== Reduce items to control token size =====\n// Sort YT newest first\nconst yt = fresh\n .filter(x => x.sourceType === \"youtube\")\n .sort((a, b) => toMs(b.publishedAt) - toMs(a.publishedAt))\n .slice(0, MAX_YT_ITEMS);\n\n// RSS: calculates the hot score -> gets the top 10 hottest topics.\nconst rssAll = fresh.filter(x => x.sourceType === \"rss\");\nconst rssHotTop10 = rssAll\n .map(x => ({ ...x, _hotScore: rssHotScore(x) }))\n .sort((a, b) => (b._hotScore || 0) - (a._hotScore || 0))\n .slice(0, RSS_HOT_POOL)\n .map(({ _hotScore, ...rest }) => rest);\n\n// Final list: prioritize YouTube (max 2), then fill in the top 10 hottest RSS feeds until you have 8.\nconst final = [...yt];\nfor (const r of rssHotTop10) {\n if (final.length >= MAX_ITEMS_TOTAL) break;\n final.push(r);\n}\n\n// If there is still a shortage (low RSS), fill from the remaining (YT/RSS) according to recency.\nif (final.length < MAX_ITEMS_TOTAL) {\n const finalIds = new Set(final.map(x => x.id));\n const remainder = fresh\n .filter(x => !finalIds.has(x.id))\n .sort((a, b) => toMs(b.publishedAt) - toMs(a.publishedAt));\n\n for (const it of remainder) {\n if (final.length >= MAX_ITEMS_TOTAL) break;\n final.push(it);\n }\n}\n\n// --- Build compact context (decrease token) ---\nconst lines = [];\nlines.push(`DATE=${nowIso.slice(0, 10)} | ITEMS=${final.length}`);\nlines.push(`TASK=insight>summary; detect positioning; propose 1 counter-tactic.`);\nlines.push(\"\");\n\nfinal.forEach((it, idx) => {\n lines.push(`--#${idx + 1} ${it.sourceType.toUpperCase()} ${it.competitorTag}`);\n if (it.publishedAt) lines.push(`P=${it.publishedAt}`);\n lines.push(`T=${it.title}`);\n lines.push(`U=${it.url}`);\n lines.push(`C=${(it.content || \"\").replace(/\\s+/g, \" \").trim()}`);\n lines.push(\"\");\n});\n\nconst competitorTags = Array.from(new Set(final.map(x => x.competitorTag))).filter(Boolean);\nconst pageCompetitorTag = competitorTags.length === 1 ? competitorTags[0] : \"mixed\";\n\nreturn [{\n json: {\n competitorContext: lines.join(\"\\n\"),\n sources: final,\n stats: { input: inputItems.length, normalized: normalized.length, fresh: final.length },\n competitorTag: pageCompetitorTag,\n date: nowIso.slice(0, 10),\n // debug optional:\n hotPool: {\n rssCandidates: rssAll.length,\n rssHotTop10: rssHotTop10.length,\n ytPicked: yt.length,\n final: final.length,\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "dd74498e-fecc-4d59-9002-457c04bf37b1",
"name": "Google Gemini - Generate",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1792,
64
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {
"temperature": 0.3,
"systemMessage": "You are a Competitor Intelligence Analyst."
},
"messages": {
"values": [
{
"content": "=Read this content (Blogs & Video Transcripts):\n\n{{ $json.competitorContext }}\n\nDeep Analysis:\n1) Core Message: What is the ONE main thing they want the audience to believe today?\n2) Hidden Strategy: Detect keywords or angles they are pushing (e.g., 'Cheap', 'Enterprise', 'AI-first').\n3) Counter-Tactic: Suggest 1 specific content angle for us to beat them.\n\nReturn STRICT JSON only (no markdown fences), schema:\n{\n \"report_title\": \"Daily Intel: {{$node[\"Code (Data Prep)\"].json.date}}\",\n \"summary\": \"Short summary\",\n \"strategy_markdown\": \"## Strategy\\n* Point 1\\n* Point 2\",\n \"telegram_html\": \"<b>Title</b>...\"\n}\n\nConstraints:\n- telegram_html < 4000 chars. NO <br>, NO <ul>, NO <li>; use \\n and \u2022 bullets only; only tags allowed: <b>, <i>, <u>, <s>, <code>, <pre>.\n- Include source links at the bottom of telegram_html (use URLs from the context). No <br>, <ul>, <li>; use newline + bullet.\n- Focus on insights, not just summary"
}
]
},
"simplify": false,
"jsonOutput": true
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "93d83705-5488-4d28-af76-9920c4eee683",
"name": "Telegram: Send Message",
"type": "n8n-nodes-base.telegram",
"position": [
2368,
-32
],
"parameters": {
"text": "={{ $json.telegram_html }}",
"chatId": "input your chat ID here",
"additionalFields": {
"parse_mode": "HTML"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "2e89d05d-3791-4464-81a9-288d4d806ce0",
"name": "Code (Build Notion Blocks)",
"type": "n8n-nodes-base.code",
"position": [
2592,
160
],
"parameters": {
"jsCode": "function normalizeInline(s) {\n let t = String(s ?? \"\");\n\n // Convert basic HTML bold/italic to markdown\n t = t.replace(/<\\/?b>/gi, \"**\");\n t = t.replace(/<\\/?strong>/gi, \"**\");\n t = t.replace(/<\\/?i>/gi, \"*\");\n t = t.replace(/<\\/?em>/gi, \"*\");\n t = t.replace(/<br\\s*\\/?>/gi, \"\\n\");\n\n // Strip any remaining html tags\n t = t.replace(/<[^>]+>/g, \"\");\n\n return t;\n}\n\nfunction pushText(arr, content, annotations = {}, link = null) {\n if (!content) return;\n arr.push({\n type: \"text\",\n text: {\n content,\n ...(link ? { link: { url: link } } : {}),\n },\n annotations: {\n bold: !!annotations.bold,\n italic: !!annotations.italic,\n strikethrough: !!annotations.strikethrough,\n underline: !!annotations.underline,\n code: !!annotations.code,\n color: \"default\",\n },\n });\n}\n\n// Inline markdown parser: **bold**, *italic*, `code`, URLs\nfunction inlineToRichText(input) {\n const s0 = normalizeInline(input);\n const out = [];\n\n const tokenRegex =\n /(`[^`]+`)|(\\*\\*[^*]+\\*\\*)|(\\*[^*]+\\*)|(https?:\\/\\/[^\\s)>\"']+)/g;\n\n let last = 0;\n let m;\n while ((m = tokenRegex.exec(s0)) !== null) {\n if (m.index > last) pushText(out, s0.slice(last, m.index));\n\n const tok = m[0];\n\n if (tok.startsWith(\"`\") && tok.endsWith(\"`\")) {\n pushText(out, tok.slice(1, -1), { code: true });\n } else if (tok.startsWith(\"**\") && tok.endsWith(\"**\")) {\n pushText(out, tok.slice(2, -2), { bold: true });\n } else if (tok.startsWith(\"*\") && tok.endsWith(\"*\")) {\n pushText(out, tok.slice(1, -1), { italic: true });\n } else if (tok.startsWith(\"http\")) {\n pushText(out, tok, {}, tok);\n } else {\n pushText(out, tok);\n }\n\n last = tokenRegex.lastIndex;\n }\n\n if (last < s0.length) pushText(out, s0.slice(last));\n\n return out.filter(x => x?.text?.content?.length);\n}\n\nfunction heading2(text) {\n return {\n object: \"block\",\n type: \"heading_2\",\n heading_2: { rich_text: inlineToRichText(text) },\n };\n}\n\nfunction divider() {\n return { object: \"block\", type: \"divider\", divider: {} };\n}\n\nfunction paragraph(text) {\n const t = String(text ?? \"\").trim();\n if (!t) return null;\n return {\n object: \"block\",\n type: \"paragraph\",\n paragraph: { rich_text: inlineToRichText(t) },\n };\n}\n\nfunction bullet(text) {\n const t = String(text ?? \"\").trim();\n if (!t) return null;\n return {\n object: \"block\",\n type: \"bulleted_list_item\",\n bulleted_list_item: { rich_text: inlineToRichText(t) },\n };\n}\n\nfunction toggle(title, children = []) {\n return {\n object: \"block\",\n type: \"toggle\",\n toggle: {\n rich_text: inlineToRichText(title),\n children,\n },\n };\n}\n\nfunction extractUrls(text) {\n const s = String(text ?? \"\");\n const urls = s.match(/https?:\\/\\/[^\\s)>\"']+/g) || [];\n const seen = new Set();\n const out = [];\n for (const u of urls) {\n const clean = u.replace(/[.,;]+$/g, \"\");\n if (!seen.has(clean)) {\n seen.add(clean);\n out.push(clean);\n }\n }\n return out;\n}\n\nfunction mdToBlocks(md) {\n const blocks = [];\n const lines = String(md ?? \"\").split(/\\r?\\n/);\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line) continue;\n\n if (line.startsWith(\"## \")) {\n blocks.push(heading2(line.replace(/^##\\s+/, \"\")));\n continue;\n }\n\n if (line.startsWith(\"* \") || line.startsWith(\"- \")) {\n blocks.push(bullet(line.replace(/^(\\*|\\-)\\s+/, \"\")));\n continue;\n }\n\n blocks.push(paragraph(line));\n }\n\n return blocks.filter(Boolean);\n}\n\n// ===== Input =====\nconst rp = $node[\"Code - Robust Parser (Gemini JSON)\"].json;\nconst summary = rp.summary;\nconst strategyMd = rp.strategy_markdown;\nconst telegramHtml = rp.telegram_html;\n\n// ===== Build page content (WITH ALL 3 EMOJI OPTIONS) =====\nconst children = [];\n\n// Option #1: Emoji headings\nchildren.push(heading2(\"\ud83e\udde0 Summary\"));\nchildren.push(paragraph(summary || \"(no summary)\"));\nchildren.push(divider());\n\n// Strategy section\nconst strategyBlocks = mdToBlocks(strategyMd);\nchildren.push(heading2(\"\ud83e\udde9 Strategy\"));\n\n// Option #3: Emoji in toggle title\nchildren.push(toggle(\"\ud83d\udc47 Open strategy details\", strategyBlocks));\nchildren.push(divider());\n\n// Sources\nconst urls = extractUrls(telegramHtml);\nif (urls.length) {\n children.push(heading2(\"\ud83d\udd17 Sources\"));\n\n // Option #2: Emoji prefix for source bullets\n for (const u of urls.slice(0, 30)) children.push(bullet(`\ud83d\udd17 ${u}`));\n}\n\nreturn [{ json: { children } }];"
},
"typeVersion": 2
},
{
"id": "37f4bff9-7a2c-4a39-a9cb-0b84e33dc403",
"name": "Notion - Create Database Page",
"type": "n8n-nodes-base.notion",
"position": [
2368,
160
],
"parameters": {
"simple": false,
"options": {
"iconType": "emoji"
},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "id",
"value": "INPUT_DATABASE_ID_FROM_URL"
},
"propertiesUi": {
"propertyValues": [
{
"key": "date|date",
"date": "={{ $json.meta.date }}",
"includeTime": false
},
{
"key": "Name|title",
"title": "={{ $json.report_title }}"
}
]
}
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "ede0979a-12e6-431f-a7ea-52f7f8553c92",
"name": "HTTP Request (Notion Append Children)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2816,
160
],
"parameters": {
"url": "={{`https://api.notion.com/v1/blocks/${$node[\"Notion - Create Database Page\"].json.id}/children`}}",
"method": "PATCH",
"options": {},
"jsonBody": "={{ JSON.stringify({ children: $node[\"Code (Build Notion Blocks)\"].json.children }) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "notionApi"
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.3
},
{
"id": "b7cdd909-272a-4c99-8db2-f4906eaba205",
"name": "Code - Robust Parser (Gemini JSON)",
"type": "n8n-nodes-base.code",
"position": [
2144,
64
],
"parameters": {
"jsCode": "function extractJsonObject(text) {\n const t = (text ?? \"\").toString().trim();\n const s = t.indexOf(\"{\");\n const e = t.lastIndexOf(\"}\");\n if (s === -1 || e === -1 || e <= s) {\n throw new Error(\"No JSON object found in Gemini output\");\n }\n return t.slice(s, e + 1);\n}\n\nfunction sanitizeTelegramHtml(html) {\n let s = (html ?? \"\").toString();\n\n // Telegram HTML DOES NOT support <br>, <ul>, <li>\n s = s.replace(/<br\\s*\\/?>/gi, \"\\n\");\n s = s.replace(/<\\/?ul[^>]*>/gi, \"\\n\");\n s = s.replace(/<li[^>]*>\\s*/gi, \"\u2022 \");\n s = s.replace(/<\\/li>\\s*/gi, \"\\n\");\n\n // Convert links <a href=\"url\">text</a> => \"text: url\"\n // (Telegram supports <a>, but Gemini often truncates mid-tag => easiest is to remove <a> entirely)\n s = s.replace(\n /<a\\s+[^>]*href\\s*=\\s*\"(.*?)\"[^>]*>(.*?)<\\/a>/gi,\n (_, url, text) => {\n const t = (text ?? \"\").replace(/<[^>]+>/g, \"\").trim();\n return t ? `${t}: ${url}` : url;\n }\n );\n\n // Remove ALL tags except Telegram-safe subset\n // Allowed: b/strong, i/em, u/ins, s/strike/del, code, pre\n s = s.replace(\n /<(?!\\/?(b|strong|i|em|u|ins|s|strike|del|code|pre)\\b)[^>]*>/gi,\n \"\"\n );\n\n // Normalize newlines\n s = s.replace(/\\r\\n/g, \"\\n\");\n s = s.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n // If Gemini left unbalanced tags, safest is to strip ALL remaining tags\n // (Telegram will fail on unbalanced tags)\n const openB = (s.match(/<b>/gi) || []).length;\n const closeB = (s.match(/<\\/b>/gi) || []).length;\n const openI = (s.match(/<i>/gi) || []).length;\n const closeI = (s.match(/<\\/i>/gi) || []).length;\n const openU = (s.match(/<u>/gi) || []).length;\n const closeU = (s.match(/<\\/u>/gi) || []).length;\n\n if (openB !== closeB || openI !== closeI || openU !== closeU) {\n s = s.replace(/<[^>]+>/g, \"\");\n }\n\n return s;\n}\n\nfunction safeTruncate(text, limit = 3800) {\n if (!text) return text;\n if (text.length <= limit) return text;\n\n // Cut near a newline to avoid ugly cut\n const cut = text.slice(0, limit - 30);\n const lastNl = cut.lastIndexOf(\"\\n\");\n const out = (lastNl > 200 ? cut.slice(0, lastNl) : cut).trim();\n\n return out + \"\\n\\n(\u2026truncated)\";\n}\n\n// ---- Read Gemini node output (robust paths) ----\nconst j = $json;\n\nconst candidatesText =\n j?.candidates?.[0]?.content?.parts?.map(p => p.text).filter(Boolean).join(\"\\n\");\n\nconst possible =\n candidatesText ||\n j?.text ||\n j?.output ||\n j?.response ||\n j?.data ||\n j?.result ||\n j?.content ||\n j?.message ||\n (typeof j === \"string\" ? j : \"\");\n\nconst raw = extractJsonObject(possible);\n\nlet parsed;\ntry {\n parsed = JSON.parse(raw);\n} catch (err) {\n throw new Error(`JSON.parse failed: ${err.message}\\nRAW_JSON:\\n${raw}`);\n}\n\n// ---- Sanitize + truncate for Telegram HTML parse mode ----\nparsed.telegram_html = safeTruncate(sanitizeTelegramHtml(parsed.telegram_html), 4000);\n\n// ---- Add meta (keep it the same) ----\nreturn [{\n json: {\n ...parsed,\n meta: {\n generatedAt: new Date().toISOString(),\n competitorTag: $node[\"Code (Data Prep)\"].json.competitorTag,\n date: $node[\"Code (Data Prep)\"].json.date,\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "91b1f5c5-ebeb-4d2b-8406-045abb4057e7",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-720,
-400
],
"parameters": {
"width": 480,
"height": 944,
"content": "## AI-Powered Intelligence Watchdog: YouTube & RSS to Telegram & Notion\n\nTransform how you track market rivals. Instead of drowning in noise, this workflow acts as your dedicated **AI Strategy Analyst**. It autonomously monitors competitor communication channels, decodes their underlying tactics, and delivers a high-level executive briefing every morning.\n\n### Key Capabilities\n* **Deep Video Analysis:** Unlike simple trackers, this workflow uses **Apify** to fetch full **video transcripts**, allowing AI to \"watch\" and analyze competitor videos in depth, not just read titles.\n* **Strategic Intelligence:** Google Gemini doesn't just summarize; it identifies **Core Messages**, detects shifts in **Tone/Positioning**, and suggests actionable **Counter-Tactics**.\n* **Dual Delivery:** Sends an instant, actionable HTML summary to **Telegram** for quick reading, and simultaneously archives a beautifully formatted report (using Notion Blocks) into **Notion** for long-term research.\n\n### Setup Instructions\n1. **Credentials:** You need API keys for `YouTube Data API`, `Apify`, `Google Gemini`, `Telegram`, and `Notion`.\n2. **Apify Setup:** Create a free account at Apify.com. This workflow uses the `youtube-transcript-scraper` actor (efficient & low cost).\n3. **Notion Setup:** Create a Database with two properties: `Name` (Title) and `date` (Date). Copy the Database ID from the URL into the **Notion** node.\n4. **Configuration:**\n * **YouTube:** Update `Channel ID` in the YouTube nodes to target your specific competitors.\n * **RSS:** Update feed URLs in the RSS nodes.\n\n### Customization Tips\n* **Adjust the \"Lens\":** Open the **Gemini** node and modify the System Instruction to change the analysis focus (e.g., focus on \"Pricing Changes\" or \"New Product Features\").\n* **Control Token Usage:** The `Code - Data Prep` node has a config section to limit the number of items processed daily (default is optimized for free tier limits)."
},
"typeVersion": 1
},
{
"id": "ece84946-03c8-4155-9688-555ea666d1e9",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-176,
-368
],
"parameters": {
"color": 7,
"width": 480,
"height": 880,
"content": "## 1. Data Sources (User Config)\n* **Setup:** Enter `Channel ID` for YouTube & `URL` for RSS nodes.\n* **Scale Up:** To track more competitors, duplicate these nodes and connect them to the **Merge** nodes.\n* **Note:** Remember to increase the **\"Number of Inputs\"** in the Merge nodes if you add more sources."
},
"typeVersion": 1
},
{
"id": "ef79b0ad-978d-4e77-9e59-8a6775fb8369",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
384,
-368
],
"parameters": {
"color": 7,
"width": 864,
"height": 800,
"content": "## 2. Ingestion & Transcripts\n* **Aggregation:** Combines data from all sources.\n* **Deep Dive:** Triggers **Apify** to fetch full transcripts for all new YouTube videos found.\n* **Normalization:** Standardizes Blog & Video data into a unified format."
},
"typeVersion": 1
},
{
"id": "5c695a4c-fab5-4c52-97ef-4221f8d11a9c",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1312,
-160
],
"parameters": {
"color": 7,
"width": 736,
"height": 416,
"content": "## 3. AI Strategy Core\n* **Data Prep:** Deduplicates items (Static Data) & prepares context.\n* **Gemini Analyst:** Reads content to extract \"Strategy\", \"Tone\", and \"Counter-Tactics\"."
},
"typeVersion": 1
},
{
"id": "c78df5b9-4568-481d-92ac-1af11cd67747",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2112,
-160
],
"parameters": {
"color": 7,
"width": 880,
"height": 512,
"content": "## 4. Professional Reporting\n* **Telegram:** Sends an executive HTML summary.\n* **Notion:** Archives the full report with rich formatting (Headings, Toggles)."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "94d00ca3-e315-4292-a186-69e75d59e37e",
"connections": {
"Code (Data Prep)": {
"main": [
[
{
"node": "Google Gemini - Generate",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "YouTube (Competitor A): Search Video",
"type": "main",
"index": 0
},
{
"node": "YouTube (Competitor B): Search Video",
"type": "main",
"index": 0
},
{
"node": "RSS Feed (Competitor A): TechCrunch",
"type": "main",
"index": 0
},
{
"node": "RSS Feed (Competitor B): n8n Blog",
"type": "main",
"index": 0
}
]
]
},
"Apify - Run an Actor": {
"main": [
[
{
"node": "Apify - Get Dataset Items",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini - Generate": {
"main": [
[
{
"node": "Code - Robust Parser (Gemini JSON)",
"type": "main",
"index": 0
}
]
]
},
"Merge (RSS): Mode Append": {
"main": [
[
{
"node": "Merge (All Data): Mode Append",
"type": "main",
"index": 1
}
]
]
},
"Apify - Get Dataset Items": {
"main": [
[
{
"node": "Code - Normalize Apify Items",
"type": "main",
"index": 0
}
]
]
},
"Code (Build Notion Blocks)": {
"main": [
[
{
"node": "HTTP Request (Notion Append Children)",
"type": "main",
"index": 0
}
]
]
},
"Code - Normalize Apify Items": {
"main": [
[
{
"node": "Merge (All Data): Mode Append",
"type": "main",
"index": 0
}
]
]
},
"Merge (All Data): Mode Append": {
"main": [
[
{
"node": "Code (Data Prep)",
"type": "main",
"index": 0
}
]
]
},
"Merge (YouTube) (Mode Append)": {
"main": [
[
{
"node": "Apify - Run an Actor",
"type": "main",
"index": 0
}
]
]
},
"Notion - Create Database Page": {
"main": [
[
{
"node": "Code (Build Notion Blocks)",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed (Competitor B): n8n Blog": {
"main": [
[
{
"node": "Merge (RSS): Mode Append",
"type": "main",
"index": 1
}
]
]
},
"Code - Robust Parser (Gemini JSON)": {
"main": [
[
{
"node": "Notion - Create Database Page",
"type": "main",
"index": 0
},
{
"node": "Telegram: Send Message",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed (Competitor A): TechCrunch": {
"main": [
[
{
"node": "Merge (RSS): Mode Append",
"type": "main",
"index": 0
}
]
]
},
"YouTube (Competitor A): Search Video": {
"main": [
[
{
"node": "Merge (YouTube) (Mode Append)",
"type": "main",
"index": 0
}
]
]
},
"YouTube (Competitor B): Search Video": {
"main": [
[
{
"node": "Merge (YouTube) (Mode Append)",
"type": "main",
"index": 1
}
]
]
}
}
}
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.
apifyOAuth2ApigooglePalmApinotionApitelegramApiyouTubeOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Turn your n8n workflow into an automated competitive intelligence unit. This template monitors competitor activities across blog feeds and YouTube channels to detect strategic shifts. Instead of simply aggregating links, it uses Apify to fetch full video transcripts and Google…
Source: https://n8n.io/workflows/12161/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Turn any Amazon India product URL into a fully-edited 10-second lifestyle video and auto-publish it to Instagram, Facebook, X (Twitter), LinkedIn, YouTube, and Threads — with platform-optimized captio
Perfect for social media managers, content creators, and personal brands who want to stay relevant on X without manually tracking trends or writing posts every day. The workflow runs 3 times daily by
Manually checking websites for updates or competitor changes can be tedious. This workflow automates the process by scraping target pages, capturing screenshots, and analyzing content changes using Fi
Schedule Trigger runs every 6 hours (customizable) Apify Scraper fetches Upwork jobs matching your criteria Deduplication filters out jobs you've already seen AI Scoring (GPT-4) evaluates fit, client
Automate price monitoring for e-commerce competitors—ideal for retailers, analysts, and pricing teams. Scrapes competitor sites, extracts pricing/stock data via AI, detects changes, and sends instant