{
  "updatedAt": "2026-02-12T19:43:23.885Z",
  "createdAt": "2025-12-12T10:12:28.713Z",
  "id": "HgA9JOnX1vsDYaED",
  "name": "Virtual Tutor Core - DB Edition (Secured)",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "jsCode": "// 1. Parse incoming data (GET or POST)\nconst input = $input.first().json;\nlet body = {};\ntry {\n  body = (input.query && input.query.data)\n    ? JSON.parse(input.query.data)\n    : (input.body || input);\n} catch (e) {\n  body = {};\n}\n\nconst payload = body.data || body;\n\n// Sanitize and validate inputs\nconst sanitize = (str, maxLen = 500) => {\n  if (typeof str !== 'string') return '';\n  return str.slice(0, maxLen).trim();\n};\n\nconst validateStudentId = (id) => {\n  // Convert to integer, default to 999 for guest\n  if (!id || id === 'guest' || id === 'STU_DEFAULT') return 999;\n  const parsed = parseInt(id);\n  return isNaN(parsed) ? parsed : 999;\n};\n\nconst allowedSubjects = ['Maths', 'English', 'Science', 'Biology', 'Chemistry', 'Physics', 'History', 'Geography', 'Computer Science', 'General'];\nconst normalizeSubject = (subj) => {\n  if (typeof subj !== 'string') return 'General';\n  const match = allowedSubjects.find(s => s.toLowerCase() === subj.toLowerCase().trim());\n  return match || 'General';\n};\n\n// Get action type\nconst action = body.action || 'ask_tutor';\nconst studentId = validateStudentId(body.studentId || body.userId);\nconst question = sanitize(payload.question || 'Hello', 1000);\nconst subject = normalizeSubject(payload.subject || body.subject);\nconst count = parseInt(payload.count) || 5;\nconst difficulty = payload.difficulty || 'medium';\nconst topic = sanitize(payload.topic || '', 200);\n\nif (!question && action !== 'get_history') {\n  throw new Error('Question is required');\n}\n\nreturn {\n  action,\n  studentId,\n  question,\n  subject,\n  count,\n  difficulty,\n  topic,\n  timestamp: new Date().toISOString()\n};"
      },
      "id": "178eda94-2bc8-46f4-84e0-f3f12d8b581a",
      "name": "Parse Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2960,
        -1680
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO students (student_id, student_name, xp_points, current_level) VALUES ($1, $2, 0, 1) ON CONFLICT (student_id) DO UPDATE SET student_id = EXCLUDED.student_id RETURNING *;",
        "options": {
          "queryReplacement": "={{ [$json.studentId, 'Student ' + $json.studentId] }}"
        }
      },
      "id": "a24d8f7e-6775-42b9-a5da-21613d7221c5",
      "name": "Fetch/Create Student",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -2784,
        -1680
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// Build GCSE-optimized prompt\nconst student = $input.first().json;\nconst request = $(\"Parse Request\").first().json;\n\n// Determine student rank based on XP\nconst getRank = (xp) => {\n  if (xp >= 1000) return 'Scholar';\n  if (xp >= 500) return 'Apprentice';\n  if (xp >= 100) return 'Learner';\n  return 'Initiate';\n};\n\nconst rank = getRank(student.current_level || 0);\nconst level = student.current_level || 1;\n\n// Check action type\nconst action = request.action || 'ask_tutor';\nlet prompt;\n\nif (action === 'generate_quiz') {\n  // Quiz generation prompt\n  const difficultyDesc = {\n    easy: 'basic recall and understanding questions suitable for foundation tier',\n    medium: 'application and analysis questions suitable for crossover grades 4-6',\n    hard: 'evaluation and synthesis questions suitable for higher tier grades 7-9'\n  };\n  \n  prompt = `Generate exactly ${request.count} GCSE ${request.subject} multiple choice quiz questions${request.topic ? ` about \"${request.topic}\"` : ''}.\nDifficulty: ${request.difficulty} (${difficultyDesc[request.difficulty]})\n\nIMPORTANT: Respond ONLY with valid JSON, no markdown, no explanation. Use this exact format:\n{\n  \"questions\": [\n    {\n      \"question\": \"Question text here?\",\n      \"options\": [\"A) First option\", \"B) Second option\", \"C) Third option\", \"D) Fourth option\"],\n      \"correct\": 0,\n      \"explanation\": \"Brief explanation of why the answer is correct and common misconceptions.\"\n    }\n  ]\n}\n\nRules:\n- Each question must have exactly 4 options labeled A) B) C) D)\n- \"correct\" is the index (0-3) of the correct answer\n- Make questions exam-style and appropriate for UK GCSE\n- Include a mix of question types (recall, application, analysis)\n- Explanations should mention mark scheme points where relevant`;\n} else {\n  // Standard Q&A prompt\n  prompt = `You are an expert GCSE ${request.subject} tutor specialising in UK exam preparation.\n\n## Your Teaching Approach\n- Align explanations with AQA/Edexcel/OCR GCSE specifications\n- Reference mark scheme requirements when relevant (e.g., \"For full marks, you need to...\")\n- Use command word definitions (Explain = state + reason, Evaluate = pros/cons + judgement)\n- Break complex topics into digestible chunks suitable for 14-16 year olds\n- Include exam technique tips where applicable\n- Use real-world examples relatable to teenagers\n\n## Student Context\n- Level: ${level} | Rank: ${rank}\n- XP: ${student.xp_points || 0} points\n- Subject focus: ${request.subject}\n\n## Student Question\n\"${request.question}\"\n\n## Response Guidelines\n1. Start with a direct answer to the question\n2. Provide a clear explanation with examples\n3. If relevant, mention how this might appear in an exam\n4. End with a follow-up question or challenge to reinforce learning\n\nKeep your response focused, encouraging, and under 400 words unless the topic requires more depth.`;\n}\n\nreturn {\n  prompt,\n  action: request.action,\n  studentId: student.student_id || request.studentId,\n  currentXp: student.xp_points || 0,\n  newXp: (student.xp_points || 0) + 10,\n  subject: request.subject,\n  question: request.question\n};"
      },
      "id": "113d112e-eae7-4db8-bb58-8f7d979677df",
      "name": "Prepare Neural Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2560,
        -1776
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE students SET xp_points = $1 WHERE student_id = $2; INSERT INTO interaction_logs (student_id, subject, question, ai_response) VALUES ($3, $4, $5, $6);",
        "options": {
          "queryReplacement": "={{ [$json.newXp, $json.studentId, $json.studentId, $json.subject, $json.question, $json.aiResponse] }}"
        }
      },
      "id": "acb39e1c-ae25-400f-b57f-54de9fabee29",
      "name": "DB Sync (XP & Log)",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -1776,
        -1856
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "id": "5d73f2c7-c2c2-43e8-92b8-5b92e6312acc",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -1264,
        -1728
      ]
    },
    {
      "parameters": {
        "path": "tutor",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "beed975e-db31-4479-bcea-077004742903",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -3152,
        -1680
      ]
    },
    {
      "parameters": {
        "modelId": "gpt-4o-mini",
        "messages": {
          "values": [
            {
              "content": "={{ $json.prompt }}"
            }
          ]
        },
        "simplify": false,
        "options": {
          "maxTokens": 1024,
          "temperature": 0.7
        }
      },
      "id": "b490ea9c-29f9-40e3-9d74-d9a6a7683794",
      "name": "OpenAI",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.6,
      "position": [
        -2368,
        -1776
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// Prepare data for DB logging\nconst prepData = $(\"Prepare Neural Prompt\").first().json;\nconst openAiResponse = $input.first().json;\n\nconst aiResponse = openAiResponse?.choices?.[0]?.message?.content \n  || openAiResponse?.output \n  || 'Unable to generate response';\n\nreturn {\n  studentId: prepData.studentId,\n  newXp: prepData.newXp,\n  subject: prepData.subject,\n  question: prepData.question,\n  aiResponse: aiResponse\n};"
      },
      "id": "9f6a58b7-aed3-4742-ab73-f50cf30ef6dd",
      "name": "Prep DB Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2000,
        -1856
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({\n  \"success\": false,\n  \"error\": \"An error occurred processing your request\",\n  \"code\": \"PROCESSING_ERROR\"\n}) }}",
        "options": {
          "responseCode": 500,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "id": "dc0a448c-1af7-4917-9fa0-05d52b93f719",
      "name": "Error Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -2544,
        -1584
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle OpenAI failure gracefully\nconst prepData = $(\"Prepare Neural Prompt\").first().json;\n\nreturn {\n  studentId: prepData.studentId,\n  newXp: prepData.currentXp, // Don't award XP on failure\n  subject: prepData.subject,\n  question: prepData.question,\n  aiResponse: \"I'm having trouble generating a response right now. Please try again in a moment.\",\n  isError: true\n};"
      },
      "id": "ed8f5945-0ff7-40cd-bef3-fee6671bf5e4",
      "name": "OpenAI Error Handler",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1920,
        -1600
      ]
    },
    {
      "parameters": {
        "jsCode": "const prepData = $('Prep DB Data').first().json;\n\nreturn {\n  success: true,\n  answer: prepData.aiResponse,\n  student: {\n    id: prepData.studentId,\n    xp: prepData.newXp,\n    sessions: Math.floor(prepData.newXp / 10)\n  },\n  meta: {\n    subject: prepData.subject,\n    timestamp: new Date().toISOString()\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1488,
        -1728
      ],
      "id": "774a1258-417d-43bf-812c-a659e219d481",
      "name": "Build Response"
    }
  ],
  "connections": {
    "Parse Request": {
      "main": [
        [
          {
            "node": "Fetch/Create Student",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch/Create Student": {
      "main": [
        [
          {
            "node": "Prepare Neural Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Neural Prompt": {
      "main": [
        [
          {
            "node": "OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI": {
      "main": [
        [
          {
            "node": "Prep DB Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "OpenAI Error Handler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep DB Data": {
      "main": [
        [
          {
            "node": "DB Sync (XP & Log)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Error Handler": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB Sync (XP & Log)": {
      "main": [
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Parse Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "70233b7e-4090-4365-8e7d-0cf0ddaa75de",
  "activeVersionId": "70233b7e-4090-4365-8e7d-0cf0ddaa75de",
  "versionCounter": 185,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2025-12-12T10:12:28.713Z",
      "createdAt": "2025-12-12T10:12:28.713Z",
      "role": "workflow:owner",
      "workflowId": "HgA9JOnX1vsDYaED",
      "projectId": "xsdyFVsct988qLUy",
      "project": {
        "updatedAt": "2025-12-04T20:24:17.537Z",
        "createdAt": "2025-12-04T19:47:48.685Z",
        "id": "xsdyFVsct988qLUy",
        "name": "andrew johnson <andrew.ralston.johnson@gmail.com>",
        "type": "personal",
        "icon": null,
        "description": null,
        "creatorId": "e2485274-7097-4eb5-8502-e39b2308096c"
      }
    }
  ],
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-02-12T19:43:34.679Z",
    "createdAt": "2026-02-12T19:43:23.886Z",
    "versionId": "70233b7e-4090-4365-8e7d-0cf0ddaa75de",
    "workflowId": "HgA9JOnX1vsDYaED",
    "nodes": [
      {
        "parameters": {
          "jsCode": "// 1. Parse incoming data (GET or POST)\nconst input = $input.first().json;\nlet body = {};\ntry {\n  body = (input.query && input.query.data)\n    ? JSON.parse(input.query.data)\n    : (input.body || input);\n} catch (e) {\n  body = {};\n}\n\nconst payload = body.data || body;\n\n// Sanitize and validate inputs\nconst sanitize = (str, maxLen = 500) => {\n  if (typeof str !== 'string') return '';\n  return str.slice(0, maxLen).trim();\n};\n\nconst validateStudentId = (id) => {\n  // Convert to integer, default to 999 for guest\n  if (!id || id === 'guest' || id === 'STU_DEFAULT') return 999;\n  const parsed = parseInt(id);\n  return isNaN(parsed) ? parsed : 999;\n};\n\nconst allowedSubjects = ['Maths', 'English', 'Science', 'Biology', 'Chemistry', 'Physics', 'History', 'Geography', 'Computer Science', 'General'];\nconst normalizeSubject = (subj) => {\n  if (typeof subj !== 'string') return 'General';\n  const match = allowedSubjects.find(s => s.toLowerCase() === subj.toLowerCase().trim());\n  return match || 'General';\n};\n\n// Get action type\nconst action = body.action || 'ask_tutor';\nconst studentId = validateStudentId(body.studentId || body.userId);\nconst question = sanitize(payload.question || 'Hello', 1000);\nconst subject = normalizeSubject(payload.subject || body.subject);\nconst count = parseInt(payload.count) || 5;\nconst difficulty = payload.difficulty || 'medium';\nconst topic = sanitize(payload.topic || '', 200);\n\nif (!question && action !== 'get_history') {\n  throw new Error('Question is required');\n}\n\nreturn {\n  action,\n  studentId,\n  question,\n  subject,\n  count,\n  difficulty,\n  topic,\n  timestamp: new Date().toISOString()\n};"
        },
        "id": "178eda94-2bc8-46f4-84e0-f3f12d8b581a",
        "name": "Parse Request",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -2960,
          -1680
        ]
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "INSERT INTO students (student_id, student_name, xp_points, current_level) VALUES ($1, $2, 0, 1) ON CONFLICT (student_id) DO UPDATE SET student_id = EXCLUDED.student_id RETURNING *;",
          "options": {
            "queryReplacement": "={{ [$json.studentId, 'Student ' + $json.studentId] }}"
          }
        },
        "id": "a24d8f7e-6775-42b9-a5da-21613d7221c5",
        "name": "Fetch/Create Student",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.5,
        "position": [
          -2784,
          -1680
        ],
        "credentials": {
          "postgres": {
            "id": "LadjvFzuiQFYT469",
            "name": "Postgres account 2"
          }
        },
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "jsCode": "// Build GCSE-optimized prompt\nconst student = $input.first().json;\nconst request = $(\"Parse Request\").first().json;\n\n// Determine student rank based on XP\nconst getRank = (xp) => {\n  if (xp >= 1000) return 'Scholar';\n  if (xp >= 500) return 'Apprentice';\n  if (xp >= 100) return 'Learner';\n  return 'Initiate';\n};\n\nconst rank = getRank(student.current_level || 0);\nconst level = student.current_level || 1;\n\n// Check action type\nconst action = request.action || 'ask_tutor';\nlet prompt;\n\nif (action === 'generate_quiz') {\n  // Quiz generation prompt\n  const difficultyDesc = {\n    easy: 'basic recall and understanding questions suitable for foundation tier',\n    medium: 'application and analysis questions suitable for crossover grades 4-6',\n    hard: 'evaluation and synthesis questions suitable for higher tier grades 7-9'\n  };\n  \n  prompt = `Generate exactly ${request.count} GCSE ${request.subject} multiple choice quiz questions${request.topic ? ` about \"${request.topic}\"` : ''}.\nDifficulty: ${request.difficulty} (${difficultyDesc[request.difficulty]})\n\nIMPORTANT: Respond ONLY with valid JSON, no markdown, no explanation. Use this exact format:\n{\n  \"questions\": [\n    {\n      \"question\": \"Question text here?\",\n      \"options\": [\"A) First option\", \"B) Second option\", \"C) Third option\", \"D) Fourth option\"],\n      \"correct\": 0,\n      \"explanation\": \"Brief explanation of why the answer is correct and common misconceptions.\"\n    }\n  ]\n}\n\nRules:\n- Each question must have exactly 4 options labeled A) B) C) D)\n- \"correct\" is the index (0-3) of the correct answer\n- Make questions exam-style and appropriate for UK GCSE\n- Include a mix of question types (recall, application, analysis)\n- Explanations should mention mark scheme points where relevant`;\n} else {\n  // Standard Q&A prompt\n  prompt = `You are an expert GCSE ${request.subject} tutor specialising in UK exam preparation.\n\n## Your Teaching Approach\n- Align explanations with AQA/Edexcel/OCR GCSE specifications\n- Reference mark scheme requirements when relevant (e.g., \"For full marks, you need to...\")\n- Use command word definitions (Explain = state + reason, Evaluate = pros/cons + judgement)\n- Break complex topics into digestible chunks suitable for 14-16 year olds\n- Include exam technique tips where applicable\n- Use real-world examples relatable to teenagers\n\n## Student Context\n- Level: ${level} | Rank: ${rank}\n- XP: ${student.xp_points || 0} points\n- Subject focus: ${request.subject}\n\n## Student Question\n\"${request.question}\"\n\n## Response Guidelines\n1. Start with a direct answer to the question\n2. Provide a clear explanation with examples\n3. If relevant, mention how this might appear in an exam\n4. End with a follow-up question or challenge to reinforce learning\n\nKeep your response focused, encouraging, and under 400 words unless the topic requires more depth.`;\n}\n\nreturn {\n  prompt,\n  action: request.action,\n  studentId: student.student_id || request.studentId,\n  currentXp: student.xp_points || 0,\n  newXp: (student.xp_points || 0) + 10,\n  subject: request.subject,\n  question: request.question\n};"
        },
        "id": "113d112e-eae7-4db8-bb58-8f7d979677df",
        "name": "Prepare Neural Prompt",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -2560,
          -1776
        ]
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "UPDATE students SET xp_points = $1 WHERE student_id = $2; INSERT INTO interaction_logs (student_id, subject, question, ai_response) VALUES ($3, $4, $5, $6);",
          "options": {
            "queryReplacement": "={{ [$json.newXp, $json.studentId, $json.studentId, $json.subject, $json.question, $json.aiResponse] }}"
          }
        },
        "id": "acb39e1c-ae25-400f-b57f-54de9fabee29",
        "name": "DB Sync (XP & Log)",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.5,
        "position": [
          -1776,
          -1856
        ],
        "credentials": {
          "postgres": {
            "id": "LadjvFzuiQFYT469",
            "name": "Postgres account 2"
          }
        },
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "options": {
            "responseHeaders": {
              "entries": [
                {
                  "name": "Access-Control-Allow-Origin",
                  "value": "*"
                },
                {
                  "name": "Content-Type",
                  "value": "application/json"
                }
              ]
            }
          }
        },
        "id": "5d73f2c7-c2c2-43e8-92b8-5b92e6312acc",
        "name": "Respond to Webhook",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          -1264,
          -1728
        ]
      },
      {
        "parameters": {
          "path": "tutor",
          "responseMode": "responseNode",
          "options": {}
        },
        "id": "beed975e-db31-4479-bcea-077004742903",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -3152,
          -1680
        ],
        "webhookId": "b82cd066-12a6-47e2-971f-808653c744af"
      },
      {
        "parameters": {
          "modelId": "gpt-4o-mini",
          "messages": {
            "values": [
              {
                "content": "={{ $json.prompt }}"
              }
            ]
          },
          "simplify": false,
          "options": {
            "maxTokens": 1024,
            "temperature": 0.7
          }
        },
        "id": "b490ea9c-29f9-40e3-9d74-d9a6a7683794",
        "name": "OpenAI",
        "type": "@n8n/n8n-nodes-langchain.openAi",
        "typeVersion": 1.6,
        "position": [
          -2368,
          -1776
        ],
        "credentials": {
          "openAiApi": {
            "id": "sAiUxZnK5nm6DZfX",
            "name": "OpenAi account 2"
          }
        },
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "jsCode": "// Prepare data for DB logging\nconst prepData = $(\"Prepare Neural Prompt\").first().json;\nconst openAiResponse = $input.first().json;\n\nconst aiResponse = openAiResponse?.choices?.[0]?.message?.content \n  || openAiResponse?.output \n  || 'Unable to generate response';\n\nreturn {\n  studentId: prepData.studentId,\n  newXp: prepData.newXp,\n  subject: prepData.subject,\n  question: prepData.question,\n  aiResponse: aiResponse\n};"
        },
        "id": "9f6a58b7-aed3-4742-ab73-f50cf30ef6dd",
        "name": "Prep DB Data",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -2000,
          -1856
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({\n  \"success\": false,\n  \"error\": \"An error occurred processing your request\",\n  \"code\": \"PROCESSING_ERROR\"\n}) }}",
          "options": {
            "responseCode": 500,
            "responseHeaders": {
              "entries": [
                {
                  "name": "Access-Control-Allow-Origin",
                  "value": "*"
                },
                {
                  "name": "Content-Type",
                  "value": "application/json"
                }
              ]
            }
          }
        },
        "id": "dc0a448c-1af7-4917-9fa0-05d52b93f719",
        "name": "Error Response",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          -2544,
          -1584
        ]
      },
      {
        "parameters": {
          "jsCode": "// Handle OpenAI failure gracefully\nconst prepData = $(\"Prepare Neural Prompt\").first().json;\n\nreturn {\n  studentId: prepData.studentId,\n  newXp: prepData.currentXp, // Don't award XP on failure\n  subject: prepData.subject,\n  question: prepData.question,\n  aiResponse: \"I'm having trouble generating a response right now. Please try again in a moment.\",\n  isError: true\n};"
        },
        "id": "ed8f5945-0ff7-40cd-bef3-fee6671bf5e4",
        "name": "OpenAI Error Handler",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -1920,
          -1600
        ]
      },
      {
        "parameters": {
          "jsCode": "const prepData = $('Prep DB Data').first().json;\n\nreturn {\n  success: true,\n  answer: prepData.aiResponse,\n  student: {\n    id: prepData.studentId,\n    xp: prepData.newXp,\n    sessions: Math.floor(prepData.newXp / 10)\n  },\n  meta: {\n    subject: prepData.subject,\n    timestamp: new Date().toISOString()\n  }\n};"
        },
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -1488,
          -1728
        ],
        "id": "774a1258-417d-43bf-812c-a659e219d481",
        "name": "Build Response"
      }
    ],
    "connections": {
      "Parse Request": {
        "main": [
          [
            {
              "node": "Fetch/Create Student",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Fetch/Create Student": {
        "main": [
          [
            {
              "node": "Prepare Neural Prompt",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Error Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prepare Neural Prompt": {
        "main": [
          [
            {
              "node": "OpenAI",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "OpenAI": {
        "main": [
          [
            {
              "node": "Prep DB Data",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "OpenAI Error Handler",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prep DB Data": {
        "main": [
          [
            {
              "node": "DB Sync (XP & Log)",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "OpenAI Error Handler": {
        "main": [
          [
            {
              "node": "Build Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "DB Sync (XP & Log)": {
        "main": [
          [
            {
              "node": "Build Response",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Build Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Webhook": {
        "main": [
          [
            {
              "node": "Parse Request",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Response": {
        "main": [
          [
            {
              "node": "Respond to Webhook",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "andrew johnson",
    "name": "Version 70233b7e",
    "description": "",
    "autosaved": true,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-02-12T19:43:34.677Z",
        "id": 145,
        "workflowId": "HgA9JOnX1vsDYaED",
        "versionId": "70233b7e-4090-4365-8e7d-0cf0ddaa75de",
        "event": "activated",
        "userId": "e2485274-7097-4eb5-8502-e39b2308096c"
      }
    ]
  }
}