AutomationFlowsAI & RAG › Auto-reply to Udemy Student Q&a with Mistral AI and Google Sheets

Auto-reply to Udemy Student Q&a with Mistral AI and Google Sheets

ByHesham Mashhour @thefamoushesham on n8n.io

If you teach on Udemy at any meaningful scale, you already know the problem: 80% of student messages are variations of the same handful of questions, but every one of them needs a thoughtful reply to keep your response rate up and your reviews healthy. Meanwhile, the actually…

Cron / scheduled trigger★★★★☆ complexityAI-powered29 nodesN8N Nodes GlobalsHTTP RequestGoogle SheetsAgentMemory Redis ChatLm Chat Mistral CloudAnthropic ChatJina Ai Tool
AI & RAG Trigger: Cron / scheduled Nodes: 29 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #15311 — we link there as the canonical source.

This workflow follows the Agent → Gmail 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
{
  "nodes": [
    {
      "id": "a3d70ad2-5e07-41b2-8e09-420ab377f17d",
      "name": "Trigger: Scheduled Run",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1600,
        192
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "50268d00-75f4-4117-9784-f5c7e2fec03a",
      "name": "Load Global Constants",
      "type": "n8n-nodes-globals.globalConstants",
      "position": [
        -1360,
        192
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "0877929f-5fe2-46ed-82cb-888763ffb239",
      "name": "Fetch Unreplied Threads (Udemy API)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1136,
        192
      ],
      "parameters": {
        "url": "https://www.udemy.com/instructor-api/v1/message-threads/",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "unreplied"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0d7940d4-0fd3-45f1-b052-fe78d280d5e8",
      "name": "Split Threads Array",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -896,
        192
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "results"
      },
      "typeVersion": 1
    },
    {
      "id": "03c62900-d0a6-451c-abfd-3e19d0ebd72c",
      "name": "Loop Through Each Thread",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -624,
        192
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "b5d82906-5567-4a12-b25e-3e7279e838c8",
      "name": "Fetch Full Thread History",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -336,
        208
      ],
      "parameters": {
        "url": "=https://www.udemy.com/instructor-api/v1/message-threads/{{ $json.id }}/messages/",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "message_thread_id",
              "value": "={{ $json.id }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "383df8ad-9ff2-4e22-a699-0741e9770fca",
      "name": "Log New Message to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        384,
        432
      ],
      "parameters": {
        "columns": {
          "value": {
            "ID": "={{ $json.results[0].id }}",
            "DATE": "={{ $json.results[0].created }}",
            "User": "={{ $json.results[0].user.title }}",
            "STATUS": "={{ $('Load Global Constants').first().json.constants.status6 }}",
            "Message Content": "={{ $json.results[0].content.replace(/<[^>]*>/g, '') }}",
            "Responded (Y/N)": "N",
            "Previous Interactions (Aggregated)": "={{ $json.results.slice(1).map((r, i) => `[${i + 1}] ${r.user.name}: ${r.content.replace(/<[^>]*>/g, '')}`).join('\\n\\n') }}"
          },
          "schema": [
            {
              "id": "ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "STATUS",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "STATUS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "DATE",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "DATE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "User",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "User",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message Content",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Message Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Previous Interactions (Aggregated)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Previous Interactions (Aggregated)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Response",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Response",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vetting",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Vetting",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Pass/Fail",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Pass/Fail",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Responded (Y/N)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Responded (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Further Action (Y/N)",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Further Action (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Load Global Constants').first().json.constants.sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Load Global Constants').first().json.constants.sheetid }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "28f0665e-7c04-41bd-b93f-82abfd2b2658",
      "name": "Skip if Instructor Sent Last Message",
      "type": "n8n-nodes-base.if",
      "position": [
        -64,
        208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1f150c14-4eaf-4c1f-b71e-583ae59ebe7b",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.results[0].user.title }}",
              "rightValue": "={{ $('Load Global Constants').first().json.constants.instructor_name }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "99a5e07d-419b-4b3a-8fee-46b3882139e9",
      "name": "Generate Redis Session Key",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        432
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\nlet result = '';\nlet length = 48; // Desired length of the random string\nfor (let i = 0; i < length; i++) {\n  result += characters.charAt(Math.floor(Math.random() * characters.length));\n}\nreturn { randomString: result };"
      },
      "typeVersion": 2
    },
    {
      "id": "f1e0d0f4-92e0-4740-9364-4a5c2136646e",
      "name": "AI Agent: Reply or Escalate",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        800,
        432
      ],
      "parameters": {
        "text": "=## Latest Message\n\n{{ $('Log New Message to Sheet').item.json['Message Content'] }}\n\n## Message Context\n\n{{ $('Log New Message to Sheet').item.json['Previous Interactions (Aggregated)'] }}\n\nOutput Format: Format your response in Markdown. Use \\n\\n to separate paragraphs. Use standard Markdown syntax for emphasis (**bold**, *italic*) or links ([text](url)) where appropriate. Do not use any HTML tags whatsoever (no <p>, <br>, <strong>, <em>, etc.). The response must be pure Markdown.",
        "options": {
          "systemMessage": "=You are an AI assistant responding on behalf of a Udemy instructor. Your role is to handle student Q&A messages professionally and efficiently.\n\n## Instructor Information\n- **Name:** {{ $('Load Global Constants').first().json.constants.instructor_name }}\n- **Bio:** {{ $('Load Global Constants').first().json.constants.instructor_bio }}\n\n## Your Task\nYou will receive:\n1. The latest student message requiring a response\n2. Previous message context (if available)\n\nYou must decide whether to respond directly OR escalate to the instructor.\n\n## Escalation Rules (Instructor Required ~20% of messages)\n\n### PRIORITY: Sales Opportunities\nALWAYS escalate when you detect a potential sales opportunity, including:\n- Student expresses interest in related topics not covered in the current course\n- Student asks about other courses the instructor offers\n- Student mentions wanting to go deeper or advance their skills\n- Student asks for recommendations on \"what to learn next\"\n- Student praises the course and expresses enthusiasm for more content\n- Student mentions career goals that could align with other offerings\n- Student asks about certifications, career paths, or comprehensive learning plans\n- Any opening where the instructor could naturally recommend their other courses or services\n\nThese are high-value interactions. The instructor's personal touch converts interest into enrollments.\n\n### Other Escalation Triggers\nESCALATE to the instructor when the message:\n- Asks personal questions about the instructor's background, experience, or life beyond the bio\n- Requests private coaching, consulting, or 1-on-1 mentorship\n- Asks about pricing, refunds, or payment issues\n- Requests course content changes, additions, or custom materials\n- Involves complaints or disputes requiring human judgment\n- Asks for contact information, social media, or communication outside Udemy\n- References information about the instructor's other services/offerings you don't have access to\n- Contains sensitive personal disclosures requiring empathetic human response\n- Is genuinely ambiguous AFTER the student has provided details\u2014meaning you cannot determine what they're asking even with full context (note: vague openers like \"I have a question\" are NOT ambiguous; just ask for clarification)\n\n## Auto-Response Rules (AI Handles ~80% of messages)\nRESPOND directly when the message:\n- Asks technical questions related to course content (use Jina Search to verify/supplement)\n- Seeks clarification on concepts taught in the course\n- Is a general greeting, thank you, or positive feedback WITHOUT expressed interest in more content\n- Asks about general best practices in the subject domain\n- Requests resource recommendations for FREE documentation, tools, or further reading\n- Reports minor technical issues with videos/quizzes (provide standard troubleshooting)\n- Asks \"how to\" questions within the course's subject matter\n- Is a simple follow-up to a previous exchange you can contextually address\n\n### Conversation Openers & Incomplete Messages\nRESPOND directly (do not escalate) when the message:\n- Is a greeting with a vague or unspecified question (\"Hi, I had a question about...\")\n- Says they have a question but doesn't actually ask it yet\n- Is an introduction without specific content\n\nFor these, respond warmly and invite them to share their question. Keep it brief and friendly, e.g.:\n\"Hey! Happy to help\u2014what's your question?\"\n\nDo NOT escalate these as \"ambiguous.\" They're just incomplete\u2014prompt for details.\n\n## Response Guidelines\nWhen you respond:\n1. **Length:** 10-100 words. Match complexity to the question\u2014simple questions get concise answers; detailed technical questions get fuller explanations. Never pad responses unnecessarily.\n2. **Tone:** Professional yet warm and approachable. You represent a friendly expert instructor.\n3. **Style Matching:** Mirror the formality level of the student and previous context, but always maintain professionalism. If the student is overly casual or uses slang, elevate slightly while remaining friendly.\n4. **Technical Accuracy:** Use Jina Search to verify technical information. Never guess on technical details.\n5. **No Fluff:** Avoid generic filler phrases like \"Great question!\" or \"I'm happy to help!\" unless the context genuinely calls for warmth.\n6. **Sign-off:** Do not include a sign-off or signature. The platform handles that.\n7. **Output Format:** Format your response in Markdown. Use \\n\\n to separate paragraphs. Use standard Markdown syntax (**bold**, *italic*, [text](url)) where appropriate. Never include HTML tags of any kind (no <p>, <br>, <strong>, <em>, <ul>, <li>, etc.). The output must be pure Markdown with zero HTML.\n8. **No Bullet Points:** Never use bullet points, numbered lists, or option menus in responses unless the student explicitly asks for a list.\n\n## Tools Available\n- **Jina Search (Research Mode enabled):** For technical queries, documentation lookups, and verifying current information. Research Mode allows for deeper, more comprehensive searches\u2014use it proactively for any technical question to ensure accuracy."
        },
        "promptType": "define",
        "needsFallback": true,
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "cf862460-b366-4799-9a19-bcd217d40f9d",
      "name": "Memory: Redis Chat",
      "type": "@n8n/n8n-nodes-langchain.memoryRedisChat",
      "position": [
        896,
        624
      ],
      "parameters": {
        "sessionKey": "={{ $json.randomString }}",
        "sessionTTL": 7200,
        "sessionIdType": "customKey",
        "contextWindowLength": 15
      },
      "typeVersion": 1.5
    },
    {
      "id": "4cd9d283-b8e5-41fb-a4c6-24494926cdc1",
      "name": "LLM: Mistral Large (Primary)",
      "type": "@n8n/n8n-nodes-langchain.lmChatMistralCloud",
      "position": [
        800,
        256
      ],
      "parameters": {
        "model": "mistral-large-latest",
        "options": {
          "topP": 0.9,
          "temperature": 0.5
        }
      },
      "typeVersion": 1
    },
    {
      "id": "53934c7f-0a30-4318-acba-de4c5fbd0ee1",
      "name": "LLM: Claude Sonnet 4.5 (Fallback)",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        784,
        624
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-5-20250929",
          "cachedResultName": "Claude Sonnet 4.5"
        },
        "options": {
          "thinking": true
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "18843db5-2c6d-4c14-b602-8a13fdd9ac49",
      "name": "Tool: Jina Deep Research",
      "type": "n8n-nodes-base.jinaAiTool",
      "position": [
        1008,
        624
      ],
      "parameters": {
        "options": {
          "maxReturnedSources": 8
        },
        "resource": "research",
        "simplify": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Simplify', ``, 'boolean') }}",
        "researchQuery": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Research_Query', ``, 'string') }}",
        "requestOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "48b56251-9745-4347-9dbc-f7c552e6f764",
      "name": "Parser: Structured JSON Output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        912,
        304
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"escalate_to_instructor\": {\n      \"type\": \"boolean\",\n      \"description\": \"True if the message requires instructor attention, false if AI can handle it\"\n    },\n    \"escalation_reason\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"sales_opportunity\",\n        \"personal_question\",\n        \"coaching_request\",\n        \"payment_or_refund\",\n        \"content_change_request\",\n        \"complaint_or_dispute\",\n        \"contact_request\",\n        \"unknown_instructor_info\",\n        \"sensitive_disclosure\",\n        \"ambiguous_message\",\n        \"not_applicable\"\n      ],\n      \"description\": \"Reason for escalation. Use 'not_applicable' when escalate_to_instructor is false. 'sales_opportunity' takes priority.\"\n    },\n    \"response\": {\n      \"type\": \"string\",\n      \"description\": \"The AI-generated response to send to the student, formatted in pure Markdown only. Must not contain any HTML tags (no <p>, <br>, <strong>, <em>, etc.). Use \\\\n\\\\n for paragraph breaks and standard Markdown syntax for emphasis or links. Empty string if escalated to instructor.\"\n    },\n    \"confidence\": {\n      \"type\": \"string\",\n      \"enum\": [\"high\", \"medium\", \"low\"],\n      \"description\": \"Confidence level in the response or escalation decision\"\n    },\n    \"tools_used\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\"jina_search\", \"none\"]\n      },\n      \"description\": \"Which tools were used to formulate the response\"\n    }\n  },\n  \"required\": [\"escalate_to_instructor\", \"escalation_reason\", \"response\", \"confidence\", \"tools_used\"]\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "186f663f-9f18-4571-b411-bac4623102a3",
      "name": "LLM: GPT-4.1-mini (Parser)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        912,
        176
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "14d670f3-0869-4d3d-8735-2896c2387874",
      "name": "Branch: Auto-Reply or Escalate?",
      "type": "n8n-nodes-base.if",
      "position": [
        1344,
        432
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "0fde09e9-8ed3-404b-b585-bb5ab9e96822",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.output.escalate_to_instructor }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "c201a01a-3ddf-4dd4-979e-1af442a57d9b",
      "name": "Mark Row as Escalated",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1600,
        416
      ],
      "parameters": {
        "columns": {
          "value": {
            "ID": "={{ $('Log New Message to Sheet').item.json.ID }}",
            "STATUS": "={{ $('Load Global Constants').first().json.constants.status3 }}",
            "Responded (Y/N)": "N",
            "Further Action (Y/N)": "Y"
          },
          "schema": [
            {
              "id": "ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "STATUS",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "STATUS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "DATE",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "DATE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "User",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "User",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message Content",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Message Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Previous Interactions (Aggregated)",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Previous Interactions (Aggregated)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Response",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Response",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vetting",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Vetting",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Pass/Fail",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Pass/Fail",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Responded (Y/N)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Responded (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Further Action (Y/N)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Further Action (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Load Global Constants').first().json.constants.sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Load Global Constants').first().json.constants.sheetid }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "02da2c2e-16cc-4e6a-9505-622ad724d064",
      "name": "Save AI Response to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1600,
        640
      ],
      "parameters": {
        "columns": {
          "value": {
            "ID": "={{ $('Log New Message to Sheet').item.json.ID }}",
            "STATUS": "={{ $('Load Global Constants').first().json.constants.status5 }}",
            "Vetting": "={{ $json.output.confidence }}",
            "Response": "={{ $json.output.response }}",
            "Responded (Y/N)": "Y",
            "Further Action (Y/N)": "N"
          },
          "schema": [
            {
              "id": "ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "STATUS",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "STATUS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "DATE",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "DATE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "User",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "User",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message Content",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Message Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Previous Interactions (Aggregated)",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Previous Interactions (Aggregated)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Response",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Response",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vetting",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Vetting",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Pass/Fail",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Pass/Fail",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Responded (Y/N)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Responded (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Further Action (Y/N)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Further Action (Y/N)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Load Global Constants').first().json.constants.sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Load Global Constants').first().json.constants.sheetid }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "99286269-bf33-450c-84dd-daea6b31a160",
      "name": "Post AI Reply to Udemy",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1808,
        640
      ],
      "parameters": {
        "url": "=https://www.udemy.com/instructor-api/v1/message-threads/{{ $('Loop Through Each Thread').item.json.id }}/messages/",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"content\": {{ JSON.stringify($json.Response) }}\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.3
    },
    {
      "id": "9e178b2e-5f94-4f52-a854-588fa06b2b9e",
      "name": "Email Instructor for Manual Reply",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1808,
        416
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=Hi {{ $('Load Global Constants').first().json.constants.instructor_name }}\n\nYou have a student message that requires your input on Udemy and the system has deemed it inappropriate to be answered by an LLM.\n\nMessage:\n{{ $('Log New Message to Sheet').item.json['Message Content'] }}\n\nURL:\nhttps://www.udemy.com/instructor/communication/messages",
        "options": {
          "ccList": "",
          "senderName": "Udemy API Messenger by Hesham"
        },
        "subject": "Udemy Student Message Requiring Instructor Response"
      },
      "typeVersion": 2.2
    },
    {
      "id": "e8791752-2f69-4669-9005-4199157b0e38",
      "name": "Note: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2640,
        -176
      ],
      "parameters": {
        "color": 3,
        "width": 880,
        "height": 376,
        "content": "## \ud83d\udcec Udemy Q&A Auto-Responder\n\n**What this does:** Automatically fetches unreplied student messages from your Udemy instructor inbox, generates context-aware AI replies for ~80% of them, and escalates the remaining ~20% (sales opportunities, complaints, personal questions, refund requests, etc.) to you via email for a human touch.\n\n**How it works:**\n1. Polls Udemy's Instructor API for unreplied message threads\n2. Fetches the full thread history for each one\n3. Skips threads where you (the instructor) sent the last message\n4. Logs the message + context to Google Sheets for auditability\n5. Runs an AI Agent (Mistral primary, Claude fallback) with a structured output parser to decide: respond OR escalate\n6. If respond \u2192 posts the reply back to Udemy via API + updates the sheet\n7. If escalate \u2192 emails you with the message and a link to your Udemy inbox\n\n**Cost:** ~$0.001\u20130.005 per message depending on which model handles it.\n\n**Runtime:** Ships with a 30-minute schedule. Adjust the Trigger: Scheduled Run interval to your preferred cadence before going live."
      },
      "typeVersion": 1
    },
    {
      "id": "c4b59e57-4090-40e7-a40c-1a6513ce5ff7",
      "name": "Note: Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2640,
        240
      ],
      "parameters": {
        "color": 5,
        "width": 880,
        "height": 720,
        "content": "## \u2699\ufe0f SETUP REQUIRED BEFORE FIRST RUN\n\n### 1. Credentials (8 total)\nCreate these credentials in n8n's Credentials tab and attach them to the corresponding nodes:\n\n- **Udemy Instructor API** (HTTP Header Auth) \u2014 `Authorization: Bearer <token>`. Get token from Udemy instructor dashboard \u2192 API Clients. Used by `Fetch Unreplied Threads (Udemy API)`, `Fetch Full Thread History`, and `Post AI Reply to Udemy`.\n- **Google Sheets** (OAuth2) \u2014 for the audit log. Used by `Log New Message to Sheet`, `Mark Row as Escalated`, and `Save AI Response to Sheet`.\n- **Gmail** (OAuth2) \u2014 for escalation emails. Used by `Email Instructor for Manual Reply`.\n- **Mistral Cloud** \u2014 primary LLM. Used by `LLM: Mistral Large (Primary)`.\n- **Anthropic** \u2014 fallback LLM (Claude Sonnet 4.5). Used by `LLM: Claude Sonnet 4.5 (Fallback)`.\n- **OpenAI** \u2014 drives the structured output parser. Used by `LLM: GPT-4.1-mini (Parser)`.\n- **Jina AI** \u2014 research tool for technical answers. Used by `Tool: Jina Deep Research`.\n- **Redis** \u2014 chat memory (any Redis instance works). Used by `Memory: Redis Chat`.\n\n### 2. Global Constants (n8n-nodes-globals)\nPopulate the `Udemy Q&A Global Constants` credential with these keys:\n\n- `instructor_name` \u2014 your Udemy instructor display name, EXACTLY as it appears on Udemy (case-sensitive, including pipe characters and spacing). Used both in the AI's system prompt AND in `Skip if Instructor Sent Last Message` to detect your own messages.\n- `instructor_bio` \u2014 short bio injected into the AI's system prompt\n- `sheetid` \u2014 the Google Sheet ID from the URL\n- `sheet_name` \u2014 the tab name (e.g. \"Messages\")\n- `status3` \u2014 value written when message is escalated (e.g. \"Escalated\")\n- `status5` \u2014 value written when AI replied (e.g. \"Auto-Replied\")\n- `status6` \u2014 value written on initial logging (e.g. \"Pending\")\n\n### 3. Configure `Email Instructor for Manual Reply`\nUpdate the `sendTo` field to your own email address (currently set to placeholder `instructor@email.com`). The greeting auto-fills your name from `instructor_name`."
      },
      "typeVersion": 1
    },
    {
      "id": "377f9b65-aabf-4e5b-8173-54c4e94c57ce",
      "name": "Note: Google Sheet Schema",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1712,
        448
      ],
      "parameters": {
        "color": 6,
        "width": 992,
        "height": 388,
        "content": "## \ud83d\udcca Google Sheet Schema\n\nCreate a sheet (any name, but match it to the `sheet_name` global constant) with these column headers in **row 1**, in any order \u2014 the workflow uses named-column matching:\n\n| Column Name | Purpose | Filled By |\n|---|---|---|\n| `ID` | Udemy message ID (unique key) | `Log New Message to Sheet` |\n| `STATUS` | Pending \u2192 Auto-Replied / Escalated | Updated through workflow |\n| `DATE` | ISO timestamp from Udemy | `Log New Message to Sheet` |\n| `User` | Student's display title | `Log New Message to Sheet` |\n| `Message Content` | Latest student message (HTML stripped) | `Log New Message to Sheet` |\n| `Previous Interactions (Aggregated)` | Full thread history flattened | `Log New Message to Sheet` |\n| `Response` | The AI-generated reply that was sent | `Save AI Response to Sheet` |\n| `Vetting` | AI's self-reported confidence | `Save AI Response to Sheet` |\n| `Pass/Fail` | Manual review column (you fill this in) | Manual |\n| `Responded (Y/N)` | Whether a reply was sent | Updated through workflow |\n| `Further Action (Y/N)` | Whether instructor needs to follow up | Updated through workflow |\n\n\u26a0\ufe0f **Column names are case-sensitive** and must match exactly. Spaces and parentheses matter.\n\n\ud83d\udca1 **Tip:** Use the `Pass/Fail` column to spot-check AI responses for the first week. If quality is consistent, you can stop reviewing."
      },
      "typeVersion": 1
    },
    {
      "id": "a9d1b611-449c-413d-83e9-9f1e12aa094a",
      "name": "Note: Stage 1 \u2014 Fetch & Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1712,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 1292,
        "height": 552,
        "content": "## \ud83d\udd0d Stage 1: Fetch & Filter\n\n**Fetch Unreplied Threads (Udemy API)** \u2192 calls `GET /instructor-api/v1/message-threads/?status=unreplied` to pull all threads needing a response.\n\n**Split Threads Array** \u2192 splits the `results` array so each thread becomes its own item.\n\n**Loop Through Each Thread** \u2192 processes one thread at a time (prevents rate-limit issues on Udemy and the AI APIs).\n\n**Fetch Full Thread History** \u2192 for each thread, fetches the full message history at `/message-threads/{id}/messages/`.\n\n**Skip if Instructor Sent Last Message** \u2192 checks if the most recent message was sent by the instructor (i.e. you replied last and the student hasn't followed up yet). Compares the thread's latest sender against `instructor_name` from Global Constants. If it matches \u2192 skip and move to the next thread. If not \u2192 log + respond."
      },
      "typeVersion": 1
    },
    {
      "id": "ca7b94c8-4c86-40ac-bab7-0431353735f5",
      "name": "Note: Stage 2 \u2014 AI Decision",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 912,
        "height": 1096,
        "content": "## \ud83e\udd16 Stage 2: AI Decision Engine\n\n**Log New Message to Sheet** \u2192 logs the new message with status \"Pending\". Creates the audit trail.\n\n**Generate Redis Session Key** \u2192 creates a unique 48-char session key so each thread gets its own isolated conversation context in `Memory: Redis Chat`.\n\n**AI Agent: Reply or Escalate** \u2192 the brain. Uses:\n- **LLM: Mistral Large (Primary)** \u2014 fast and cheap for routine Q&A\n- **LLM: Claude Sonnet 4.5 (Fallback)** \u2014 kicks in if Mistral fails\n- **Memory: Redis Chat** \u2014 2-hour TTL, 15-message context window\n- **Tool: Jina Deep Research** \u2014 verifies technical claims before answering\n- **Parser: Structured JSON Output** (powered by `LLM: GPT-4.1-mini (Parser)`) \u2014 forces output into a strict schema with: `escalate_to_instructor`, `escalation_reason`, `response`, `confidence`, `tools_used`\n\n**Output is pure Markdown** \u2014 no HTML tags. Udemy's Q&A field renders Markdown natively.\n\n\u26a0\ufe0f **System prompt notes:**\n- ~20% escalation target is a soft guideline\n- Sales opportunities ALWAYS escalate (high-value, instructor's personal touch converts)\n- Conversation openers (\"Hi, I had a question\u2026\") get a friendly prompt-for-detail reply, NOT an escalation"
      },
      "typeVersion": 1
    },
    {
      "id": "8c070190-e411-4358-baa2-3214b390d000",
      "name": "Note: Stage 3 \u2014 Respond or Escalate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 1056,
        "height": 896,
        "content": "## \ud83d\udce4 Stage 3: Respond or Escalate\n\n**Branch: Auto-Reply or Escalate?** \u2192 IF node checks `output.escalate_to_instructor`. Routes to one of two branches.\n\n### \u2705 Branch A \u2014 AI handles it (false)\n1. **Save AI Response to Sheet** \u2192 marks Status=\"Auto-Replied\", Response=<AI reply>, Vetting=<confidence>, Responded=Y\n2. **Post AI Reply to Udemy** \u2192 POSTs the reply to `/message-threads/{id}/messages/` with `{\"content\": \"<reply>\"}` in JSON body\n\n### \ud83d\udea8 Branch B \u2014 Escalate to instructor (true)\n1. **Mark Row as Escalated** \u2192 marks Status=\"Escalated\", Responded=N, Further Action=Y\n2. **Email Instructor for Manual Reply** \u2192 emails the instructor with the message content and a deep link to the Udemy inbox\n\nBoth branches loop back to **Loop Through Each Thread** to process the next thread."
      },
      "typeVersion": 1
    },
    {
      "id": "e8fad344-1048-446d-a55c-67cff0b543c7",
      "name": "Note: Limitations & Testing1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        160
      ],
      "parameters": {
        "color": 3,
        "width": 166,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "a4a79c4d-0dc0-4fb4-a139-31aba2ceb88d",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        2160,
        768
      ],
      "parameters": {
        "mode": "chooseBranch",
        "output": "empty"
      },
      "typeVersion": 3.2
    }
  ],
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Loop Through Each Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Memory: Redis Chat": {
      "ai_memory": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Split Threads Array": {
      "main": [
        [
          {
            "node": "Loop Through Each Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Global Constants": {
      "main": [
        [
          {
            "node": "Fetch Unreplied Threads (Udemy API)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Row as Escalated": {
      "main": [
        [
          {
            "node": "Email Instructor for Manual Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post AI Reply to Udemy": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Trigger: Scheduled Run": {
      "main": [
        [
          {
            "node": "Load Global Constants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log New Message to Sheet": {
      "main": [
        [
          {
            "node": "Generate Redis Session Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Through Each Thread": {
      "main": [
        [],
        [
          {
            "node": "Fetch Full Thread History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool: Jina Deep Research": {
      "ai_tool": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Full Thread History": {
      "main": [
        [
          {
            "node": "Skip if Instructor Sent Last Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save AI Response to Sheet": {
      "main": [
        [
          {
            "node": "Post AI Reply to Udemy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Redis Session Key": {
      "main": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM: GPT-4.1-mini (Parser)": {
      "ai_languageModel": [
        [
          {
            "node": "Parser: Structured JSON Output",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent: Reply or Escalate": {
      "main": [
        [
          {
            "node": "Branch: Auto-Reply or Escalate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM: Mistral Large (Primary)": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Parser: Structured JSON Output": {
      "ai_outputParser": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Branch: Auto-Reply or Escalate?": {
      "main": [
        [
          {
            "node": "Mark Row as Escalated",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save AI Response to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Instructor for Manual Reply": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM: Claude Sonnet 4.5 (Fallback)": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent: Reply or Escalate",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    },
    "Fetch Unreplied Threads (Udemy API)": {
      "main": [
        [
          {
            "node": "Split Threads Array",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip if Instructor Sent Last Message": {
      "main": [
        [
          {
            "node": "Loop Through Each Thread",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log New Message to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

If you teach on Udemy at any meaningful scale, you already know the problem: 80% of student messages are variations of the same handful of questions, but every one of them needs a thoughtful reply to keep your response rate up and your reviews healthy. Meanwhile, the actually…

Source: https://n8n.io/workflows/15311/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This n8n automation workflow automates the creation, scripting, production, and posting of YouTube videos. It leverages AI (OpenAI), image generation (PIAPI), video rendering (Shotstack), and platform

Agent, OpenAI Chat, Airtable Tool +7
AI & RAG

Created by: Peyton Leveillee Last updated: October 2025

OpenAI Chat, Google Sheets, HTTP Request +5
AI & RAG

The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe

Google Sheets, Gmail, Google Drive +6
AI & RAG

SEO Blog Article Generation Workflow. Uses outputParserStructured, httpRequest, agent, lmChatOpenAi. Scheduled trigger; 56 nodes.

Output Parser Structured, HTTP Request, Agent +4
AI & RAG

This workflow was born out of a very real problem.

Output Parser Structured, OpenAI Chat, Memory Buffer Window +11