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-13T20:12:28.086Z",
"createdAt": "2026-02-13T19:47:57.154Z",
"id": "mxDwUMJ4LvfvTcKK",
"name": "Interactive Lesson Workflow",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"path": "lesson",
"options": {}
},
"id": "f949a577-7d7e-4d4c-9ad8-947788305b8f",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
-608,
128
]
},
{
"parameters": {
"jsCode": "// Parse request and validate\nconst query = $input.item.json.query || {};\nconst action = query.action || 'get_lesson';\nconst studentId = parseInt(query.studentId) || 999;\nconst lessonId = parseInt(query.lessonId);\nconst topicId = parseInt(query.topicId);\nconst completionPercentage = parseInt(query.completionPercentage) || 100;\nconst confidenceRating = parseInt(query.confidenceRating);\nconst timeSpent = parseInt(query.timeSpent) || 0;\n\n// Validate required fields based on action\nif (action === 'get_lesson' && !lessonId && !topicId) {\n return {\n json: {\n error: true,\n message: 'Either lessonId or topicId is required',\n action\n }\n };\n}\n\nif (action === 'mark_complete' && !lessonId) {\n return {\n json: {\n error: true,\n message: 'lessonId is required for mark_complete action',\n action\n }\n };\n}\n\n// Sanitize inputs\nconst sanitize = (str, maxLength = 500) => {\n if (typeof str !== 'string') return '';\n return str.slice(0, maxLength).trim();\n};\n\nconst notes = sanitize(query.notes || '', 1000);\n\nreturn {\n json: {\n action,\n studentId,\n lessonId: lessonId || null,\n topicId: topicId || null,\n completionPercentage,\n confidenceRating: confidenceRating || null,\n timeSpent,\n notes,\n timestamp: new Date().toISOString()\n }\n};"
},
"id": "7a807867-b172-4c39-8d71-599b74913b5f",
"name": "Parse Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-400,
128
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.error }}",
"value2": "true"
}
]
}
},
"id": "42f2e262-714f-4ac8-a26d-60534ec1f826",
"name": "If Error",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-208,
128
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET, POST, OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type"
}
]
}
}
},
"id": "40e29a7c-6089-4aec-a93c-0dea3497559d",
"name": "Respond Error",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
0,
0
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "get_lesson"
}
]
}
},
"id": "8d6fb72f-bb74-498c-8f9d-8db5a4824979",
"name": "If Get Lesson",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
0,
240
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "get_lessons_by_topic"
}
]
}
},
"id": "2e6ecc99-4491-48e4-8cc1-25f93cd4bed3",
"name": "If Get Lessons By Topic",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
208,
400
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "mark_complete"
}
]
}
},
"id": "cfc0e340-5c9c-4e0b-98a5-f4e03b912c11",
"name": "If Mark Complete",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
400,
560
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n l.lesson_id,\n l.lesson_title,\n l.lesson_type,\n l.lesson_number,\n l.content_blocks,\n l.learning_objectives,\n l.success_criteria,\n l.estimated_minutes,\n l.difficulty_rating,\n l.mark_scheme_notes,\n l.exam_tips,\n l.common_mistakes,\n t.topic_name,\n t.topic_id,\n s.subject_name,\n s.subject_id,\n slp.status as progress_status,\n slp.completion_percentage,\n slp.confidence_rating,\n slp.time_spent_minutes,\n slp.notes as student_notes,\n slp.bookmarked,\n slp.last_accessed_at\nFROM lessons l\nJOIN topics t ON l.topic_id = t.topic_id\nJOIN subjects s ON t.subject_id = s.subject_id\nLEFT JOIN student_lesson_progress slp ON l.lesson_id = slp.lesson_id AND slp.student_id = $1\nWHERE l.lesson_id = $2 AND l.is_published = true",
"options": {}
},
"id": "39318f59-0ed6-4f58-9b6b-95aec31036c9",
"name": "Fetch Lesson",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
208,
128
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n l.lesson_id,\n l.lesson_title,\n l.lesson_type,\n l.lesson_number,\n l.estimated_minutes,\n l.difficulty_rating,\n slp.status as progress_status,\n slp.completion_percentage,\n slp.confidence_rating\nFROM lessons l\nLEFT JOIN student_lesson_progress slp ON l.lesson_id = slp.lesson_id AND slp.student_id = $1\nWHERE l.topic_id = $2 AND l.is_published = true\nORDER BY l.lesson_number ASC",
"options": {}
},
"id": "2efe68fd-26c6-400b-9ff4-490442d34b06",
"name": "Fetch Lessons By Topic",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
400,
288
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Update or insert lesson progress\nINSERT INTO student_lesson_progress (\n student_id, \n lesson_id, \n status, \n completion_percentage, \n time_spent_minutes,\n confidence_rating,\n notes,\n started_at,\n completed_at,\n last_accessed_at\n)\nVALUES (\n $1, $2, $3, $4, $5, $6, $7,\n COALESCE((SELECT started_at FROM student_lesson_progress WHERE student_id = $1 AND lesson_id = $2), NOW()),\n CASE WHEN $4 >= 100 THEN NOW() ELSE NULL END,\n NOW()\n)\nON CONFLICT (student_id, lesson_id)\nDO UPDATE SET\n status = CASE \n WHEN $4 >= 100 THEN 'completed'\n WHEN $4 > 0 THEN 'in_progress'\n ELSE student_lesson_progress.status\n END,\n completion_percentage = $4,\n time_spent_minutes = student_lesson_progress.time_spent_minutes + $5,\n confidence_rating = COALESCE($6, student_lesson_progress.confidence_rating),\n notes = COALESCE($7, student_lesson_progress.notes),\n completed_at = CASE \n WHEN $4 >= 100 AND student_lesson_progress.completed_at IS NULL THEN NOW()\n ELSE student_lesson_progress.completed_at\n END,\n last_accessed_at = NOW();\n\n-- Update student stats\nUPDATE students SET\n total_lessons_completed = (\n SELECT COUNT(*) FROM student_lesson_progress \n WHERE student_id = $1 AND status = 'completed'\n ),\n xp_points = xp_points + CASE WHEN $4 >= 100 THEN 20 ELSE 5 END,\n last_activity_date = CURRENT_DATE,\n updated_at = NOW()\nWHERE student_id = $1;\n\n-- Return updated progress\nSELECT \n slp.*,\n s.xp_points,\n s.current_level,\n s.total_lessons_completed\nFROM student_lesson_progress slp\nJOIN students s ON slp.student_id = s.student_id\nWHERE slp.student_id = $1 AND slp.lesson_id = $2;",
"options": {}
},
"id": "8dcbb09c-82c0-4c90-a671-50f7807e8b5b",
"name": "Update Progress",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
608,
448
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Log confidence rating\nINSERT INTO confidence_logs (student_id, topic_id, confidence_rating, context, notes, logged_at)\nSELECT $1, l.topic_id, $2, 'lesson_complete', $3, NOW()\nFROM lessons l\nWHERE l.lesson_id = $4;\n\n-- Log learning event\nINSERT INTO learning_events (student_id, event_type, resource_type, resource_id, metadata, timestamp)\nVALUES ($1, 'lesson_completed', 'lesson', $4, \n json_build_object(\n 'completion_percentage', $5,\n 'time_spent_minutes', $6,\n 'confidence_rating', $2\n )::jsonb,\n NOW()\n);",
"options": {}
},
"id": "980401eb-4c59-4c62-90ac-fed113704781",
"name": "Log Confidence & Event",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
800,
448
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Build lesson response\nconst lesson = $input.first().json;\n\nif (!lesson || !lesson.lesson_id) {\n return {\n json: {\n success: false,\n error: 'Lesson not found',\n lessonId: $('Parse Request').item.json.lessonId\n }\n };\n}\n\n// Parse content blocks if string\nlet contentBlocks = lesson.content_blocks;\nif (typeof contentBlocks === 'string') {\n try {\n contentBlocks = JSON.parse(contentBlocks);\n } catch (e) {\n contentBlocks = [];\n }\n}\n\nreturn {\n json: {\n success: true,\n lesson: {\n id: lesson.lesson_id,\n title: lesson.lesson_title,\n type: lesson.lesson_type,\n number: lesson.lesson_number,\n contentBlocks: contentBlocks || [],\n learningObjectives: lesson.learning_objectives || [],\n successCriteria: lesson.success_criteria || [],\n estimatedMinutes: lesson.estimated_minutes,\n difficultyRating: lesson.difficulty_rating,\n markSchemeNotes: lesson.mark_scheme_notes,\n examTips: lesson.exam_tips || [],\n commonMistakes: lesson.common_mistakes || [],\n topic: {\n id: lesson.topic_id,\n name: lesson.topic_name\n },\n subject: {\n id: lesson.subject_id,\n name: lesson.subject_name\n }\n },\n progress: {\n status: lesson.progress_status || 'not_started',\n completionPercentage: lesson.completion_percentage || 0,\n confidenceRating: lesson.confidence_rating || null,\n timeSpentMinutes: lesson.time_spent_minutes || 0,\n notes: lesson.student_notes || '',\n bookmarked: lesson.bookmarked || false,\n lastAccessedAt: lesson.last_accessed_at\n },\n meta: {\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "c6fe3dc7-146a-4859-ad2f-9eb6f5c79e8b",
"name": "Build Lesson Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
128
]
},
{
"parameters": {
"jsCode": "// Build lessons list response\nconst lessons = $input.all().map(item => item.json);\nconst request = $('Parse Request').item.json;\n\nreturn {\n json: {\n success: true,\n topicId: request.topicId,\n lessons: lessons.map(l => ({\n id: l.lesson_id,\n title: l.lesson_title,\n type: l.lesson_type,\n number: l.lesson_number,\n estimatedMinutes: l.estimated_minutes,\n difficultyRating: l.difficulty_rating,\n progress: {\n status: l.progress_status || 'not_started',\n completionPercentage: l.completion_percentage || 0,\n confidenceRating: l.confidence_rating || null\n }\n })),\n meta: {\n totalLessons: lessons.length,\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "749df3b6-9bbc-486f-ab03-a5626c1948da",
"name": "Build Lessons List Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
288
]
},
{
"parameters": {
"jsCode": "// Build completion response\nconst progress = $input.first().json;\nconst request = $('Parse Request').item.json;\n\nreturn {\n json: {\n success: true,\n message: request.completionPercentage >= 100 ? 'Lesson completed!' : 'Progress saved',\n progress: {\n lessonId: progress.lesson_id,\n status: progress.status,\n completionPercentage: progress.completion_percentage,\n confidenceRating: progress.confidence_rating,\n timeSpentMinutes: progress.time_spent_minutes\n },\n student: {\n xpPoints: progress.xp_points,\n currentLevel: progress.current_level,\n totalLessonsCompleted: progress.total_lessons_completed,\n xpGained: request.completionPercentage >= 100 ? 20 : 5\n },\n meta: {\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "ef03467f-0e5b-4d48-815c-c16e9e0acc18",
"name": "Build Completion Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1008,
448
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET, POST, OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type"
}
]
}
}
},
"id": "ba7828d7-428f-47fa-9f3a-43dc6b5e10f4",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1200,
288
]
},
{
"parameters": {
"jsCode": "// Handle unknown action\nconst request = $input.item.json;\n\nreturn {\n json: {\n success: false,\n error: 'Unknown action',\n action: request.action,\n validActions: ['get_lesson', 'get_lessons_by_topic', 'mark_complete']\n }\n};"
},
"id": "030ede0f-5269-4997-87c4-356fb3b751bd",
"name": "Unknown Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
720
]
}
],
"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 Lesson",
"type": "main",
"index": 0
}
]
]
},
"If Get Lesson": {
"main": [
[
{
"node": "Fetch Lesson",
"type": "main",
"index": 0
}
],
[
{
"node": "If Get Lessons By Topic",
"type": "main",
"index": 0
}
]
]
},
"If Get Lessons By Topic": {
"main": [
[
{
"node": "Fetch Lessons By Topic",
"type": "main",
"index": 0
}
],
[
{
"node": "If Mark Complete",
"type": "main",
"index": 0
}
]
]
},
"If Mark Complete": {
"main": [
[
{
"node": "Update Progress",
"type": "main",
"index": 0
}
],
[
{
"node": "Unknown Action",
"type": "main",
"index": 0
}
]
]
},
"Fetch Lesson": {
"main": [
[
{
"node": "Build Lesson Response",
"type": "main",
"index": 0
}
]
]
},
"Fetch Lessons By Topic": {
"main": [
[
{
"node": "Build Lessons List Response",
"type": "main",
"index": 0
}
]
]
},
"Update Progress": {
"main": [
[
{
"node": "Log Confidence & Event",
"type": "main",
"index": 0
}
]
]
},
"Log Confidence & Event": {
"main": [
[
{
"node": "Build Completion Response",
"type": "main",
"index": 0
}
]
]
},
"Build Lesson Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Build Lessons List Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Build Completion 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": "40e8e684-0ce5-4cfa-a2f6-cb29690efee6",
"activeVersionId": "40e8e684-0ce5-4cfa-a2f6-cb29690efee6",
"versionCounter": 9,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-02-13T19:47:57.154Z",
"createdAt": "2026-02-13T19:47:57.154Z",
"role": "workflow:owner",
"workflowId": "mxDwUMJ4LvfvTcKK",
"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:01:56.378Z",
"createdAt": "2026-02-13T19:48:04.057Z",
"versionId": "40e8e684-0ce5-4cfa-a2f6-cb29690efee6",
"workflowId": "mxDwUMJ4LvfvTcKK",
"nodes": [
{
"parameters": {
"path": "lesson",
"options": {}
},
"id": "f949a577-7d7e-4d4c-9ad8-947788305b8f",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
-608,
128
],
"webhookId": "lesson-api"
},
{
"parameters": {
"jsCode": "// Parse request and validate\nconst query = $input.item.json.query || {};\nconst action = query.action || 'get_lesson';\nconst studentId = parseInt(query.studentId) || 999;\nconst lessonId = parseInt(query.lessonId);\nconst topicId = parseInt(query.topicId);\nconst completionPercentage = parseInt(query.completionPercentage) || 100;\nconst confidenceRating = parseInt(query.confidenceRating);\nconst timeSpent = parseInt(query.timeSpent) || 0;\n\n// Validate required fields based on action\nif (action === 'get_lesson' && !lessonId && !topicId) {\n return {\n json: {\n error: true,\n message: 'Either lessonId or topicId is required',\n action\n }\n };\n}\n\nif (action === 'mark_complete' && !lessonId) {\n return {\n json: {\n error: true,\n message: 'lessonId is required for mark_complete action',\n action\n }\n };\n}\n\n// Sanitize inputs\nconst sanitize = (str, maxLength = 500) => {\n if (typeof str !== 'string') return '';\n return str.slice(0, maxLength).trim();\n};\n\nconst notes = sanitize(query.notes || '', 1000);\n\nreturn {\n json: {\n action,\n studentId,\n lessonId: lessonId || null,\n topicId: topicId || null,\n completionPercentage,\n confidenceRating: confidenceRating || null,\n timeSpent,\n notes,\n timestamp: new Date().toISOString()\n }\n};"
},
"id": "7a807867-b172-4c39-8d71-599b74913b5f",
"name": "Parse Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-400,
128
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.error }}",
"value2": "true"
}
]
}
},
"id": "42f2e262-714f-4ac8-a26d-60534ec1f826",
"name": "If Error",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-208,
128
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET, POST, OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type"
}
]
}
}
},
"id": "40e29a7c-6089-4aec-a93c-0dea3497559d",
"name": "Respond Error",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
0,
0
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "get_lesson"
}
]
}
},
"id": "8d6fb72f-bb74-498c-8f9d-8db5a4824979",
"name": "If Get Lesson",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
0,
240
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "get_lessons_by_topic"
}
]
}
},
"id": "2e6ecc99-4491-48e4-8cc1-25f93cd4bed3",
"name": "If Get Lessons By Topic",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
208,
400
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.action }}",
"value2": "mark_complete"
}
]
}
},
"id": "cfc0e340-5c9c-4e0b-98a5-f4e03b912c11",
"name": "If Mark Complete",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
400,
560
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n l.lesson_id,\n l.lesson_title,\n l.lesson_type,\n l.lesson_number,\n l.content_blocks,\n l.learning_objectives,\n l.success_criteria,\n l.estimated_minutes,\n l.difficulty_rating,\n l.mark_scheme_notes,\n l.exam_tips,\n l.common_mistakes,\n t.topic_name,\n t.topic_id,\n s.subject_name,\n s.subject_id,\n slp.status as progress_status,\n slp.completion_percentage,\n slp.confidence_rating,\n slp.time_spent_minutes,\n slp.notes as student_notes,\n slp.bookmarked,\n slp.last_accessed_at\nFROM lessons l\nJOIN topics t ON l.topic_id = t.topic_id\nJOIN subjects s ON t.subject_id = s.subject_id\nLEFT JOIN student_lesson_progress slp ON l.lesson_id = slp.lesson_id AND slp.student_id = $1\nWHERE l.lesson_id = $2 AND l.is_published = true",
"options": {}
},
"id": "39318f59-0ed6-4f58-9b6b-95aec31036c9",
"name": "Fetch Lesson",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
208,
128
],
"credentials": {
"postgres": {
"id": "LadjvFzuiQFYT469",
"name": "Postgres account 2"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n l.lesson_id,\n l.lesson_title,\n l.lesson_type,\n l.lesson_number,\n l.estimated_minutes,\n l.difficulty_rating,\n slp.status as progress_status,\n slp.completion_percentage,\n slp.confidence_rating\nFROM lessons l\nLEFT JOIN student_lesson_progress slp ON l.lesson_id = slp.lesson_id AND slp.student_id = $1\nWHERE l.topic_id = $2 AND l.is_published = true\nORDER BY l.lesson_number ASC",
"options": {}
},
"id": "2efe68fd-26c6-400b-9ff4-490442d34b06",
"name": "Fetch Lessons By Topic",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
400,
288
],
"credentials": {
"postgres": {
"id": "LadjvFzuiQFYT469",
"name": "Postgres account 2"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Update or insert lesson progress\nINSERT INTO student_lesson_progress (\n student_id, \n lesson_id, \n status, \n completion_percentage, \n time_spent_minutes,\n confidence_rating,\n notes,\n started_at,\n completed_at,\n last_accessed_at\n)\nVALUES (\n $1, $2, $3, $4, $5, $6, $7,\n COALESCE((SELECT started_at FROM student_lesson_progress WHERE student_id = $1 AND lesson_id = $2), NOW()),\n CASE WHEN $4 >= 100 THEN NOW() ELSE NULL END,\n NOW()\n)\nON CONFLICT (student_id, lesson_id)\nDO UPDATE SET\n status = CASE \n WHEN $4 >= 100 THEN 'completed'\n WHEN $4 > 0 THEN 'in_progress'\n ELSE student_lesson_progress.status\n END,\n completion_percentage = $4,\n time_spent_minutes = student_lesson_progress.time_spent_minutes + $5,\n confidence_rating = COALESCE($6, student_lesson_progress.confidence_rating),\n notes = COALESCE($7, student_lesson_progress.notes),\n completed_at = CASE \n WHEN $4 >= 100 AND student_lesson_progress.completed_at IS NULL THEN NOW()\n ELSE student_lesson_progress.completed_at\n END,\n last_accessed_at = NOW();\n\n-- Update student stats\nUPDATE students SET\n total_lessons_completed = (\n SELECT COUNT(*) FROM student_lesson_progress \n WHERE student_id = $1 AND status = 'completed'\n ),\n xp_points = xp_points + CASE WHEN $4 >= 100 THEN 20 ELSE 5 END,\n last_activity_date = CURRENT_DATE,\n updated_at = NOW()\nWHERE student_id = $1;\n\n-- Return updated progress\nSELECT \n slp.*,\n s.xp_points,\n s.current_level,\n s.total_lessons_completed\nFROM student_lesson_progress slp\nJOIN students s ON slp.student_id = s.student_id\nWHERE slp.student_id = $1 AND slp.lesson_id = $2;",
"options": {}
},
"id": "8dcbb09c-82c0-4c90-a671-50f7807e8b5b",
"name": "Update Progress",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
608,
448
],
"credentials": {
"postgres": {
"id": "LadjvFzuiQFYT469",
"name": "Postgres account 2"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "-- Log confidence rating\nINSERT INTO confidence_logs (student_id, topic_id, confidence_rating, context, notes, logged_at)\nSELECT $1, l.topic_id, $2, 'lesson_complete', $3, NOW()\nFROM lessons l\nWHERE l.lesson_id = $4;\n\n-- Log learning event\nINSERT INTO learning_events (student_id, event_type, resource_type, resource_id, metadata, timestamp)\nVALUES ($1, 'lesson_completed', 'lesson', $4, \n json_build_object(\n 'completion_percentage', $5,\n 'time_spent_minutes', $6,\n 'confidence_rating', $2\n )::jsonb,\n NOW()\n);",
"options": {}
},
"id": "980401eb-4c59-4c62-90ac-fed113704781",
"name": "Log Confidence & Event",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
800,
448
],
"credentials": {
"postgres": {
"id": "LadjvFzuiQFYT469",
"name": "Postgres account 2"
}
}
},
{
"parameters": {
"jsCode": "// Build lesson response\nconst lesson = $input.first().json;\n\nif (!lesson || !lesson.lesson_id) {\n return {\n json: {\n success: false,\n error: 'Lesson not found',\n lessonId: $('Parse Request').item.json.lessonId\n }\n };\n}\n\n// Parse content blocks if string\nlet contentBlocks = lesson.content_blocks;\nif (typeof contentBlocks === 'string') {\n try {\n contentBlocks = JSON.parse(contentBlocks);\n } catch (e) {\n contentBlocks = [];\n }\n}\n\nreturn {\n json: {\n success: true,\n lesson: {\n id: lesson.lesson_id,\n title: lesson.lesson_title,\n type: lesson.lesson_type,\n number: lesson.lesson_number,\n contentBlocks: contentBlocks || [],\n learningObjectives: lesson.learning_objectives || [],\n successCriteria: lesson.success_criteria || [],\n estimatedMinutes: lesson.estimated_minutes,\n difficultyRating: lesson.difficulty_rating,\n markSchemeNotes: lesson.mark_scheme_notes,\n examTips: lesson.exam_tips || [],\n commonMistakes: lesson.common_mistakes || [],\n topic: {\n id: lesson.topic_id,\n name: lesson.topic_name\n },\n subject: {\n id: lesson.subject_id,\n name: lesson.subject_name\n }\n },\n progress: {\n status: lesson.progress_status || 'not_started',\n completionPercentage: lesson.completion_percentage || 0,\n confidenceRating: lesson.confidence_rating || null,\n timeSpentMinutes: lesson.time_spent_minutes || 0,\n notes: lesson.student_notes || '',\n bookmarked: lesson.bookmarked || false,\n lastAccessedAt: lesson.last_accessed_at\n },\n meta: {\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "c6fe3dc7-146a-4859-ad2f-9eb6f5c79e8b",
"name": "Build Lesson Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
128
]
},
{
"parameters": {
"jsCode": "// Build lessons list response\nconst lessons = $input.all().map(item => item.json);\nconst request = $('Parse Request').item.json;\n\nreturn {\n json: {\n success: true,\n topicId: request.topicId,\n lessons: lessons.map(l => ({\n id: l.lesson_id,\n title: l.lesson_title,\n type: l.lesson_type,\n number: l.lesson_number,\n estimatedMinutes: l.estimated_minutes,\n difficultyRating: l.difficulty_rating,\n progress: {\n status: l.progress_status || 'not_started',\n completionPercentage: l.completion_percentage || 0,\n confidenceRating: l.confidence_rating || null\n }\n })),\n meta: {\n totalLessons: lessons.length,\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "749df3b6-9bbc-486f-ab03-a5626c1948da",
"name": "Build Lessons List Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
288
]
},
{
"parameters": {
"jsCode": "// Build completion response\nconst progress = $input.first().json;\nconst request = $('Parse Request').item.json;\n\nreturn {\n json: {\n success: true,\n message: request.completionPercentage >= 100 ? 'Lesson completed!' : 'Progress saved',\n progress: {\n lessonId: progress.lesson_id,\n status: progress.status,\n completionPercentage: progress.completion_percentage,\n confidenceRating: progress.confidence_rating,\n timeSpentMinutes: progress.time_spent_minutes\n },\n student: {\n xpPoints: progress.xp_points,\n currentLevel: progress.current_level,\n totalLessonsCompleted: progress.total_lessons_completed,\n xpGained: request.completionPercentage >= 100 ? 20 : 5\n },\n meta: {\n timestamp: new Date().toISOString()\n }\n }\n};"
},
"id": "ef03467f-0e5b-4d48-815c-c16e9e0acc18",
"name": "Build Completion Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1008,
448
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Access-Control-Allow-Methods",
"value": "GET, POST, OPTIONS"
},
{
"name": "Access-Control-Allow-Headers",
"value": "Content-Type"
}
]
}
}
},
"id": "ba7828d7-428f-47fa-9f3a-43dc6b5e10f4",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1200,
288
]
},
{
"parameters": {
"jsCode": "// Handle unknown action\nconst request = $input.item.json;\n\nreturn {\n json: {\n success: false,\n error: 'Unknown action',\n action: request.action,\n validActions: ['get_lesson', 'get_lessons_by_topic', 'mark_complete']\n }\n};"
},
"id": "030ede0f-5269-4997-87c4-356fb3b751bd",
"name": "Unknown Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
720
]
}
],
"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 Lesson",
"type": "main",
"index": 0
}
]
]
},
"If Get Lesson": {
"main": [
[
{
"node": "Fetch Lesson",
"type": "main",
"index": 0
}
],
[
{
"node": "If Get Lessons By Topic",
"type": "main",
"index": 0
}
]
]
},
"If Get Lessons By Topic": {
"main": [
[
{
"node": "Fetch Lessons By Topic",
"type": "main",
"index": 0
}
],
[
{
"node": "If Mark Complete",
"type": "main",
"index": 0
}
]
]
},
"If Mark Complete": {
"main": [
[
{
"node": "Update Progress",
"type": "main",
"index": 0
}
],
[
{
"node": "Unknown Action",
"type": "main",
"index": 0
}
]
]
},
"Fetch Lesson": {
"main": [
[
{
"node": "Build Lesson Response",
"type": "main",
"index": 0
}
]
]
},
"Fetch Lessons By Topic": {
"main": [
[
{
"node": "Build Lessons List Response",
"type": "main",
"index": 0
}
]
]
},
"Update Progress": {
"main": [
[
{
"node": "Log Confidence & Event",
"type": "main",
"index": 0
}
]
]
},
"Log Confidence & Event": {
"main": [
[
{
"node": "Build Completion Response",
"type": "main",
"index": 0
}
]
]
},
"Build Lesson Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Build Lessons List Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Build Completion Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
},
"Unknown Action": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "andrew johnson",
"name": "Version 40e8e684",
"description": "",
"autosaved": true,
"workflowPublishHistory": [
{
"createdAt": "2026-02-13T20:01:56.376Z",
"id": 146,
"workflowId": "mxDwUMJ4LvfvTcKK",
"versionId": "40e8e684-0ce5-4cfa-a2f6-cb29690efee6",
"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.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Interactive Lesson Workflow. Uses postgres. Webhook trigger; 16 nodes.
Source: https://github.com/AndyJay72/VenueDesk/blob/11c589f9baa17d8de267debed3a74b13db5e3286/n8n-workflows/mxDwUMJ4LvfvTcKK.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.
CMM. Uses httpRequest, postgres, redis. Webhook trigger; 90 nodes.
Scraping. Uses httpRequest, postgres, @apify/n8n-nodes-apify, respondToWebhook. Webhook trigger; 61 nodes.
Workflow B — AI Listing Engine. Uses httpRequest, postgres, errorTrigger. Webhook trigger; 47 nodes.