This workflow follows the OpenAI → Postgres 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 →
{
"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"
}
]
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
openAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Virtual Tutor Core - DB Edition (Secured). Uses postgres, openAi. Webhook trigger; 11 nodes.
Source: https://github.com/AndyJay72/VenueDesk/blob/main/n8n-workflows/HgA9JOnX1vsDYaED.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Eu Clara – Funil Kiwify Completo. Uses postgres, openAi, httpRequest, gmail. Webhook trigger; 70 nodes.
Lua Nova - Sistema Completo. Uses postgres, httpRequest, openAi. Webhook trigger; 55 nodes.
User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 37 nodes.
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.