AutomationFlowsData & Sheets › The AI Stackk - Course Finder Chatbot

The AI Stackk - Course Finder Chatbot

The AI Stackk - Course Finder Chatbot. Uses chatTrigger, googleSheets. Chat trigger; 9 nodes.

Chat trigger trigger★★★★☆ complexityAI-powered9 nodesChat TriggerGoogle Sheets
Data & Sheets Trigger: Chat trigger Nodes: 9 Complexity: ★★★★☆ AI nodes: yes Added:

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 →

Download .json
{
  "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": []
}
Pro

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 →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

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

HTTP Request, Google Sheets, Chat Trigger
Data & Sheets

📺 Full walkthrough video: https://youtu.be/x3PDYon4qKk

Chat Trigger, @Mendable/N8N Nodes Firecrawl, Google Sheets +1
Data & Sheets

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.

Chat Trigger, Google Sheets, HTTP Request
Data & Sheets

📺 Full walkthrough video: https://youtu.be/yjeKYfZP0kU

Google Drive, Google Sheets, @Mendable/N8N Nodes Firecrawl +2
Data & Sheets

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

N8N Nodes Serpapi, Chat Trigger, Google Sheets