This workflow corresponds to n8n.io template #17350 — we link there as the canonical source.
This workflow follows the Agent → 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": "q5qGINfIHOWWBr1V",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Google Trends to slack",
"tags": [],
"nodes": [
{
"id": "7986d495-fd51-44f1-95db-f9f5c67c4662",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
448,
-256
],
"parameters": {
"width": 480,
"height": 784,
"content": "## Automated Google Trends to slack\n\n### How it works\n\nThe workflow runs on a defined schedule and fetches data from Google Trends.\nIt processes and filters the fetched items based on specific conditions.\nFiltered items are aggregated and passed to a Gemini AI Agent for analysis.\nThe AI structures the analysis, which is then formatted by a code node.\nFinally, the formatted analysis is sent as a message to a designated Slack channel.\n\n### Setup steps\n\n- [ ] Configure the Schedule Trigger for your desired interval.\n- [ ] Authenticate the Google Gemini Chat Model with your API credentials.\n- [ ] Set up your Slack credentials and specify the target channel in the Send a message node.\n\n### Customization\n\nYou can customize the prompt in the AI Agent to change how the trends are analyzed, or adjust the filter criteria in the If node."
},
"typeVersion": 1
},
{
"id": "4090da5b-8c37-486f-8393-6dc9fc09221e",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
-256
],
"parameters": {
"color": 7,
"width": 416,
"height": 304,
"content": "## Trigger and fetch data\n\nTriggers on a schedule and fetches Google Trends data"
},
"typeVersion": 1
},
{
"id": "76a327cf-002c-45ef-a02b-e5cb4eb93746",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1456,
-240
],
"parameters": {
"color": 7,
"width": 864,
"height": 272,
"content": "## Filter and aggregate trends\n\nParses the trend data, filters out irrelevant items, and aggregates the results"
},
"typeVersion": 1
},
{
"id": "f4cb01b3-8286-4d12-93de-4ec6120fc0b0",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2352,
-256
],
"parameters": {
"color": 7,
"width": 352,
"height": 512,
"content": "## Analyze with AI\n\nPasses aggregated trends to an AI agent to analyze and structure the output"
},
"typeVersion": 1
},
{
"id": "3dc07c8e-26fd-414c-b976-032c37b8d2c9",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2736,
-256
],
"parameters": {
"color": 7,
"width": 416,
"height": 304,
"content": "## Send Slack notification\n\nFormats the AI response and sends a notification to a Slack channel"
},
"typeVersion": 1
},
{
"id": "d35893b1-37b5-4749-9f0d-d4575a790eca",
"name": "Fetch Google Trends Data",
"type": "n8n-nodes-base.httpRequest",
"position": [
1280,
-128
],
"parameters": {
"url": "https://trends.google.com/trending?geo=US&hl=en-US&hours=168&status=active",
"options": {}
},
"typeVersion": 4.4
},
{
"id": "8cecc5ce-99e4-4efc-a8ef-afdc70603d5d",
"name": "Parse Trend Data",
"type": "n8n-nodes-base.code",
"position": [
1504,
-128
],
"parameters": {
"jsCode": "// ============================================================\n// Google Trends Extractor \u2014 Bracket-Balanced Parser\n// No regex for data extraction; walks chars to find JSON\n// ============================================================\n\nconst RAW_HTML = $input.first().json.data;\n\nif (typeof RAW_HTML !== 'string') {\n throw new Error('Expected string HTML. Got: ' + typeof RAW_HTML);\n}\n\n// -----------------------------------------------------------\n// Bracket-balanced extractor\n// Walks from a '[' or '{' and finds the matching close char,\n// respecting strings and escape sequences. Then tries JSON.parse.\n// -----------------------------------------------------------\nfunction extractBalancedJSON(html, start) {\n const open = html[start];\n if (open !== '[' && open !== '{') return null;\n\n let depth = 0;\n let inStr = false;\n let esc = false;\n let strChar = '';\n const limit = Math.min(start + 6_000_000, html.length);\n\n for (let i = start; i < limit; i++) {\n const c = html[i];\n if (esc) { esc = false; continue; }\n if (inStr) {\n if (c === '\\\\') { esc = true; continue; }\n if (c === strChar) { inStr = false; }\n continue;\n }\n if (c === '\"' || c === \"'\") { inStr = true; strChar = c; continue; }\n if (c === '[' || c === '{') { depth++; continue; }\n if (c === ']' || c === '}') {\n depth--;\n if (depth === 0) {\n const raw = html.slice(start, i + 1);\n // Strip Google's %.@. wire-format prefix if present\n const clean = raw.startsWith('%.@.') ? raw.slice(4) : raw;\n try { return JSON.parse(clean); } catch { return null; }\n }\n }\n }\n return null;\n}\n\n// -----------------------------------------------------------\n// Walk the HTML for every AF_initDataCallback occurrence,\n// locate the data: key, skip optional function(){return ...}\n// wrapper, then extract with the balanced walker.\n// -----------------------------------------------------------\nfunction extractAllCallbacks(html) {\n const results = [];\n const marker = 'AF_initDataCallback';\n let pos = 0;\n\n while (true) {\n const cbPos = html.indexOf(marker, pos);\n if (cbPos === -1) break;\n pos = cbPos + marker.length;\n\n const dataKeyPos = html.indexOf('data:', cbPos);\n if (dataKeyPos === -1 || dataKeyPos - cbPos > 600) continue;\n\n let dataStart = dataKeyPos + 5;\n // Skip whitespace\n while (dataStart < html.length && ' \\t\\n\\r'.includes(html[dataStart])) dataStart++;\n // Skip function(){return ...} wrapper if present\n if (html.slice(dataStart, dataStart + 8) === 'function') {\n const retIdx = html.indexOf('return', dataStart);\n if (retIdx !== -1 && retIdx - dataStart < 60) {\n dataStart = retIdx + 6;\n while (dataStart < html.length && ' \\t\\n\\r'.includes(html[dataStart])) dataStart++;\n }\n }\n\n const payload = extractBalancedJSON(html, dataStart);\n if (payload !== null) results.push(payload);\n }\n\n return results;\n}\n\n// -----------------------------------------------------------\n// Recursively search parsed payloads for the trends rows.\n// Signature: an array of arrays where each row has\n// - at least 6 elements\n// - row[0] is a non-empty string (the query)\n// Checks first 3 rows to confirm the pattern.\n// -----------------------------------------------------------\nfunction findTrendsArray(node, depth = 0) {\n if (depth > 10 || !node || typeof node !== 'object') return null;\n\n if (Array.isArray(node) && node.length >= 3) {\n const looksLikeTrends = node\n .slice(0, Math.min(3, node.length))\n .every(row => Array.isArray(row) && row.length >= 6 && typeof row[0] === 'string' && row[0].length > 0);\n if (looksLikeTrends) return node;\n }\n\n const children = Array.isArray(node) ? node : Object.values(node);\n for (const child of children) {\n if (child && typeof child === 'object') {\n const found = findTrendsArray(child, depth + 1);\n if (found) return found;\n }\n }\n return null;\n}\n\n// -----------------------------------------------------------\n// Run extraction\n// -----------------------------------------------------------\nconst payloads = extractAllCallbacks(RAW_HTML);\n\nif (payloads.length === 0) {\n const cbCount = (RAW_HTML.match(/AF_initDataCallback/g) || []).length;\n throw new Error(\n `No parseable AF_initDataCallback payloads found. ` +\n `Saw ${cbCount} callback marker(s) in ${RAW_HTML.length} chars. ` +\n `HTML snippet (first 500 chars): ${RAW_HTML.slice(0, 500)}`\n );\n}\n\nlet trendsRows = null;\nfor (const payload of payloads) {\n trendsRows = findTrendsArray(payload);\n if (trendsRows) break;\n}\n\nif (!trendsRows) {\n throw new Error(\n `Parsed ${payloads.length} callback payload(s) but no trends rows found inside. ` +\n `Top-level types: ${payloads.map(p => Array.isArray(p) ? 'array['+p.length+']' : 'object').join(', ')}`\n );\n}\n\n// -----------------------------------------------------------\n// Map raw rows \u2192 clean named objects\n//\n// Row index reference:\n// [0] query string\n// [1] entity/topic name (or null)\n// [2] country code\n// [3] [startTimestamp] \u2014 unix seconds in a 1-item array\n// [4] [endTimestamp] \u2014 unix seconds in a 1-item array\n// [6] search volume estimate\n// [8] trend/breakout score\n// [9] related queries \u2014 string[]\n// [10] category IDs \u2014 number[]\n// [12] normalized query\n// -----------------------------------------------------------\nfunction toISO(ts) { return ts ? new Date(ts * 1000).toISOString() : null; }\nfunction safe(arr, i, fb) { return Array.isArray(arr) && arr[i] != null ? arr[i] : (fb ?? null); }\nfunction tsFromWrapped(arr) { return Array.isArray(arr) && arr[0] != null ? arr[0] : null; }\n\nreturn trendsRows.map(row => ({\n json: {\n query: safe(row, 0, ''),\n entity: safe(row, 1),\n country: safe(row, 2, 'US'),\n startTime: toISO(tsFromWrapped(row[3])),\n endTime: toISO(tsFromWrapped(row[4])),\n searchVolume: safe(row, 6, 0),\n trendScore: safe(row, 8, 0),\n relatedQueries: Array.isArray(row[9]) ? row[9] : [],\n categoryIds: Array.isArray(row[10]) ? row[10] : [],\n normalizedQuery: safe(row, 12) || safe(row, 0, ''),\n }\n}));"
},
"typeVersion": 2
},
{
"id": "a98e6f04-d998-4568-83c8-2feca7835fa8",
"name": "If Trend Active",
"type": "n8n-nodes-base.if",
"position": [
1728,
-128
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "730e027e-119c-4a08-9028-e515fdfb71fd",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.endTime === null }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "60862c74-b6cd-42c3-b23c-1dd45a1956c9",
"name": "Prepare AI Input Data",
"type": "n8n-nodes-base.set",
"position": [
2176,
-128
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9ae4ac91-d6c1-410f-ad6f-3d6ce1cb8171",
"name": "combined things",
"type": "string",
"value": "={{ $json.data.map(item => item.query).join(', ') }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "6245af17-90e5-4876-8ffe-00ce1f059c26",
"name": "Aggregate Trend Results",
"type": "n8n-nodes-base.aggregate",
"position": [
1952,
-128
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "9a4fc4cf-8f36-4cd8-8605-db3755ae22b1",
"name": "Trend Summarizer Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2400,
-128
],
"parameters": {
"text": "=INPUT TERMS:\n{{ $json['combined things'] }}",
"options": {
"systemMessage": "You are a highly restrictive trend-filtering assistant for an automation agency (blankarray). The agency builds n8n workflows exclusively for **brick-and-mortar retail**, **physical store operations**, and **on-the-ground warehouse/logistics**.\n\nYour sole job is to evaluate a comma-separated string of trending search terms and extract ONLY the items that are explicitly tied to the physical retail and logistics niche.\n\nINCLUSION CRITERIA (Must strictly match one of these):\n1. Physical Retail & Store Ops: Brick-and-mortar formats (grocery, discount, big box, specialty), foot traffic, checkout ops, staffing/shift management, shelf inventory, physical merchandising.\n2. Warehousing & Logistics: Distribution centers, on-premise fulfillment, supply chain ops, warehouse robotics, palletizing, last-mile physical delivery fleets.\n3. In-Store Technology: Point of Sale (POS) hardware/systems, Self-Checkout (SCO), RFID, electronic shelf labels (ESL), physical loss prevention/shrink, smart carts, physical store security.\n4. Physical Retail Business News: Brick-and-mortar bankruptcies, physical store closures/openings, retail real estate expansions, warehouse unionization.\n\nEXCLUSION CRITERIA (Instantly reject these):\n- Pure E-commerce & D2C (Shopify, WooCommerce, dropshipping, digital storefronts) UNLESS explicitly tied to physical stores (e.g., BOPIS - Buy Online, Pick Up In Store).\n- Digital marketing, SEO, social media algorithms, or general SaaS.\n- General consumer tech, gadgets, or software unrelated to store/warehouse operations.\n- Sports, entertainment, pop culture, politics, weather, or celebrities.\n- General finance or macroeconomics (unless directly detailing physical retail sales/foot traffic).\n\nINSTRUCTIONS:\n- For each valid match, provide a brief `reason` explaining its direct connection to physical stores or warehouses\n- If a term is borderline or vague (e.g., \"AI integration\" or \"supply and demand\"), REJECT IT. Only accept clear, unambiguous physical retail/logistics terms.\n- Return ONLY valid JSON in the exact structure provided below. Do not include markdown formatting fences, preambles, or post-response explanations.\n\nIf no items are relevant, return exactly: {\"matches\": [], \"match_count\": 0}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3.1
},
{
"id": "d6ba6902-1792-4fae-933d-ef001d31eb0d",
"name": "Parse AI Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
2544,
96
],
"parameters": {
"jsonSchemaExample": "{\n \"matches\": [\n {\n \"term\": \"string\",\n \"reason\": \"string (Why it strictly fits physical retail/warehousing)\"\n }\n ],\n \"match_count\": 0\n}\n"
},
"typeVersion": 1.3
},
{
"id": "d04c675b-672c-471e-86d2-54faf71c2f58",
"name": "Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2416,
96
],
"parameters": {
"options": {},
"modelName": "models/gemini-3.1-flash-lite"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "3df3d00c-bebd-405e-ace6-79b5b37f3703",
"name": "Format Summary for Slack",
"type": "n8n-nodes-base.code",
"position": [
2784,
-128
],
"parameters": {
"jsCode": "// 1. Grab ALL trend items that passed the 'If' node (not just the first)\nconst allTrends = $('If Trend Active').all().map(i => i.json);\n\n// 2. Grab the array of matches from the AI Agent's structured output\nconst matchesArray = $input.first().json.output?.matches || [];\n\nconst successfulMatches = [];\n\nfor (const item of matchesArray) {\n const termString = String(item.term).toLowerCase();\n\n // 3. Find which original trend this AI match actually corresponds to\n const matchedTrend = allTrends.find(trend => {\n const queryString = String(trend.query).toLowerCase();\n return termString.includes(queryString) || queryString.includes(termString);\n });\n\n if (matchedTrend) {\n successfulMatches.push({\n term: item.term,\n reason: item.reason,\n original_query: matchedTrend.query,\n original_data: matchedTrend\n });\n }\n}\n\n// 4. If nothing matched, still return a clean, single-item shape\nif (successfulMatches.length === 0) {\n return [{\n json: {\n matches: [],\n message: \"No physical retail or logistics trends stood out this week \u2014 nothing worth flagging.\"\n }\n }];\n}\n\n// 5. Helper to make big numbers readable (e.g. 100000 -> 100K)\nfunction formatVolume(n) {\n if (n >= 1000000) return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1) + 'M';\n if (n >= 1000) return (n / 1000).toFixed(n % 1000 === 0 ? 0 : 1) + 'K';\n return String(n);\n}\n\n// 6. Build a conversational, Slack mrkdwn-formatted message\nconst lines = successfulMatches.map(m => {\n const vol = formatVolume(m.original_data.searchVolume);\n return `\u2022 *${m.original_query}* \u2014 ~${vol} searches this week (trend score: ${m.original_data.trendScore}). ${m.reason}`;\n});\n\nconst intro = successfulMatches.length === 1\n ? `Found a trend this week worth a look \ud83d\udc40`\n : `Found ${successfulMatches.length} trends this week worth a look \ud83d\udc40`;\n\nconst message = `${intro}\\n\\n${lines.join('\\n')}`;\n\n// 7. Return ONE item with both the raw matches and the message\nreturn [{\n json: {\n matches: successfulMatches,\n message\n }\n}];"
},
"typeVersion": 2
},
{
"id": "3474ce26-9627-433f-a936-f93d2c881838",
"name": "Post Summary to Slack",
"type": "n8n-nodes-base.slack",
"position": [
3008,
-128
],
"parameters": {
"text": "={{ $json.message }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0B8VH1M5PX",
"cachedResultName": "general"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.5
},
{
"id": "269fe5f0-f91d-4c64-ba8c-caa87810258f",
"name": "Every Week Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1056,
-128
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks"
}
]
}
},
"typeVersion": 1.3
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "e8144c02-19d0-424d-8117-fd7e13049037",
"nodeGroups": [],
"connections": {
"If Trend Active": {
"main": [
[
{
"node": "Aggregate Trend Results",
"type": "main",
"index": 0
}
]
]
},
"Parse AI Output": {
"ai_outputParser": [
[
{
"node": "Trend Summarizer Agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Parse Trend Data": {
"main": [
[
{
"node": "If Trend Active",
"type": "main",
"index": 0
}
]
]
},
"Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Trend Summarizer Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Every Week Trigger": {
"main": [
[
{
"node": "Fetch Google Trends Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare AI Input Data": {
"main": [
[
{
"node": "Trend Summarizer Agent",
"type": "main",
"index": 0
}
]
]
},
"Trend Summarizer Agent": {
"main": [
[
{
"node": "Format Summary for Slack",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Trend Results": {
"main": [
[
{
"node": "Prepare AI Input Data",
"type": "main",
"index": 0
}
]
]
},
"Fetch Google Trends Data": {
"main": [
[
{
"node": "Parse Trend Data",
"type": "main",
"index": 0
}
]
]
},
"Format Summary for Slack": {
"main": [
[
{
"node": "Post Summary to Slack",
"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.
googlePalmApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Video: https://www.youtube.com/shorts/6wRN6BODU
Source: https://n8n.io/workflows/17350/ — 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.
This workflow is designed for Japanese-speaking professionals, and learners who want to efficiently stay up to date with practical productivity, lifehack, and efficiency-related insights from Japanese
Video Explanation: https://youtu.be/5TNw4aaHtHI?si=wLY3Gc_i9n3vKYNQ
公認資格ワークフロー. Uses rssFeedRead, chainLlm, lmChatGoogleGemini, outputParserStructured. Scheduled trigger; 25 nodes.
This workflow runs every Monday at 8AM to pull a competitor list from Google Sheets, scrape the latest Crunchbase data via Apify, detect new funding, generate a strategic impact summary with Google Ge
blog writing automation. Uses rssFeedRead, lmChatOpenAi, textClassifier, agent. Scheduled trigger; 90 nodes.