AutomationFlowsAI & RAG › Run Whatsapp Quizzes and Track Student Progress with Wati, Gpt-4.1 and Sheets

Run Whatsapp Quizzes and Track Student Progress with Wati, Gpt-4.1 and Sheets

ByJitesh Dugar @jiteshdugar on n8n.io

Turn WhatsApp into an interactive personal classroom. This workflow automates the entire learning cycle—from generating AI-powered quizzes to tracking student progress in real-time—by combining WATI, OpenAI AI Agents, and Google Sheets.

Event trigger★★★★☆ complexityAI-powered21 nodesGoogle SheetsN8N Nodes WatiAgentOpenAI Chat
AI & RAG Trigger: Event Nodes: 21 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #13655 — we link there as the canonical source.

This workflow follows the Agent → Google Sheets 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
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "011057a0-718e-4c13-86da-fc4af6db18ec",
      "name": "\ud83d\udccb Flow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -224,
        -672
      ],
      "parameters": {
        "width": 560,
        "height": 628,
        "content": "## \ud83c\udf93 WhatsApp Learning Assistant \u2013 Quiz Generator & Progress Tracker\n\n**How it works:**\n1. Student sends a topic e.g. *quiz math* on WhatsApp via WATI\n2. Switch routes message by keyword command\n3. OpenAI generates 3 MCQ questions on that topic\n4. Student replies with answers e.g. *answer 1a 2b 3c*\n5. Score calculated, saved to Google Sheets, feedback sent back\n6. Student types *progress* anytime to see score history\n\n**Credentials needed:** WATI, OpenAI API Key (Header Auth), Google Sheets OAuth2"
      },
      "typeVersion": 1
    },
    {
      "id": "d652bc1d-16a9-495a-9a32-288e97b9e4b8",
      "name": "Sticky \u2013 Trigger & Route",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 370,
        "height": 510,
        "content": "### 1\ufe0f\u20e3 Trigger & Route\n**WATI Trigger** listens for incoming WhatsApp messages.\n**Route Message Switch** detects the command keyword:\n- Starts with `quiz` \u2192 generate quiz flow\n- Starts with `answer` \u2192 evaluate answers flow\n- `progress` \u2192 fetch score history\n- Anything else \u2192 send help message"
      },
      "typeVersion": 1
    },
    {
      "id": "abd16359-1519-470b-9583-c6168bae51b0",
      "name": "Sticky \u2013 Quiz Generation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 1254,
        "height": 382,
        "content": "### 2\ufe0f\u20e3 Quiz Generation\n**Extract Topic Code** parses the topic from the message.\n**AI Agent** generates 3 MCQ questions with options A-D and correct answers.\n**Format Quiz Code** parses the AI response, formats for WhatsApp and saves quiz session to Google Sheets.\n**WATI \u2013 Send Text Message** delivers the formatted report back to the student."
      },
      "typeVersion": 1
    },
    {
      "id": "95713db8-e5cb-4f3a-98ff-06d2ba5e438c",
      "name": "Sticky \u2013 Answer Evaluation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 966,
        "height": 350,
        "content": "### 3\ufe0f\u20e3 Answer Evaluation\n**Parse Answers Code** extracts student answers from message e.g. `answer 1a 2b 3c`.\n**Sheets \u2013 Read Quiz** fetches the stored quiz for this student.\n**Evaluate & Score Code** compares answers, calculates score and builds detailed feedback.\n**Sheets \u2013 Save Score** logs result: phone, topic, score, date.\n**WATI \u2013 Send Text Message** delivers the formatted report back to the student."
      },
      "typeVersion": 1
    },
    {
      "id": "a55cbd2e-9c04-4e5a-8f13-0d2a9a50f20b",
      "name": "Sticky \u2013 Progress Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        720
      ],
      "parameters": {
        "color": 7,
        "width": 626,
        "height": 302,
        "content": "### 4\ufe0f\u20e3 Progress Report\n**Sheets \u2013 Read Progress** fetches all score rows for this student's phone number.\n**Build Progress Report Code** calculates total quizzes, average score, best topic and recent history with visual bar.\n**WATI \u2013 Send Text Message** delivers the formatted report back to the student."
      },
      "typeVersion": 1
    },
    {
      "id": "d29141aa-70fd-4dfa-91a7-1767ca92cc41",
      "name": "Route Message",
      "type": "n8n-nodes-base.switch",
      "position": [
        736,
        288
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Quiz Request",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "7b75a712-fea7-46b3-8af0-0a958c50e381",
                    "operator": {
                      "type": "string",
                      "operation": "startsWith"
                    },
                    "leftValue": "={{ $json.text }}",
                    "rightValue": "quiz "
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Submit Answer",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "34d4f348-48e4-4f4b-832f-af9c56b4f81b",
                    "operator": {
                      "type": "string",
                      "operation": "startsWith"
                    },
                    "leftValue": "={{ $json.text }}",
                    "rightValue": "answer "
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Progress",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "89f902f7-e331-4821-b8f8-e4dd476a4245",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.text.toLowerCase() }}",
                    "rightValue": "progress"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "a90b94dd-7da7-4afd-8e3a-6d77ebe0a734",
      "name": "Extract Topic",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        16
      ],
      "parameters": {
        "jsCode": "// Extract Topic from Quiz Request\n// Input: 'quiz photosynthesis' or 'quiz world war 2'\nconst text = $json.text || '';\nconst phone = $json.waId || $json.from || 'unknown';\nconst senderName = $json.senderName || 'Student';\n\nconst topic = text.replace(/^quiz\\s+/i, '').trim();\nif (!topic) throw new Error('No topic provided.');\n\nconst today = new Date().toISOString().split('T')[0];\nconst sessionKey = `${phone}_${today}`;\n\nreturn { json: { phone, senderName, topic, sessionKey, today } };"
      },
      "typeVersion": 2
    },
    {
      "id": "c9b1c12d-e189-46f1-8c62-dcac2d866169",
      "name": "Format Quiz & Build Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        0
      ],
      "parameters": {
        "jsCode": "// Format Quiz & Build WhatsApp Message\n// Parses OpenAI response and formats quiz for WhatsApp display\n\nconst rawText = $json.output || '{}';\n\nlet quiz = {};\ntry {\n  quiz = JSON.parse(rawText);\n} catch (e) {\n  const match = rawText.match(/\\{[\\s\\S]*\\}/);\n  if (match) { try { quiz = JSON.parse(match[0]); } catch (e2) { quiz = {}; } }\n}\n\nconst phone = $('Extract Topic').item.json.phone;\nconst senderName = $('Extract Topic').item.json.senderName;\nconst topic = $('Extract Topic').item.json.topic;\nconst sessionKey = $('Extract Topic').item.json.sessionKey;\nconst today = $('Extract Topic').item.json.today;\nconst questions = quiz.questions || [];\n\nif (questions.length === 0) throw new Error('OpenAI did not return valid questions');\n\n// Build WhatsApp quiz message\nconst lines = [\n  `\ud83c\udf93 *Quiz Time, ${senderName}!*`,\n  `\ud83d\udcda *Topic: ${topic.toUpperCase()}*`,\n  '',\n  'Reply with: *answer 1X 2X 3X*',\n  'Example: *answer 1a 2c 3b*',\n  '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501'\n];\n\nconst correctAnswers = {};\nfor (const q of questions) {\n  lines.push('');\n  lines.push(`*Q${q.number}.* ${q.question}`);\n  lines.push(`  \ud83c\udd50 ${q.options.a}`);\n  lines.push(`  \ud83c\udd51 ${q.options.b}`);\n  lines.push(`  \ud83c\udd52 ${q.options.c}`);\n  lines.push(`  \ud83c\udd53 ${q.options.d}`);\n  correctAnswers[q.number] = q.correct.toLowerCase();\n}\n\nlines.push('');\nlines.push('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501');\nlines.push('\u23f1\ufe0f Reply with your answers now!');\n\n// Serialize correct answers: '1:a,2:c,3:b'\nconst answersString = Object.entries(correctAnswers).map(([n,a]) => `${n}:${a}`).join(',');\n\nreturn {\n  json: {\n    phone, senderName, topic, sessionKey, today,\n    quizMessage: lines.join('\\n'),\n    correctAnswers: answersString,\n    questionCount: questions.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "314bc871-8e7d-43e4-9ade-f13838691eeb",
      "name": "Google Sheets \u2013 Save Active Quiz",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1728,
        0
      ],
      "parameters": {
        "columns": {
          "value": {
            "phone": "={{ $json.phone }}",
            "today": "={{ $json.today }}",
            "topic": "={{ $json.topic }}",
            "sessionKey": "={{ $json.sessionKey }}",
            "questionCount": "={{ $json.questionCount }}",
            "correctAnswers": "={{ $json.correctAnswers }}"
          },
          "schema": [
            {
              "id": "sessionKey",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "sessionKey",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "topic",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "correctAnswers",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "correctAnswers",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "questionCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "questionCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "today",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "today",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit#gid=0",
          "cachedResultName": "Active Quizzes"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit?usp=drivesdk",
          "cachedResultName": "Untitled spreadsheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d248438c-e50b-4d20-b547-f53be5b7e106",
      "name": "Parse Student Answers",
      "type": "n8n-nodes-base.code",
      "position": [
        1104,
        480
      ],
      "parameters": {
        "jsCode": "// Parse Student Answers\n// Input: 'answer 1a 2c 3b'\nconst text = ($json.text || '').trim();\nconst phone = $json.waId || $json.from || 'unknown';\nconst senderName = $json.senderName || 'Student';\n\nconst answerPart = text.replace(/^answer\\s+/i, '').trim();\nconst matches = answerPart.match(/([1-9][a-dA-D])/g);\n\nif (!matches || matches.length === 0) {\n  throw new Error('Could not parse answers. Format: answer 1a 2b 3c');\n}\n\nconst studentAnswers = {};\nfor (const match of matches) {\n  studentAnswers[parseInt(match[0])] = match[1].toLowerCase();\n}\n\nconst today = new Date().toISOString().split('T')[0];\nconst sessionKey = `${phone}_${today}`;\n\nreturn { json: { phone, senderName, sessionKey, today, studentAnswers } };"
      },
      "typeVersion": 2
    },
    {
      "id": "395a0afa-1d20-4d17-9c24-b6980519ee7d",
      "name": "Google Sheets \u2013 Read Active Quiz",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1280,
        480
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit#gid=0",
          "cachedResultName": "Active Quizzes"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit?usp=drivesdk",
          "cachedResultName": "Untitled spreadsheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4cb5e7d9-ebe6-4e0c-9e9a-5b39b41065d6",
      "name": "Evaluate & Calculate Score",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        480
      ],
      "parameters": {
        "jsCode": "// Evaluate Answers & Calculate Score\n// Compares student answers vs correct answers from Google Sheets\n\nconst parsedData = $('Parse Student Answers').item.json;\nconst { phone, senderName, sessionKey, today, studentAnswers } = parsedData;\n\n// Find matching quiz row\nconst allRows = $input.all();\nconst quizRow = allRows.find(r => r.json.sessionKey === sessionKey);\n\nif (!quizRow) {\n  return {\n    json: {\n      phone,\n      feedbackMessage: `\u26a0\ufe0f *No active quiz found!*\\n\\nYou haven't requested a quiz today yet.\\nSend *quiz <topic>* to start one!\\nExample: *quiz solar system*`,\n      score: 0, total: 0, topic: 'N/A', today, hasQuiz: false\n    }\n  };\n}\n\nconst topic = quizRow.json.topic;\nconst questionCount = parseInt(quizRow.json.questionCount) || 3;\n\n// Parse correct answers from '1:a,2:c,3:b'\nconst correctAnswers = {};\nfor (const pair of (quizRow.json.correctAnswers || '').split(',')) {\n  const [num, ans] = pair.split(':');\n  if (num && ans) correctAnswers[parseInt(num)] = ans.toLowerCase();\n}\n\n// Score calculation\nlet score = 0;\nconst resultLines = [];\nfor (let i = 1; i <= questionCount; i++) {\n  const studentAns = (studentAnswers[i] || '?').toLowerCase();\n  const correctAns = correctAnswers[i] || '?';\n  const isCorrect = studentAns === correctAns;\n  if (isCorrect) score++;\n  resultLines.push(`Q${i}: You answered *${studentAns.toUpperCase()}* ${isCorrect ? '\u2705' : `\u274c (correct: ${correctAns.toUpperCase()})`}`);\n}\n\nconst percentage = Math.round((score / questionCount) * 100);\nlet performanceMsg = percentage === 100 ? '\ud83c\udfc6 Perfect score! Outstanding!'\n  : percentage >= 66 ? '\ud83c\udf1f Great job! Keep it up!'\n  : percentage >= 33 ? '\ud83d\udcd6 Good effort! Review and try again!'\n  : '\ud83d\udcaa Keep studying! You can do better!';\n\nconst feedbackLines = [\n  `\ud83d\udcdd *Quiz Results \u2013 ${topic.toUpperCase()}*`,\n  '',\n  ...resultLines,\n  '',\n  '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501',\n  `\ud83c\udfaf *Score: ${score}/${questionCount} (${percentage}%)*`,\n  performanceMsg,\n  '',\n  'Type *progress* to see your full history!',\n  'Type *quiz <topic>* to try another quiz!'\n];\n\nreturn {\n  json: {\n    phone, senderName, topic, score,\n    total: questionCount, percentage, sessionKey, today,\n    feedbackMessage: feedbackLines.join('\\n'),\n    hasQuiz: true\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "24e96f7e-8fcd-4899-950b-f9dbed03eeb4",
      "name": "Google Sheets \u2013 Save Score",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1616,
        480
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.today }}",
            "phone": "={{ $json.phone }}",
            "score": "={{ $json.score }}",
            "topic": "={{ $json.topic }}",
            "total": "={{ $json.total }}",
            "percentage": "={{ $json.percentage }}",
            "senderName": "={{ $json.senderName }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "senderName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "senderName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "topic",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "percentage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "percentage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 916130242,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit#gid=916130242",
          "cachedResultName": "Scores"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit?usp=drivesdk",
          "cachedResultName": "Untitled spreadsheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "f11f8afb-154e-4771-a5e8-98ee025d720d",
      "name": "Google Sheets \u2013 Read Progress",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1088,
        848
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 916130242,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit#gid=916130242",
          "cachedResultName": "Scores"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_qWPXSk-WfGb3lCl_Gkb17h8K0qJr_DfPxsSZ1i8hY/edit?usp=drivesdk",
          "cachedResultName": "Untitled spreadsheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "15333ca7-f728-41be-b707-7cf186e7f3ba",
      "name": "Build Progress Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1296,
        848
      ],
      "parameters": {
        "jsCode": "// Build Progress Report\n// Fetches all scores for this student and builds a summary\n\nconst phone =  $('Wati Trigger1').first().json.waId|| $('Wati Trigger1').json.from;\nconst senderName =  $('Wati Trigger1').first().json.senderName|| 'Student';\nconst allRows = $input.all();\nconst myRows = allRows.filter(r => String(r.json.phone) === String(phone));\n\nif (myRows.length === 0) {\n  return [{ json: {\n    phone,\n    progressMessage: `\ud83d\udcca *No quiz history yet, ${senderName}!*\\n\\nStart your first quiz:\\n*quiz <topic>*\\nExample: *quiz photosynthesis*`\n  }}];\n}\n\nconst totalQuizzes = myRows.length;\nconst totalScore = myRows.reduce((s, r) => s + (parseFloat(r.json.score) || 0), 0);\nconst totalQuestions = myRows.reduce((s, r) => s + (parseFloat(r.json.total) || 0), 0);\nconst avgPercentage = Math.round((totalScore / totalQuestions) * 100);\n\n// Best topic calculation\nconst topicMap = {};\nfor (const r of myRows) {\n  const t = r.json.topic || 'Unknown';\n  if (!topicMap[t]) topicMap[t] = { score: 0, total: 0 };\n  topicMap[t].score += parseFloat(r.json.score) || 0;\n  topicMap[t].total += parseFloat(r.json.total) || 0;\n}\nlet bestTopic = '';\nlet bestPct = 0;\nfor (const [topic, data] of Object.entries(topicMap)) {\n  const pct = data.total > 0 ? (data.score / data.total) * 100 : 0;\n  if (pct > bestPct) { bestPct = pct; bestTopic = topic; }\n}\n\n// Recent 5 quizzes\nconst recent = myRows.slice(-5).reverse();\nconst recentLines = recent.map(r => {\n  const pct = parseFloat(r.json.percentage) || 0;\n  const emoji = pct === 100 ? '\ud83c\udfc6' : pct >= 66 ? '\ud83c\udf1f' : pct >= 33 ? '\ud83d\udcd6' : '\ud83d\udcaa';\n  return `${emoji} ${r.json.topic} \u2013 ${r.json.score}/${r.json.total} (${pct}%) on ${r.json.date}`;\n});\n\n// Progress bar\nconst filled = Math.round(avgPercentage / 10);\nconst bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);\n\nconst lines = [\n  `\ud83d\udcca *Progress Report*`,\n  `\ud83d\udc64 *${senderName}*`,\n  '',\n  `${bar} *${avgPercentage}% avg score*`,\n  '',\n  `\ud83d\udcdd *Total Quizzes Taken:* ${totalQuizzes}`,\n  `\u2705 *Total Correct:* ${totalScore}/${totalQuestions}`,\n  `\ud83c\udfc5 *Best Topic:* ${bestTopic} (${Math.round(bestPct)}%)`,\n  '',\n  '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501',\n  '*Recent Quizzes:*',\n  ...recentLines,\n  '',\n  'Type *quiz <topic>* to keep learning! \ud83d\ude80'\n];\n\nreturn [{ json: { phone, progressMessage: lines.join('\\n') } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "cf7720eb-1a8d-4620-be7f-22d452f4180f",
      "name": "Wati Trigger1",
      "type": "n8n-nodes-wati.watiTrigger",
      "position": [
        544,
        320
      ],
      "parameters": {
        "event": "messageReceived"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "0dd92282-065f-47a1-8fca-a99326399b5b",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1168,
        0
      ],
      "parameters": {
        "text": "=Generate a 3-question MCQ quiz about the topic: {{ $json.topic }}",
        "options": {
          "systemMessage": "=\"You are a professional tutor. Generate a 3-question MCQ quiz. Return ONLY a valid JSON object with 'questions' containing 'number', 'question', 'options' (a,b,c,d), and 'correct'. Do not include any conversational text or markdown blocks like ```json.\""
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "acafd240-d88c-444f-98a4-3fbabaa7eb2f",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1168,
        144
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "b16ab10c-284b-4d67-b1e3-084de2af1371",
      "name": "Send Quiz",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1952,
        0
      ],
      "parameters": {
        "target": "={{ $json.phone }}",
        "messageText": "={{ $('Format Quiz & Build Message').item.json.quizMessage }}ssage Text: ```markdown\n"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "592de9a8-28d1-4a77-a481-5cdcdc15f713",
      "name": "Send Score",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1792,
        480
      ],
      "parameters": {
        "target": "={{ $json.phone }}",
        "messageText": "={{ $('Evaluate & Calculate Score').item.json.feedbackMessage }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "029b3a0e-7e4f-414e-b023-fe48d93858ff",
      "name": "Send Score1",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1504,
        848
      ],
      "parameters": {
        "target": "={{ $json.phone }}",
        "messageText": "={{ $json.progressMessage }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "node": "Format Quiz & Build Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Topic": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Message": {
      "main": [
        [
          {
            "node": "Extract Topic",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Parse Student Answers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets \u2013 Read Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wati Trigger1": {
      "main": [
        [
          {
            "node": "Route Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Build Progress Report": {
      "main": [
        [
          {
            "node": "Send Score1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Student Answers": {
      "main": [
        [
          {
            "node": "Google Sheets \u2013 Read Active Quiz",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate & Calculate Score": {
      "main": [
        [
          {
            "node": "Google Sheets \u2013 Save Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Quiz & Build Message": {
      "main": [
        [
          {
            "node": "Google Sheets \u2013 Save Active Quiz",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Save Score": {
      "main": [
        [
          {
            "node": "Send Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Read Progress": {
      "main": [
        [
          {
            "node": "Build Progress Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Read Active Quiz": {
      "main": [
        [
          {
            "node": "Evaluate & Calculate Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Save Active Quiz": {
      "main": [
        [
          {
            "node": "Send Quiz",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

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

Turn WhatsApp into an interactive personal classroom. This workflow automates the entire learning cycle—from generating AI-powered quizzes to tracking student progress in real-time—by combining WATI, OpenAI AI Agents, and Google Sheets.

Source: https://n8n.io/workflows/13655/ — 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

K&S-Media Downloadliste SQL. Uses httpRequest, agent, googleSheets, lmChatOpenAi. Event-driven trigger; 97 nodes.

HTTP Request, Agent, Google Sheets +3
AI & RAG

🎯 Create viral TikToks, Shorts, Reels, podcasts, and ASMR videos in minutes — all on autopilot.

OpenAI, HTTP Request, Form Trigger +7
AI & RAG

Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.

@Blotato/N8N Nodes Blotato, Google Sheets, OpenAI Chat +9
AI & RAG

&gt; Note: This workflow uses sticky notes extensively to document each logical section of the automation. Sticky notes are mandatory and already included to explain OCR, AI parsing, folder logic, dup

QuickBooks, Google Sheets, Google Drive +5
AI & RAG

This template is designed for marketers, content creators, and e-commerce brands who want to automate the creation of professional ad videos at scale. It’s ideal for teams looking to generate consiste

Telegram, Telegram Trigger, Google Drive +8