This workflow follows the Chat Trigger → Google Sheets 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 →
{
"name": "The AI Stackk - Course Finder Chatbot",
"nodes": [
{
"parameters": {
"public": true,
"initialMessages": "Welcome to The AI Stackk Course Finder.\n\nI will help you choose the best next course or video from our approved library.\n\nWhat do you want to learn?",
"options": {
"responseMode": "lastNode",
"title": "The AI Stackk Course Finder",
"subtitle": "Find the right lesson for your goal, level, and available time"
}
},
"id": "f10753d7-a644-4a07-b655-b902f5473011",
"name": "When chat message received",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.3,
"position": [
-900,
0
]
},
{
"parameters": {
"jsCode": "const input = String($json.chatInput || '').trim();\nconst normalized = input.toLowerCase();\nconst sessionId = String($json.sessionId || $json.session_id || 'demo-session');\nconst store = $getWorkflowStaticData('global');\nstore.courseFinderSessions = store.courseFinderSessions || {};\nconst profile = store.courseFinderSessions[sessionId] || { goal: '', level: '', minutes: null };\n\nconst resetRequested = /\\b(reset|restart|start over|new search)\\b/i.test(input);\nif (resetRequested) {\n store.courseFinderSessions[sessionId] = { goal: '', level: '', minutes: null };\n return [{ json: { output: 'Let us start again. What do you want to learn?', ready: false, sessionId } }];\n}\n\nconst levelMatch = normalized.match(/\\b(beginner|new|starting|intermediate|advanced|expert)\\b/);\nif (levelMatch) {\n const rawLevel = levelMatch[1];\n profile.level = ['new', 'starting'].includes(rawLevel) ? 'beginner' : rawLevel === 'expert' ? 'advanced' : rawLevel;\n}\n\nconst timeMatch = normalized.match(/(\\d+(?:\\.\\d+)?)\\s*(minutes?|mins?|min|hours?|hrs?|hr)\\b/);\nif (timeMatch) {\n const amount = Number(timeMatch[1]);\n profile.minutes = /hour|hr/.test(timeMatch[2]) ? Math.round(amount * 60) : Math.round(amount);\n}\n\nconst knownGoals = [\n 'customer support', 'technical support', 'healthcare', 'telecom', 'utilities',\n 'energy', 'automobile', 'automotive', 'hr', 'human resources', 'finance',\n 'personal finance', 'education', 'hospitality', 'chatbots', 'automation',\n 'ai agents', 'prompting', 'rag', 'artificial intelligence', 'ai'\n];\nconst detectedGoal = knownGoals\n .sort((a, b) => b.length - a.length)\n .find((goal) => normalized.includes(goal));\n\nif (detectedGoal && !/^(beginner|intermediate|advanced|expert|new|starting)$/i.test(input)) {\n profile.goal = detectedGoal === 'automotive' ? 'automobile' : detectedGoal === 'human resources' ? 'hr' : detectedGoal;\n}\n\nif (!profile.goal && input.length > 3 && !levelMatch && !timeMatch) {\n profile.goal = normalized.replace(/^(i want to learn|i need to learn|teach me|learn about)\\s+/i, '').trim();\n}\n\nstore.courseFinderSessions[sessionId] = profile;\n\nlet output = '';\nif (!profile.goal) {\n output = 'What do you want to learn or improve? For example: AI for customer support.';\n} else if (!profile.level) {\n output = `You want to learn ${profile.goal}. What is your current level: beginner, intermediate, or advanced?`;\n} else if (!profile.minutes) {\n output = `You are a ${profile.level} learning ${profile.goal}. How much time do you have today? For example: 45 minutes.`;\n}\n\nreturn [{\n json: {\n output,\n ready: Boolean(profile.goal && profile.level && profile.minutes),\n profile,\n sessionId\n }\n}];"
},
"id": "a54f1c47-8476-460a-a37a-1cf30ce54aae",
"name": "Collect learner profile",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-650,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a888fc49-0036-43de-aa1f-f5ceece29362",
"leftValue": "={{ $json.ready }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "5b318d82-9d2d-4979-bac6-c9550d1c1393",
"name": "Profile complete?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-390,
0
]
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "PASTE_GOOGLE_SHEET_ID_HERE",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "Catalog"
},
"options": {}
},
"id": "e7c57799-903f-4a29-80dd-970a1946f2c6",
"name": "Read approved catalog",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-100,
-100
],
"notes": "Select your Google Sheets credential, replace the document ID, and choose the Catalog tab."
},
{
"parameters": {
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst context = $('Collect learner profile').first().json;\nconst profile = context.profile;\nconst levelRank = { beginner: 1, intermediate: 2, advanced: 3 };\nconst learnerRank = levelRank[profile.level] || 1;\n\nconst normalize = (value) => String(value || '').trim().toLowerCase();\nconst tokenize = (value) => normalize(value).split(/[^a-z0-9]+/).filter((token) => token.length > 1);\nconst goalTokens = tokenize(profile.goal);\n\nconst scored = rows\n .filter((row) => normalize(row.approved) !== 'false')\n .filter((row) => row.title && row.url)\n .map((row) => {\n const rowLevel = normalize(row.level) || 'beginner';\n const rowRank = levelRank[rowLevel] || 1;\n const duration = Number(row.duration_minutes || row.duration || 0);\n const searchable = [row.topic, row.industry, row.use_case, row.outcome, row.title].map(normalize).join(' ');\n const matchedTokens = goalTokens.filter((token) => searchable.includes(token));\n const topicScore = matchedTokens.length * 4;\n const exactPhrase = searchable.includes(normalize(profile.goal)) ? 8 : 0;\n const levelScore = rowRank === learnerRank ? 6 : rowRank < learnerRank ? 3 : -8;\n const timeScore = duration > 0 && duration <= profile.minutes ? 5 : duration > profile.minutes ? -5 : 0;\n return { ...row, duration, rowLevel, score: topicScore + exactPhrase + levelScore + timeScore, matchedTokens };\n })\n .filter((row) => levelRank[row.rowLevel] <= learnerRank)\n .filter((row) => row.duration <= profile.minutes)\n .sort((a, b) => b.score - a.score || a.duration - b.duration);\n\nconst chosen = [];\nlet totalMinutes = 0;\nfor (const row of scored) {\n if (chosen.length >= 3) break;\n if (totalMinutes + row.duration <= profile.minutes) {\n chosen.push(row);\n totalMinutes += row.duration;\n }\n}\n\nlet output;\nif (!chosen.length || chosen[0].score < 4) {\n output = `I could not find a strong approved match for ${profile.goal} at the ${profile.level} level within ${profile.minutes} minutes. The catalog owner should add or approve a closer resource. Try a broader topic, or type restart to begin again.`;\n} else {\n const list = chosen.map((row, index) => {\n const why = row.outcome\n ? `${row.outcome} It matches your ${profile.level} level and fits your available time.`\n : `It matches your ${profile.level} level, your ${profile.goal} goal, and your available time.`;\n return `${index + 1}. ${row.title}\\n${row.format || 'Resource'} | ${row.duration} minutes\\n${row.url}\\nWhy this fits: ${why}`;\n }).join('\\n\\n');\n output = `Here is your recommended learning path for ${profile.goal}:\\n\\n${list}\\n\\nTotal learning time: ${totalMinutes} minutes.\\n\\nType restart whenever you want a new recommendation.`;\n}\n\nconst store = $getWorkflowStaticData('global');\nif (store.courseFinderSessions) delete store.courseFinderSessions[context.sessionId];\n\nreturn [{ json: { output, profile, recommendations: chosen, totalMinutes } }];"
},
"id": "c41c7f03-bc1f-4430-a6c5-a19bffcc33dc",
"name": "Rank and explain recommendations",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
180,
-100
]
},
{
"parameters": {
"jsCode": "return [{ json: { output: $json.output, profile: $json.profile || null } }];"
},
"id": "1d0b33a0-36dd-44bc-bfcd-f2ac164eec17",
"name": "Ask next question",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-100,
120
]
},
{
"parameters": {
"content": "## 1. Intake\nThe chatbot remembers goal, level, and available time for each chat session.",
"height": 250,
"width": 520,
"color": 5
},
"id": "1f066144-d8f4-43ec-84dd-ed2573057553",
"name": "Recording zone 1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-960,
-220
]
},
{
"parameters": {
"content": "## 2. Approved Catalog\nOnly rows from the connected Catalog sheet can be recommended.",
"height": 250,
"width": 260,
"color": 4
},
"id": "972c3ef0-e380-4e13-961c-7703866f08a1",
"name": "Recording zone 2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-150,
-220
]
},
{
"parameters": {
"content": "## 3. Match and Explain\nFilter by level and time, rank by relevance, return no more than three approved resources, and explain why.",
"height": 250,
"width": 360,
"color": 6
},
"id": "ae60fd7d-dad8-4f54-aace-c7c1ac848be2",
"name": "Recording zone 3",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
130,
-220
]
}
],
"connections": {
"When chat message received": {
"main": [
[
{
"node": "Collect learner profile",
"type": "main",
"index": 0
}
]
]
},
"Collect learner profile": {
"main": [
[
{
"node": "Profile complete?",
"type": "main",
"index": 0
}
]
]
},
"Profile complete?": {
"main": [
[
{
"node": "Read approved catalog",
"type": "main",
"index": 0
}
],
[
{
"node": "Ask next question",
"type": "main",
"index": 0
}
]
]
},
"Read approved catalog": {
"main": [
[
{
"node": "Rank and explain recommendations",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "43131eb6-a2ab-4c14-b085-699d23f561ef",
"meta": {
"templateCredsSetupCompleted": false
},
"tags": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
The AI Stackk - Course Finder Chatbot. Uses chatTrigger, googleSheets. Chat trigger; 9 nodes.
Source: https://github.com/satmakuru222/TheAIStackk/blob/main/course-finder-chatbot-workflow.json — 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 builds a free lead generation system that scrapes emails from Google Maps listings and exports them directly into Google Sheets. It’s built in n8n using HTTP requests and JavaScript—no p
📺 Full walkthrough video: https://youtu.be/x3PDYon4qKk
This workflow is designed for marketers, researchers, and business owners who need to quickly find and export company data from Google Maps into a structured table format.
📺 Full walkthrough video: https://youtu.be/yjeKYfZP0kU
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.