{
  "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
          }
        ]
      ]
    }
  }
}