AutomationFlowsAI & RAG › Practice Question Workflow

Practice Question Workflow

Practice Question Workflow. Uses postgres, openAi. Webhook trigger; 16 nodes.

Webhook trigger★★★★☆ complexityAI-powered16 nodesPostgresOpenAI
AI & RAG Trigger: Webhook Nodes: 16 Complexity: ★★★★☆ AI nodes: yes Added:

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 →

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

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.

Pro

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

About this workflow

Practice Question Workflow. Uses postgres, openAi. Webhook trigger; 16 nodes.

Source: https://github.com/AndyJay72/VenueDesk/blob/5e4b3845269585065305ddea8df28372d5aa9280/n8n-workflows/kjiwnkYPOsmVt2w2.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Eu Clara – Funil Kiwify Completo. Uses postgres, openAi, httpRequest, gmail. Webhook trigger; 70 nodes.

Postgres, OpenAI, HTTP Request +1
AI & RAG

Lua Nova - Sistema Completo. Uses postgres, httpRequest, openAi. Webhook trigger; 55 nodes.

Postgres, HTTP Request, OpenAI
AI & RAG

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

Postgres, HTTP Request, OpenAI +2
AI & RAG

Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 37 nodes.

Start, Postgres, OpenAI +4
AI & RAG

Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.

Start, Postgres, OpenAI +4