AutomationFlowsAI & RAG › Evaluate Omr Answer Sheets with Gemini Vision AI and Google Sheets

Evaluate Omr Answer Sheets with Gemini Vision AI and Google Sheets

ByInfyOm Technologies @infyom on n8n.io

Manual checking of OMR (Optical Mark Recognition) answer sheets is time-consuming, error-prone, and difficult to scale—especially for schools, coaching institutes, and exam centers. This workflow automates OMR evaluation end-to-end using AI, from reading a scanned answer sheet…

Webhook trigger★★★★☆ complexityAI-powered14 nodesOllamaGoogle Sheets
AI & RAG Trigger: Webhook Nodes: 14 Complexity: ★★★★☆ AI nodes: yes Added:

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

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": "6fc105e9-70fb-4b39-94aa-443c6ce55dfe",
      "name": "Analyze image",
      "type": "@n8n/n8n-nodes-langchain.ollama",
      "position": [
        960,
        1936
      ],
      "parameters": {
        "text": "=You are an OMR sheet analysis system.\n\nTask:\nAnalyze the provided OMR answer sheet image and extract ONLY the student's marked answers.\n\nStrict rules:\n- Ignore all headers, instructions, logos, barcodes, or any other text.\n- High Focus ONLY on the answer bubbles and properlly right answer provide.\n- For each question, detect the clearly filled/marked option (A, B, C, or D).\n- If multiple bubbles are filled for a question, select the darkest/most clearly marked one.\n- If no option is clearly marked for a question, skip that question entirely.\n- Do NOT guess answers.\n- Do NOT explain anything.\n- Do NOT add confidence scores or metadata.\n\nOutput format (MANDATORY):\n- Plain text only\n- Exactly in the format shown below\n- One answer per line\n- Sequential numbering starting from question 1\n- No extra spaces, symbols, or commentary\n- Extract a student details\n\nRequired output structure:\n \"student\":\n    \"name\": \"string | null\",\n    \"rollNumber\": \"string | null\",\n    \"class\": \"string | null\"\nanswer :\n1. A\n2. B\n3. C\n4. D\n5. A\n...\n(continue until the last detected question)",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gemini-3-flash-preview",
          "cachedResultName": "gemini-3-flash-preview"
        },
        "options": {
          "format": "json"
        },
        "resource": "image",
        "simplify": false
      },
      "typeVersion": 1
    },
    {
      "id": "e98c50f8-98e3-4b94-951c-40a94197cd6b",
      "name": "Merge Answer",
      "type": "n8n-nodes-base.merge",
      "position": [
        1408,
        1888
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "0d328c98-9fe6-487e-bbb1-cca7a93539dc",
      "name": "Calculate Result",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        1856
      ],
      "parameters": {
        "jsCode": "// // function parseAnswers(input) {\n// //   if (!input) return {};\n\n// //   // If already object, return as is\n// //   if (typeof input === \"object\") {\n// //     return input;\n// //   }\n\n// //   // If string, normalize and parse\n// //   if (typeof input === \"string\") {\n// //     let obj = {};\n// //     let parts = input\n// //       .replace(/->/g, \":\")        // handle \"->\" also\n// //       .replace(/\\n/g, \",\")        // handle line breaks\n// //       .split(\",\");\n\n// //     for (let p of parts) {\n// //       let [q, ans] = p.split(\":\").map(s => s.trim());\n// //       if (q && ans) obj[q] = ans.toUpperCase();\n// //     }\n// //     return obj;\n// //   }\n\n// //   return {};\n// // }\n\n// // // ---- Extract Inputs (safe handling)\n// // const item = $input.item?.json || {};\n// // const originalRaw = item.chatInput || item.CorrectAnswer || {};\n// // const userRaw = item.UserAnswers || {};\n\n// // const original = parseAnswers(originalRaw);\n// // const user = parseAnswers(userRaw);\n\n// // let correct = 0;\n// // let incorrect = 0;\n// // let results = [];\n\n// // const total = Object.keys(original).length || Object.keys(user).length || 0;\n\n// // // ---- Comparison logic\n// // for (let i = 1; i <= total; i++) {\n// //   const q = String(i);\n// //   const correctAns = original[q] || \"Not Available\";\n// //   const given = user[q] || \"Not Attempted\";\n\n// //   if (given === correctAns) {\n// //     correct++;\n// //     results.push({ question: i, given, correctAns, status: \"Correct\" });\n// //   } else {\n// //     incorrect++;\n// //     results.push({ question: i, given, correctAns, status: \"Incorrect\" });\n// //   }\n// // }\n\n// // // ---- Calculate percentage safely\n// // const scorePercentage = total > 0 ? ((correct / total) * 100).toFixed(2) : \"0.00\";\n\n// // // ---- Return final structured output\n// // return [\n// //   {\n// //     json: {\n// //       totalQuestions: total,\n// //       correct,\n// //       incorrect,\n// //       scorePercentage,\n// //       results\n// //     }\n// //   }\n// // ];\n\n\n\n\n// function parseAnswers(input) {\n//   if (!input) return {};\n\n//   // If already object, return as is\n//   if (typeof input === \"object\") {\n//     return input;\n//   }\n\n//   // If string, normalize and parse\n//   if (typeof input === \"string\") {\n//     let obj = {};\n//     let parts = input\n//       .replace(/->/g, \":\")\n//       .replace(/\\n/g, \",\")\n//       .split(\",\");\n\n//     for (let p of parts) {\n//       let [q, ans] = p.split(\":\").map(s => s.trim());\n//       if (q && ans) obj[q] = ans.toUpperCase();\n//     }\n//     return obj;\n//   }\n\n//   return {};\n// }\n\n// // ---- Extract Inputs\n// const item = $input.item?.json || {};\n\n// // \u2705 STUDENT DETAILS (DIRECT, NO PARSING)\n// const student = item.student || {\n//   name: null,\n//   rollNumber: null,\n//   class: null\n// };\n\n// // ---- Answers\n// const originalRaw = item.chatInput || item.CorrectAnswer || {};\n// const userRaw = item.UserAnswers || {};\n\n// const original = parseAnswers(originalRaw);\n// const user = parseAnswers(userRaw);\n\n// // ---- Evaluation\n// let correct = 0;\n// let incorrect = 0;\n// let results = [];\n\n// const total =\n//   Object.keys(original).length ||\n//   Object.keys(user).length ||\n//   0;\n\n// for (let i = 1; i <= total; i++) {\n//   const q = String(i);\n//   const correctAns = original[q] || \"Not Available\";\n//   const given = user[q] || \"Not Attempted\";\n\n//   if (given === correctAns) {\n//     correct++;\n//     results.push({\n//       question: i,\n//       given,\n//       correctAns,\n//       status: \"Correct\"\n//     });\n//   } else {\n//     incorrect++;\n//     results.push({\n//       question: i,\n//       given,\n//       correctAns,\n//       status: \"Incorrect\"\n//     });\n//   }\n// }\n\n// // ---- Score\n// const scorePercentage =\n//   total > 0 ? ((correct / total) * 100).toFixed(2) : \"0.00\";\n\n// // ---- FINAL API RESPONSE\n// return [\n//   {\n//     json: {\n//       student,\n//       totalQuestions: total,\n//       correct,\n//       incorrect,\n//       scorePercentage,\n//       results\n//     }\n//   }\n// ];\n\n\nfunction parseAnswers(input) {\n  if (!input) return {};\n\n  if (typeof input === \"object\") {\n    return input;\n  }\n\n  if (typeof input === \"string\") {\n    let obj = {};\n    let parts = input\n      .replace(/->/g, \":\")\n      .replace(/\\n/g, \",\")\n      .split(\",\");\n\n    for (let p of parts) {\n      let [q, ans] = p.split(\":\").map(s => s.trim());\n      if (q && ans) obj[q] = ans.toUpperCase();\n    }\n    return obj;\n  }\n\n  return {};\n}\n\n// ---- Extract Inputs\nconst item = $input.item?.json || {};\n\n// ---- Student Details\nconst student = item.student || {\n  name: null,\n  rollNumber: null,\n  class: null\n};\n\n// ---- Answers\nconst original = parseAnswers(item.chatInput || item.CorrectAnswer || {});\nconst user = parseAnswers(item.UserAnswers || {});\n\n// ---- Evaluation\nlet correct = 0;\nlet incorrect = 0;\nlet results = [];\nlet binaryResult = {}; // \ud83d\udc48 for Google Sheets (Q.1 = 1/0)\n\nconst total =\n  Object.keys(original).length ||\n  Object.keys(user).length ||\n  0;\n\nfor (let i = 1; i <= total; i++) {\n  const q = String(i);\n  const correctAns = original[q] || \"Not Available\";\n  const given = user[q] || \"Not Attempted\";\n\n  const isCorrect = given === correctAns;\n  const value = isCorrect ? 1 : 0;\n\n  if (isCorrect) {\n    correct++;\n  } else {\n    incorrect++;\n  }\n\n  results.push({\n    question: i,\n    given,\n    correctAns,\n    status: isCorrect ? \"Correct\" : \"Incorrect\",\n    value // \ud83d\udc48 1 or 0\n  });\n\n  binaryResult[`Q.${i}`] = value; // \ud83d\udc48 Q.1, Q.2, ...\n}\n\n// ---- Score\nconst scorePercentage =\n  total > 0 ? ((correct / total) * 100).toFixed(2) : \"0.00\";\n\n// ---- FINAL OUTPUT\nreturn [\n  {\n    json: {\n      student,\n      totalQuestions: total,\n      correct,\n      incorrect,\n      scorePercentage,\n      results,        // detailed\n      binaryResult    // \ud83d\udc48 PERFECT FOR GOOGLE SHEETS\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "78f5bed1-1583-4429-ae5b-960be1d44795",
      "name": "Send Result Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2016,
        1856
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  status: \"success\",\n  data: $json\n} }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "21acd37d-0f9b-4475-96a5-794a7497d4e4",
      "name": "Convert User Ans in Formate",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        1968
      ],
      "parameters": {
        "jsCode": "// // Get input JSON\n// const input = items[0].json;\n\n// // Get raw AI response text\n// const content = input.message?.content || \"\";\n\n// // Regex to capture answers like \"1:A\", \"2 : B\", etc.\n// const regex = /(\\d+)\\s*[:\\.]\\s*([A-D])/g;\n\n// let match;\n// const pairs = [];\n\n// // Extract question-answer pairs\n// while ((match = regex.exec(content)) !== null) {\n//   pairs.push(`${match[1]}:${match[2]}`);\n// }\n\n// // Join into a single string\n// const userAnswersStr = pairs.join(\", \");\n\n// // Return ONLY user answers\n// return [\n//   {\n//     json: {\n//       UserAnswers: userAnswersStr\n//     }\n//   }\n// ];\n\n\n\n// ===============================\n// 1. GET INPUT\n// ===============================\nconst input = items[0].json;\nlet content = input.message?.content || \"\";\n\n// Remove ``` fences if present\ncontent = content.replace(/```/g, \"\").trim();\n\n// ===============================\n// 2. EXTRACT STUDENT DETAILS\n// ===============================\nconst student = {\n  name: null,\n  rollNumber: null,\n  class: null\n};\n\n// Name\nconst nameMatch = content.match(/\"name\"\\s*:\\s*\"([^\"]+)\"/i);\nif (nameMatch) student.name = nameMatch[1].trim();\n\n// Roll Number\nconst rollMatch = content.match(/\"rollNumber\"\\s*:\\s*\"([^\"]+)\"/i);\nif (rollMatch) student.rollNumber = rollMatch[1].trim();\n\n// Class\nconst classMatch = content.match(/\"class\"\\s*:\\s*\"([^\"]+)\"/i);\nif (classMatch) student.class = classMatch[1].trim();\n\n// ===============================\n// 3. EXTRACT USER ANSWERS\n// ===============================\n\n// Regex to capture answers like \"1:A\", \"1. A\", \"2 : B\"\nconst regex = /(\\d+)\\s*[:\\.]\\s*([A-D])/g;\n\nlet match;\nconst pairs = [];\n\n// Extract question-answer pairs\nwhile ((match = regex.exec(content)) !== null) {\n  pairs.push(`${match[1]}:${match[2]}`);\n}\n\n// Join into a single string\nconst userAnswersStr = pairs.join(\", \");\n\n// ===============================\n// 4. FINAL OUTPUT\n// ===============================\nreturn [\n  {\n    json: {\n      student,\n      UserAnswers: userAnswersStr\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d4f4b88d-30c3-4355-a404-b9000db1d2ea",
      "name": "Set Your Correct Answer",
      "type": "n8n-nodes-base.set",
      "position": [
        1216,
        1824
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1209b6e5-0167-4c2a-b9bd-b894760a4027",
              "name": "CorrectAnswer",
              "type": "string",
              "value": "1:B, 2:A, 3:D, 4:C, 5:B, 6:D, 7:B, 8:C, 9:A, 10:B, 11:D, 12:C, 13:A, 14:D, 15:B, 16:C, 17:D, 18:A, 19:B, 20:C"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0bd1adb7-3475-4251-b343-79b2608d800f",
      "name": "Send Student Ans Img",
      "type": "n8n-nodes-base.webhook",
      "position": [
        640,
        1856
      ],
      "parameters": {
        "path": "omr-sheet-checker",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "52442ffc-824d-432b-aaad-80eab9257104",
      "name": "Append Result in Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1824,
        1856
      ],
      "parameters": {
        "columns": {
          "value": {
            "Q.1": "={{ $json.results[0].value }}",
            "Q.2": "={{ $json.results[1].value }}",
            "Q.3": "={{ $json.results[2].value }}",
            "Q.4": "={{ $json.results[3].value }}",
            "Q.5": "={{ $json.results[4].value }}",
            "Q.6": "={{ $json.results[5].value }}",
            "Q.7": "={{ $json.results[6].value }}",
            "Q.8": "={{ $json.results[7].value }}",
            "Q.9": "={{ $json.results[8].value }}",
            "Q.10": "={{ $json.results[9].value }}",
            "Q.11": "={{ $json.results[10].value }}",
            "Q.12": "={{ $json.results[11].value }}",
            "Q.13": "={{ $json.results[12].value }}",
            "Q.14": "={{ $json.results[13].value }}",
            "Q.15": "={{ $json.results[14].value }}",
            "Q.16": "={{ $json.results[15].value }}",
            "Q.17": "={{ $json.results[16].value }}",
            "Q.18": "={{ $json.results[17].value }}",
            "Q.19": "={{ $json.results[18].value }}",
            "Q.20": "={{ $json.results[19].value }}",
            "Class": "={{ $json.student.class }}",
            "Correct": "={{ $json.correct }}",
            "Roll No": "={{ $json.student.rollNumber }}",
            "Incorrect": "={{ $json.incorrect }}",
            "Student Name": "={{ $json.student.name }}",
            "Score Percentage": "={{ $json.scorePercentage }}%"
          },
          "schema": [
            {
              "id": "Student Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Student Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Roll No",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Roll No",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Class",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Class",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.4",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.5",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.6",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.6",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.8",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.8",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.10",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.10",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.11",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.11",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.12",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.12",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.13",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.13",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.14",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.14",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.15",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.15",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.16",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.16",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.17",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.17",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.18",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.18",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.19",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.19",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q.20",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Q.20",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Correct",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Correct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Incorrect",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Incorrect",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Score Percentage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Score Percentage",
              "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/15nvRI8zcJ2n-QBXNk9xyd1GM8Vj7_1lJ3K7vO7Qfq3w/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "15nvRI8zcJ2n-QBXNk9xyd1GM8Vj7_1lJ3K7vO7Qfq3w",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/15nvRI8zcJ2n-QBXNk9xyd1GM8Vj7_1lJ3K7vO7Qfq3w/edit?usp=drivesdk",
          "cachedResultName": "Students Result"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "37946fab-f3c4-41d5-ab75-64570b720429",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        2128
      ],
      "parameters": {
        "color": 4,
        "width": 416,
        "height": 256,
        "content": "## Google Sheet\nhttps://docs.google.com/spreadsheets/d/15nvRI8zcJ2n-QBXNk9xyd1GM8Vj7_1lJ3K7vO7Qfq3w/edit?usp=sharing\n\n\n## Webhook Payload :\nForm-Data\nkey-name : file\nValue : Upload Img"
      },
      "typeVersion": 1
    },
    {
      "id": "a81c3963-b846-440c-90e2-c13bd394713a",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        1696
      ],
      "parameters": {
        "width": 420,
        "height": 528,
        "content": "## How it works\nThis workflow automates OMR (Optical Mark Recognition) answer sheet evaluation. Upload a student's answer sheet image via webhook, and AI analyzes the marked bubbles to extract answers. The system compares them against correct answers, calculates scores, and stores results in Google Sheets.\n\nKey features:\n- AI-powered bubble detection using Gemini\n- Automatic student detail extraction (name, roll number, class)\n- Question-by-question comparison with correct answers\n- Binary scoring (1/0) for each question\n- Real-time results via webhook response\n\n## Setup steps\n1. Configure Ollama credentials for Gemini AI model\n2. Set correct answers in \"Set Your Correct Answer\" node\n3. Connect Google Sheets OAuth2 credentials\n4. Test webhook with form-data (key: \"file\", value: OMR image)\n5. Verify results appear in Google Sheet"
      },
      "typeVersion": 1
    },
    {
      "id": "4ab41919-537e-40a3-b051-e0bbe3a19b65",
      "name": "Input Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        1696
      ],
      "parameters": {
        "color": 7,
        "width": 264,
        "height": 412,
        "content": "## Input Section\nReceives OMR sheet image via webhook and sets up correct answer key for comparison."
      },
      "typeVersion": 1
    },
    {
      "id": "b13f3985-fec0-41e0-95fa-5fa7501fcbb1",
      "name": "AI Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        1696
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 412,
        "content": "## AI Processing\nGemini AI analyzes the image to extract student details and marked answers from bubbles."
      },
      "typeVersion": 1
    },
    {
      "id": "c75ce701-c5ac-441d-a9e2-2d3d2d29e284",
      "name": "Answer Comparison",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        1696
      ],
      "parameters": {
        "color": 7,
        "width": 408,
        "height": 412,
        "content": "## Answer Comparison\n\nMerges student answers with correct answers, then calculates score and generates detailed results."
      },
      "typeVersion": 1
    },
    {
      "id": "2622777b-a6b4-41c9-a33f-58fbaa7707a0",
      "name": "Output Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1552,
        1696
      ],
      "parameters": {
        "color": 7,
        "width": 644,
        "height": 412,
        "content": "## Output Section\nSaves results to Google Sheets and returns JSON response with complete evaluation data."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Merge Answer": {
      "main": [
        [
          {
            "node": "Calculate Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze image": {
      "main": [
        [
          {
            "node": "Convert User Ans in Formate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Result": {
      "main": [
        [
          {
            "node": "Append Result in Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Student Ans Img": {
      "main": [
        [
          {
            "node": "Analyze image",
            "type": "main",
            "index": 0
          },
          {
            "node": "Set Your Correct Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Result in Sheet": {
      "main": [
        [
          {
            "node": "Send Result Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Your Correct Answer": {
      "main": [
        [
          {
            "node": "Merge Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert User Ans in Formate": {
      "main": [
        [
          {
            "node": "Merge Answer",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

Manual checking of OMR (Optical Mark Recognition) answer sheets is time-consuming, error-prone, and difficult to scale—especially for schools, coaching institutes, and exam centers. This workflow automates OMR evaluation end-to-end using AI, from reading a scanned answer sheet…

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

Instantly map all internal URLs, perform AI-powered (ChatGPT) analysis, and deliver results in HTML via webhook, Google Sheets, or email. All from your own n8n instance!

OpenAI, HTTP Request, XML +3
AI & RAG

Watch on Youtube▶️

HTTP Request, Email Send, Google Sheets +3
AI & RAG

This workflow is perfect for marketing agencies, SEO consultants, and growth specialists who need to scale personalized outreach without spending hours on manual research.

Google Sheets, HTTP Request, OpenAI
AI & RAG

This is for creators who run Patreon and/or Kofi pages, support donations and want to automate their communication process.

Gmail, Google Sheets, OpenAI
AI & RAG

Imagine your recruitment process transformed into a sleek, efficient, AI-powered assembly line for talent. That's exactly what this system creates. It automates the heavy lifting, allowing your human

Google Sheets, OpenAI, Gmail +2