{
  "updatedAt": "2026-02-13T20:12:26.242Z",
  "createdAt": "2026-02-13T19:46:50.610Z",
  "id": "kjiwnkYPOsmVt2w2",
  "name": "Practice Question Workflow",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "path": "practice",
        "options": {}
      },
      "id": "0011703f-9cd8-47fe-8eb6-f4819a946fbf",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        -592,
        32
      ]
    },
    {
      "parameters": {
        "jsCode": "// Parse request and validate\nconst query = $input.item.json.query || {};\nconst action = query.action || 'get_questions';\nconst studentId = parseInt(query.studentId) || 999;\nconst topicId = parseInt(query.topicId);\nconst questionId = parseInt(query.questionId);\nconst difficulty = query.difficulty || 'medium';\nconst count = Math.min(parseInt(query.count) || 5, 20);\nconst questionType = query.questionType || null;\n\n// For submit action\nconst studentAnswer = query.answer || '';\nconst timeSpent = parseInt(query.timeSpent) || 0;\nconst hintUsed = query.hintUsed === 'true' || query.hintUsed === true;\n\n// Validate required fields\nif (action === 'get_questions' && !topicId) {\n  return {\n    json: {\n      error: true,\n      message: 'topicId is required for get_questions action'\n    }\n  };\n}\n\nif (action === 'submit_answer' && (!questionId || !studentAnswer)) {\n  return {\n    json: {\n      error: true,\n      message: 'questionId and answer are required for submit_answer action'\n    }\n  };\n}\n\n// Sanitize inputs\nconst sanitize = (str, maxLength = 5000) => {\n  if (typeof str !== 'string') return '';\n  return str.slice(0, maxLength).trim();\n};\n\nreturn {\n  json: {\n    action,\n    studentId,\n    topicId: topicId || null,\n    questionId: questionId || null,\n    difficulty: ['foundation', 'medium', 'higher', 'challenge'].includes(difficulty) ? difficulty : 'medium',\n    count,\n    questionType,\n    studentAnswer: sanitize(studentAnswer, 5000),\n    timeSpent,\n    hintUsed,\n    timestamp: new Date().toISOString()\n  }\n};"
      },
      "id": "608e3d2d-0efa-473f-af05-62daaaf54da5",
      "name": "Parse Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -384,
        32
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.error }}",
              "value2": "true"
            }
          ]
        }
      },
      "id": "f1636cc3-f156-4dec-809b-9f2cf15b42d9",
      "name": "If Error",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -192,
        32
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "6a9d10ff-d665-484c-ae66-127e089d8672",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        16,
        -96
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.action }}",
              "value2": "get_questions"
            }
          ]
        }
      },
      "id": "842807de-91a4-4260-89da-0fc203e6da9f",
      "name": "If Get Questions",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -32,
        144
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.action }}",
              "value2": "submit_answer"
            }
          ]
        }
      },
      "id": "0084b0c7-3bcf-401c-91ed-a0cfb6cbf2b6",
      "name": "If Submit Answer",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        144,
        288
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  pq.question_id,\n  pq.question_type,\n  pq.difficulty,\n  pq.question_text,\n  pq.question_data,\n  pq.marks_available,\n  pq.answer_type,\n  pq.mark_scheme,\n  pq.working_required,\n  pq.command_word,\n  pq.hint_text,\n  pq.specification_ref,\n  pq.exam_board,\n  t.topic_name\nFROM practice_questions pq\nJOIN topics t ON pq.topic_id = t.topic_id\nWHERE pq.topic_id = $1 \n  AND pq.difficulty = $2\n  AND ($3::varchar IS NULL OR pq.question_type = $3)\nORDER BY RANDOM()\nLIMIT $4",
        "options": {}
      },
      "id": "aa1160ce-a7a7-4b51-bd90-0fd5742d2d29",
      "name": "Fetch Questions",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        224,
        32
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  pq.question_id,\n  pq.question_text,\n  pq.marks_available,\n  pq.correct_answer,\n  pq.acceptable_answers,\n  pq.mark_scheme,\n  pq.command_word,\n  pq.explanation,\n  pq.common_errors,\n  t.topic_name\nFROM practice_questions pq\nJOIN topics t ON pq.topic_id = t.topic_id\nWHERE pq.question_id = $1",
        "options": {}
      },
      "id": "05f951ca-cb2b-49b3-ac82-42b1270918cb",
      "name": "Fetch Question Details",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        336,
        192
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Prepare AI grading prompt\nconst question = $input.first().json;\nconst request = $('Parse Request').item.json;\n\nif (!question || !question.question_id) {\n  return {\n    json: {\n      error: true,\n      message: 'Question not found'\n    }\n  };\n}\n\n// Parse mark scheme if string\nlet markScheme = question.mark_scheme;\nif (typeof markScheme === 'string') {\n  try {\n    markScheme = JSON.parse(markScheme);\n  } catch (e) {\n    markScheme = { total_marks: question.marks_available };\n  }\n}\n\n// Build grading prompt\nconst gradingPrompt = `You are an expert GCSE examiner. Grade the following student answer according to the mark scheme.\n\n**Question:** ${question.question_text}\n\n**Command Word:** ${question.command_word || 'Answer'}\n\n**Marks Available:** ${question.marks_available}\n\n**Mark Scheme:**\n${JSON.stringify(markScheme, null, 2)}\n\n**Model Answer:** ${question.correct_answer}\n\n**Acceptable Answers:** ${(question.acceptable_answers || []).join(', ') || 'See mark scheme'}\n\n**Student Answer:** ${request.studentAnswer}\n\n---\n\n**Your Task:**\n1. Award marks based on the mark scheme criteria\n2. Identify which marking points were achieved\n3. Provide specific, constructive feedback\n4. Suggest improvements aligned with the mark scheme\n5. Note any common errors from: ${(question.common_errors || []).join(', ')}\n\n**Respond in JSON format:**\n{\n  \"marks_awarded\": <number>,\n  \"is_correct\": <boolean>,\n  \"marking_points\": [\n    {\"point\": \"...\", \"awarded\": 1/0, \"comment\": \"...\"}\n  ],\n  \"overall_feedback\": \"...\",\n  \"strengths\": [\"...\"],\n  \"improvements\": [\"...\"],\n  \"next_steps\": \"...\"\n}`;\n\nreturn {\n  json: {\n    prompt: gradingPrompt,\n    questionId: question.question_id,\n    marksAvailable: question.marks_available,\n    studentAnswer: request.studentAnswer,\n    studentId: request.studentId,\n    timeSpent: request.timeSpent,\n    hintUsed: request.hintUsed\n  }\n};"
      },
      "id": "299a6d93-308f-4791-bdad-f033706c38b9",
      "name": "Prepare Grading Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        512,
        192
      ]
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4.1-mini",
          "mode": "list",
          "cachedResultName": "GPT-4.1-MINI"
        },
        "messages": {
          "values": [
            {}
          ]
        },
        "options": {
          "maxTokens": 1000,
          "temperature": 0.3
        }
      },
      "id": "aa5410e1-9363-4884-b3fa-e92e6375a085",
      "name": "OpenAI Grade Answer",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.4,
      "position": [
        688,
        192
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Parse AI grading response\nconst aiResponse = $input.first().json.message?.content || $input.first().json.text || '';\nconst questionData = $('Prepare Grading Prompt').item.json;\n\nlet feedback = {};\ntry {\n  // Try to parse JSON response\n  const jsonMatch = aiResponse.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    feedback = JSON.parse(jsonMatch[0]);\n  } else {\n    // Fallback parsing\n    feedback = {\n      marks_awarded: 0,\n      is_correct: false,\n      overall_feedback: aiResponse,\n      marking_points: [],\n      strengths: [],\n      improvements: ['Could not parse detailed feedback']\n    };\n  }\n} catch (e) {\n  feedback = {\n    marks_awarded: 0,\n    is_correct: false,\n    overall_feedback: aiResponse.slice(0, 500),\n    marking_points: [],\n    strengths: [],\n    improvements: ['Error parsing feedback']\n  };\n}\n\nreturn {\n  json: {\n    questionId: questionData.questionId,\n    studentId: questionData.studentId,\n    studentAnswer: questionData.studentAnswer,\n    marksAwarded: feedback.marks_awarded || 0,\n    marksPossible: questionData.marksAvailable,\n    isCorrect: feedback.is_correct || false,\n    feedback: feedback,\n    timeSpent: questionData.timeSpent,\n    hintUsed: questionData.hintUsed\n  }\n};"
      },
      "id": "88cc0551-f18d-453a-aa03-26e8996c99ed",
      "name": "Parse Grading Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1024,
        192
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Insert practice attempt\nINSERT INTO student_practice_attempts (\n  student_id,\n  question_id,\n  student_answer,\n  marks_awarded,\n  marks_possible,\n  is_correct,\n  feedback,\n  time_taken_seconds,\n  hint_used,\n  attempted_at\n)\nVALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, NOW())\nRETURNING attempt_id;\n\n-- Update student stats\nUPDATE students SET\n  total_practice_questions = total_practice_questions + 1,\n  xp_points = xp_points + $10,\n  last_activity_date = CURRENT_DATE,\n  updated_at = NOW()\nWHERE student_id = $1;",
        "options": {}
      },
      "id": "b36ad59d-4775-41e4-a411-a7fd0ea00bb7",
      "name": "Save Attempt & Update XP",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        1216,
        192
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build questions response\nconst questions = $input.all().map(item => item.json);\n\n// Parse JSONB fields\nconst parseJson = (field) => {\n  if (!field) return null;\n  if (typeof field === 'string') {\n    try {\n      return JSON.parse(field);\n    } catch (e) {\n      return null;\n    }\n  }\n  return field;\n};\n\nreturn {\n  json: {\n    success: true,\n    questions: questions.map(q => ({\n      id: q.question_id,\n      type: q.question_type,\n      difficulty: q.difficulty,\n      text: q.question_text,\n      data: parseJson(q.question_data),\n      marksAvailable: q.marks_available,\n      answerType: q.answer_type,\n      workingRequired: q.working_required,\n      commandWord: q.command_word,\n      hint: q.hint_text,\n      specificationRef: q.specification_ref,\n      examBoard: q.exam_board,\n      topic: q.topic_name\n    })),\n    meta: {\n      totalQuestions: questions.length,\n      difficulty: $('Parse Request').item.json.difficulty,\n      topicId: $('Parse Request').item.json.topicId,\n      timestamp: new Date().toISOString()\n    }\n  }\n};"
      },
      "id": "5873305d-c02e-4469-b5d2-ac175376e71d",
      "name": "Build Questions Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        416,
        32
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build submission response\nconst result = $input.first().json;\nconst savedData = $input.first().json;\n\nreturn {\n  json: {\n    success: true,\n    result: {\n      attemptId: savedData.attempt_id,\n      marksAwarded: result.marksAwarded,\n      marksPossible: result.marksPossible,\n      isCorrect: result.isCorrect,\n      percentageScore: Math.round((result.marksAwarded / result.marksPossible) * 100),\n      feedback: result.feedback\n    },\n    xpGained: result.isCorrect ? 15 : 5,\n    encouragement: result.isCorrect \n      ? '\ud83c\udf89 Excellent work! Keep it up!' \n      : '\ud83d\udcaa Good effort! Review the feedback and try similar questions.',\n    meta: {\n      timestamp: new Date().toISOString()\n    }\n  }\n};"
      },
      "id": "7ed535ca-3d82-48b9-b818-fa0c0890531b",
      "name": "Build Submission Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1424,
        192
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "a5328214-84a3-44ea-b3a4-03d64dc3f8ec",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1616,
        112
      ]
    },
    {
      "parameters": {
        "jsCode": "return {\n  json: {\n    success: false,\n    error: 'Unknown action',\n    validActions: ['get_questions', 'submit_answer']\n  }\n};"
      },
      "id": "47f7aa09-cea2-46cd-87fb-26fef780ca6b",
      "name": "Unknown Action",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        416,
        464
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Parse Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Request": {
      "main": [
        [
          {
            "node": "If Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Error": {
      "main": [
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If Get Questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Get Questions": {
      "main": [
        [
          {
            "node": "Fetch Questions",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If Submit Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Submit Answer": {
      "main": [
        [
          {
            "node": "Fetch Question Details",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unknown Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Questions": {
      "main": [
        [
          {
            "node": "Build Questions Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Question Details": {
      "main": [
        [
          {
            "node": "Prepare Grading Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Grading Prompt": {
      "main": [
        [
          {
            "node": "OpenAI Grade Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Grade Answer": {
      "main": [
        [
          {
            "node": "Parse Grading Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Grading Result": {
      "main": [
        [
          {
            "node": "Save Attempt & Update XP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Attempt & Update XP": {
      "main": [
        [
          {
            "node": "Build Submission Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Questions Response": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Submission Response": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unknown Action": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "2a9f56c1-236d-442b-81af-be2d361d16fc",
  "activeVersionId": "2a9f56c1-236d-442b-81af-be2d361d16fc",
  "versionCounter": 18,
  "triggerCount": 1,
  "shared": [
    {
      "updatedAt": "2026-02-13T19:46:50.610Z",
      "createdAt": "2026-02-13T19:46:50.610Z",
      "role": "workflow:owner",
      "workflowId": "kjiwnkYPOsmVt2w2",
      "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-13T20:03:41.124Z",
    "createdAt": "2026-02-13T20:03:34.597Z",
    "versionId": "2a9f56c1-236d-442b-81af-be2d361d16fc",
    "workflowId": "kjiwnkYPOsmVt2w2",
    "nodes": [
      {
        "parameters": {
          "path": "practice",
          "options": {}
        },
        "id": "0011703f-9cd8-47fe-8eb6-f4819a946fbf",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 1.1,
        "position": [
          -592,
          32
        ],
        "webhookId": "practice-api"
      },
      {
        "parameters": {
          "jsCode": "// Parse request and validate\nconst query = $input.item.json.query || {};\nconst action = query.action || 'get_questions';\nconst studentId = parseInt(query.studentId) || 999;\nconst topicId = parseInt(query.topicId);\nconst questionId = parseInt(query.questionId);\nconst difficulty = query.difficulty || 'medium';\nconst count = Math.min(parseInt(query.count) || 5, 20);\nconst questionType = query.questionType || null;\n\n// For submit action\nconst studentAnswer = query.answer || '';\nconst timeSpent = parseInt(query.timeSpent) || 0;\nconst hintUsed = query.hintUsed === 'true' || query.hintUsed === true;\n\n// Validate required fields\nif (action === 'get_questions' && !topicId) {\n  return {\n    json: {\n      error: true,\n      message: 'topicId is required for get_questions action'\n    }\n  };\n}\n\nif (action === 'submit_answer' && (!questionId || !studentAnswer)) {\n  return {\n    json: {\n      error: true,\n      message: 'questionId and answer are required for submit_answer action'\n    }\n  };\n}\n\n// Sanitize inputs\nconst sanitize = (str, maxLength = 5000) => {\n  if (typeof str !== 'string') return '';\n  return str.slice(0, maxLength).trim();\n};\n\nreturn {\n  json: {\n    action,\n    studentId,\n    topicId: topicId || null,\n    questionId: questionId || null,\n    difficulty: ['foundation', 'medium', 'higher', 'challenge'].includes(difficulty) ? difficulty : 'medium',\n    count,\n    questionType,\n    studentAnswer: sanitize(studentAnswer, 5000),\n    timeSpent,\n    hintUsed,\n    timestamp: new Date().toISOString()\n  }\n};"
        },
        "id": "608e3d2d-0efa-473f-af05-62daaaf54da5",
        "name": "Parse Request",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -384,
          32
        ]
      },
      {
        "parameters": {
          "conditions": {
            "string": [
              {
                "value1": "={{ $json.error }}",
                "value2": "true"
              }
            ]
          }
        },
        "id": "f1636cc3-f156-4dec-809b-9f2cf15b42d9",
        "name": "If Error",
        "type": "n8n-nodes-base.if",
        "typeVersion": 1,
        "position": [
          -192,
          32
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ $json }}",
          "options": {
            "responseHeaders": {
              "entries": [
                {
                  "name": "Access-Control-Allow-Origin",
                  "value": "*"
                }
              ]
            }
          }
        },
        "id": "6a9d10ff-d665-484c-ae66-127e089d8672",
        "name": "Respond Error",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1,
        "position": [
          16,
          -96
        ]
      },
      {
        "parameters": {
          "conditions": {
            "string": [
              {
                "value1": "={{ $json.action }}",
                "value2": "get_questions"
              }
            ]
          }
        },
        "id": "842807de-91a4-4260-89da-0fc203e6da9f",
        "name": "If Get Questions",
        "type": "n8n-nodes-base.if",
        "typeVersion": 1,
        "position": [
          -32,
          144
        ]
      },
      {
        "parameters": {
          "conditions": {
            "string": [
              {
                "value1": "={{ $json.action }}",
                "value2": "submit_answer"
              }
            ]
          }
        },
        "id": "0084b0c7-3bcf-401c-91ed-a0cfb6cbf2b6",
        "name": "If Submit Answer",
        "type": "n8n-nodes-base.if",
        "typeVersion": 1,
        "position": [
          144,
          288
        ]
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "SELECT \n  pq.question_id,\n  pq.question_type,\n  pq.difficulty,\n  pq.question_text,\n  pq.question_data,\n  pq.marks_available,\n  pq.answer_type,\n  pq.mark_scheme,\n  pq.working_required,\n  pq.command_word,\n  pq.hint_text,\n  pq.specification_ref,\n  pq.exam_board,\n  t.topic_name\nFROM practice_questions pq\nJOIN topics t ON pq.topic_id = t.topic_id\nWHERE pq.topic_id = $1 \n  AND pq.difficulty = $2\n  AND ($3::varchar IS NULL OR pq.question_type = $3)\nORDER BY RANDOM()\nLIMIT $4",
          "options": {}
        },
        "id": "aa1160ce-a7a7-4b51-bd90-0fd5742d2d29",
        "name": "Fetch Questions",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.4,
        "position": [
          224,
          32
        ],
        "credentials": {
          "postgres": {
            "id": "LadjvFzuiQFYT469",
            "name": "Postgres account 2"
          }
        }
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "SELECT \n  pq.question_id,\n  pq.question_text,\n  pq.marks_available,\n  pq.correct_answer,\n  pq.acceptable_answers,\n  pq.mark_scheme,\n  pq.command_word,\n  pq.explanation,\n  pq.common_errors,\n  t.topic_name\nFROM practice_questions pq\nJOIN topics t ON pq.topic_id = t.topic_id\nWHERE pq.question_id = $1",
          "options": {}
        },
        "id": "05f951ca-cb2b-49b3-ac82-42b1270918cb",
        "name": "Fetch Question Details",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.4,
        "position": [
          336,
          192
        ],
        "credentials": {
          "postgres": {
            "id": "LadjvFzuiQFYT469",
            "name": "Postgres account 2"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Prepare AI grading prompt\nconst question = $input.first().json;\nconst request = $('Parse Request').item.json;\n\nif (!question || !question.question_id) {\n  return {\n    json: {\n      error: true,\n      message: 'Question not found'\n    }\n  };\n}\n\n// Parse mark scheme if string\nlet markScheme = question.mark_scheme;\nif (typeof markScheme === 'string') {\n  try {\n    markScheme = JSON.parse(markScheme);\n  } catch (e) {\n    markScheme = { total_marks: question.marks_available };\n  }\n}\n\n// Build grading prompt\nconst gradingPrompt = `You are an expert GCSE examiner. Grade the following student answer according to the mark scheme.\n\n**Question:** ${question.question_text}\n\n**Command Word:** ${question.command_word || 'Answer'}\n\n**Marks Available:** ${question.marks_available}\n\n**Mark Scheme:**\n${JSON.stringify(markScheme, null, 2)}\n\n**Model Answer:** ${question.correct_answer}\n\n**Acceptable Answers:** ${(question.acceptable_answers || []).join(', ') || 'See mark scheme'}\n\n**Student Answer:** ${request.studentAnswer}\n\n---\n\n**Your Task:**\n1. Award marks based on the mark scheme criteria\n2. Identify which marking points were achieved\n3. Provide specific, constructive feedback\n4. Suggest improvements aligned with the mark scheme\n5. Note any common errors from: ${(question.common_errors || []).join(', ')}\n\n**Respond in JSON format:**\n{\n  \"marks_awarded\": <number>,\n  \"is_correct\": <boolean>,\n  \"marking_points\": [\n    {\"point\": \"...\", \"awarded\": 1/0, \"comment\": \"...\"}\n  ],\n  \"overall_feedback\": \"...\",\n  \"strengths\": [\"...\"],\n  \"improvements\": [\"...\"],\n  \"next_steps\": \"...\"\n}`;\n\nreturn {\n  json: {\n    prompt: gradingPrompt,\n    questionId: question.question_id,\n    marksAvailable: question.marks_available,\n    studentAnswer: request.studentAnswer,\n    studentId: request.studentId,\n    timeSpent: request.timeSpent,\n    hintUsed: request.hintUsed\n  }\n};"
        },
        "id": "299a6d93-308f-4791-bdad-f033706c38b9",
        "name": "Prepare Grading Prompt",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          512,
          192
        ]
      },
      {
        "parameters": {
          "modelId": {
            "__rl": true,
            "value": "gpt-4.1-mini",
            "mode": "list",
            "cachedResultName": "GPT-4.1-MINI"
          },
          "messages": {
            "values": [
              {}
            ]
          },
          "options": {
            "maxTokens": 1000,
            "temperature": 0.3
          }
        },
        "id": "aa5410e1-9363-4884-b3fa-e92e6375a085",
        "name": "OpenAI Grade Answer",
        "type": "@n8n/n8n-nodes-langchain.openAi",
        "typeVersion": 1.4,
        "position": [
          688,
          192
        ],
        "credentials": {
          "openAiApi": {
            "id": "sAiUxZnK5nm6DZfX",
            "name": "OpenAi account 2"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Parse AI grading response\nconst aiResponse = $input.first().json.message?.content || $input.first().json.text || '';\nconst questionData = $('Prepare Grading Prompt').item.json;\n\nlet feedback = {};\ntry {\n  // Try to parse JSON response\n  const jsonMatch = aiResponse.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    feedback = JSON.parse(jsonMatch[0]);\n  } else {\n    // Fallback parsing\n    feedback = {\n      marks_awarded: 0,\n      is_correct: false,\n      overall_feedback: aiResponse,\n      marking_points: [],\n      strengths: [],\n      improvements: ['Could not parse detailed feedback']\n    };\n  }\n} catch (e) {\n  feedback = {\n    marks_awarded: 0,\n    is_correct: false,\n    overall_feedback: aiResponse.slice(0, 500),\n    marking_points: [],\n    strengths: [],\n    improvements: ['Error parsing feedback']\n  };\n}\n\nreturn {\n  json: {\n    questionId: questionData.questionId,\n    studentId: questionData.studentId,\n    studentAnswer: questionData.studentAnswer,\n    marksAwarded: feedback.marks_awarded || 0,\n    marksPossible: questionData.marksAvailable,\n    isCorrect: feedback.is_correct || false,\n    feedback: feedback,\n    timeSpent: questionData.timeSpent,\n    hintUsed: questionData.hintUsed\n  }\n};"
        },
        "id": "88cc0551-f18d-453a-aa03-26e8996c99ed",
        "name": "Parse Grading Result",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1024,
          192
        ]
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "-- Insert practice attempt\nINSERT INTO student_practice_attempts (\n  student_id,\n  question_id,\n  student_answer,\n  marks_awarded,\n  marks_possible,\n  is_correct,\n  feedback,\n  time_taken_seconds,\n  hint_used,\n  attempted_at\n)\nVALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, NOW())\nRETURNING attempt_id;\n\n-- Update student stats\nUPDATE students SET\n  total_practice_questions = total_practice_questions + 1,\n  xp_points = xp_points + $10,\n  last_activity_date = CURRENT_DATE,\n  updated_at = NOW()\nWHERE student_id = $1;",
          "options": {}
        },
        "id": "b36ad59d-4775-41e4-a411-a7fd0ea00bb7",
        "name": "Save Attempt & Update XP",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.4,
        "position": [
          1216,
          192
        ],
        "credentials": {
          "postgres": {
            "id": "LadjvFzuiQFYT469",
            "name": "Postgres account 2"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Build questions response\nconst questions = $input.all().map(item => item.json);\n\n// Parse JSONB fields\nconst parseJson = (field) => {\n  if (!field) return null;\n  if (typeof field === 'string') {\n    try {\n      return JSON.parse(field);\n    } catch (e) {\n      return null;\n    }\n  }\n  return field;\n};\n\nreturn {\n  json: {\n    success: true,\n    questions: questions.map(q => ({\n      id: q.question_id,\n      type: q.question_type,\n      difficulty: q.difficulty,\n      text: q.question_text,\n      data: parseJson(q.question_data),\n      marksAvailable: q.marks_available,\n      answerType: q.answer_type,\n      workingRequired: q.working_required,\n      commandWord: q.command_word,\n      hint: q.hint_text,\n      specificationRef: q.specification_ref,\n      examBoard: q.exam_board,\n      topic: q.topic_name\n    })),\n    meta: {\n      totalQuestions: questions.length,\n      difficulty: $('Parse Request').item.json.difficulty,\n      topicId: $('Parse Request').item.json.topicId,\n      timestamp: new Date().toISOString()\n    }\n  }\n};"
        },
        "id": "5873305d-c02e-4469-b5d2-ac175376e71d",
        "name": "Build Questions Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          416,
          32
        ]
      },
      {
        "parameters": {
          "jsCode": "// Build submission response\nconst result = $input.first().json;\nconst savedData = $input.first().json;\n\nreturn {\n  json: {\n    success: true,\n    result: {\n      attemptId: savedData.attempt_id,\n      marksAwarded: result.marksAwarded,\n      marksPossible: result.marksPossible,\n      isCorrect: result.isCorrect,\n      percentageScore: Math.round((result.marksAwarded / result.marksPossible) * 100),\n      feedback: result.feedback\n    },\n    xpGained: result.isCorrect ? 15 : 5,\n    encouragement: result.isCorrect \n      ? '\ud83c\udf89 Excellent work! Keep it up!' \n      : '\ud83d\udcaa Good effort! Review the feedback and try similar questions.',\n    meta: {\n      timestamp: new Date().toISOString()\n    }\n  }\n};"
        },
        "id": "7ed535ca-3d82-48b9-b818-fa0c0890531b",
        "name": "Build Submission Response",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1424,
          192
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ $json }}",
          "options": {
            "responseHeaders": {
              "entries": [
                {
                  "name": "Access-Control-Allow-Origin",
                  "value": "*"
                }
              ]
            }
          }
        },
        "id": "a5328214-84a3-44ea-b3a4-03d64dc3f8ec",
        "name": "Respond Success",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1,
        "position": [
          1616,
          112
        ]
      },
      {
        "parameters": {
          "jsCode": "return {\n  json: {\n    success: false,\n    error: 'Unknown action',\n    validActions: ['get_questions', 'submit_answer']\n  }\n};"
        },
        "id": "47f7aa09-cea2-46cd-87fb-26fef780ca6b",
        "name": "Unknown Action",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          416,
          464
        ]
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "Parse Request",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parse Request": {
        "main": [
          [
            {
              "node": "If Error",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "If Error": {
        "main": [
          [
            {
              "node": "Respond Error",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "If Get Questions",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "If Get Questions": {
        "main": [
          [
            {
              "node": "Fetch Questions",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "If Submit Answer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "If Submit Answer": {
        "main": [
          [
            {
              "node": "Fetch Question Details",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Unknown Action",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Fetch Questions": {
        "main": [
          [
            {
              "node": "Build Questions Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Fetch Question Details": {
        "main": [
          [
            {
              "node": "Prepare Grading Prompt",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Prepare Grading Prompt": {
        "main": [
          [
            {
              "node": "OpenAI Grade Answer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "OpenAI Grade Answer": {
        "main": [
          [
            {
              "node": "Parse Grading Result",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Parse Grading Result": {
        "main": [
          [
            {
              "node": "Save Attempt & Update XP",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Save Attempt & Update XP": {
        "main": [
          [
            {
              "node": "Build Submission Response",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Questions Response": {
        "main": [
          [
            {
              "node": "Respond Success",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Submission Response": {
        "main": [
          [
            {
              "node": "Respond Success",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Unknown Action": {
        "main": [
          [
            {
              "node": "Respond Success",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "andrew johnson",
    "name": "Version 2a9f56c1",
    "description": "",
    "autosaved": true,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-02-13T20:03:41.123Z",
        "id": 147,
        "workflowId": "kjiwnkYPOsmVt2w2",
        "versionId": "2a9f56c1-236d-442b-81af-be2d361d16fc",
        "event": "activated",
        "userId": "e2485274-7097-4eb5-8502-e39b2308096c"
      }
    ]
  }
}