AutomationFlowsAI & RAG › Upload Exam

Upload Exam

Upload Exam. Uses googleDrive, httpRequest, openAi, postgres. Webhook trigger; 18 nodes.

Webhook trigger★★★★☆ complexityAI-powered18 nodesGoogle DriveHTTP RequestOpenAIPostgres
AI & RAG Trigger: Webhook Nodes: 18 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Google Drive → HTTP Request 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
{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "upload-exam-api",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -1472,
        208
      ],
      "id": "95728df6-0c12-4a85-b0d7-3549ffe8a23d",
      "name": "On API submission",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst body = item.json.body || item.json || {};\nconst binary = item.binary || {};\n\nconst questionFile = binary.question_file;\nconst answerFile = binary.answer_file;\n\nif (!questionFile || !answerFile) {\n  throw new Error('Thieu file question_file hoac answer_file. Gui multipart/form-data tu API backend.');\n}\n\nconst examId = Number(body.exam_id);\nif (!Number.isInteger(examId) || examId <= 0) {\n  throw new Error('exam_id khong hop le.');\n}\n\nconst examCode = String(body.exam_code || '').trim();\nif (!examCode) {\n  throw new Error('Thieu exam_code.');\n}\n\nconst title = String(body.title || '').trim();\nif (!title) {\n  throw new Error('Thieu title de thi.');\n}\n\nconst examType = String(body.exam_type || '').trim();\nif (!examType) {\n  throw new Error('Thieu exam_type.');\n}\n\nconst teacherId = Number(body.teacher_id);\nif (!Number.isInteger(teacherId) || teacherId <= 0) {\n  throw new Error('teacher_id khong hop le.');\n}\n\nconst examRound = String(body.exam_round || '').trim();\n\nconst questionFilename = String(questionFile.fileName || questionFile.filename || '').trim();\nconst answerFilename = String(answerFile.fileName || answerFile.filename || '').trim();\nconst answerName = answerFilename.toLowerCase();\nconst answerMime = String(answerFile.mimeType || answerFile.mimetype || '').toLowerCase();\nconst supportedExtensions = ['.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx'];\nconst hasSupportedExtension = supportedExtensions.some((extension) => answerName.endsWith(extension));\nconst supportedMimeTypes = [\n  'application/pdf',\n  'image/png',\n  'image/jpeg',\n  'image/webp',\n  'application/msword',\n  'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n];\n\nif (!hasSupportedExtension && !supportedMimeTypes.includes(answerMime)) {\n  throw new Error('answer_file khong thuoc dinh dang OCR duoc ho tro.');\n}\n\nreturn [{\n  json: {\n    exam_id: examId,\n    exam_code: examCode,\n    title,\n    description: String(body.description || '').trim(),\n    class_code: String(body.class_code || '').trim(),\n    subject_code: String(body.subject_code || '').trim(),\n    subject_name: String(body.subject_name || '').trim(),\n    exam_type: examType,\n    exam_round: examRound,\n    teacher_id: teacherId,\n    question_filename: questionFilename,\n    answer_filename: answerFilename,\n    question_file_path: questionFilename || null,\n    answer_file_path: answerFilename || null\n  },\n  binary: {\n    question_file: questionFile,\n    answer_file: answerFile\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1232,
        224
      ],
      "id": "dd79d149-98a8-4253-bbea-21648d7af81b",
      "name": "Validate API payload"
    },
    {
      "parameters": {
        "jsCode": "const source = $('Validate API payload').first();\nreturn [{ json: { ...source.json, }, binary: { ...source.binary } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -960,
        224
      ],
      "id": "58019132-28d3-4fd9-9527-be4694fcb7d1",
      "name": "Restore validated payload"
    },
    {
      "parameters": {
        "inputDataFieldName": "question_file",
        "name": "={{ $binary.question_file.fileName || $binary.question_file.filename || 'question-file' }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "value": "1wOZ8pKLYH-mzXpKavQsH8Y7HuMGjkECP",
          "mode": "list",
          "cachedResultName": "n8n",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1wOZ8pKLYH-mzXpKavQsH8Y7HuMGjkECP"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -736,
        224
      ],
      "id": "1667b240-f539-4b26-bbb7-072e7a8b1282",
      "name": "Upload question file",
      "alwaysOutputData": true,
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Validate API payload').first();\nconst upload = $input.first();\nconst questionFilePath = upload.json.webViewLink || upload.json.webContentLink || upload.json.id || '';\nif (!questionFilePath) {\n  throw new Error('Khong lay duoc URL Google Drive cho question_file');\n}\nreturn [{ json: { ...meta.json, question_file_path: questionFilePath, question_drive_file_id: upload.json.id || null }, binary: { ...meta.binary } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -512,
        224
      ],
      "id": "3454c324-10cc-4ccf-9ec9-4d91fdf8606d",
      "name": "Restore payload after question upload"
    },
    {
      "parameters": {
        "inputDataFieldName": "answer_file",
        "name": "={{ $binary.answer_file.fileName || $binary.answer_file.filename || 'answer-file' }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "value": "1wOZ8pKLYH-mzXpKavQsH8Y7HuMGjkECP",
          "mode": "list",
          "cachedResultName": "n8n",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1wOZ8pKLYH-mzXpKavQsH8Y7HuMGjkECP"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -288,
        224
      ],
      "id": "842a6554-76c7-42d7-951c-4b7b87ace125",
      "name": "Upload answer file",
      "alwaysOutputData": true,
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Restore payload after question upload').first().json;\nconst binary = $('Validate API payload').first().binary || {};\nconst answerUpload = $input.first();\nconst answerFilePath = answerUpload.json.webViewLink || answerUpload.json.webContentLink || answerUpload.json.id || '';\nif (!answerFilePath) {\n  throw new Error('Khong lay duoc URL Google Drive cho answer_file');\n}\nreturn [{ json: { ...meta, answer_file_path: answerFilePath, answer_drive_file_id: answerUpload.json.id || null }, binary: { ...binary } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -160,
        -48
      ],
      "id": "877cafad-9011-49a6-8887-ec3d150d51a4",
      "name": "Attach Drive uploads"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.mistral.ai/v1/files",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "mistralCloudApi",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "purpose",
              "value": "ocr"
            },
            {
              "parameterType": "formBinaryData",
              "name": "file",
              "inputDataFieldName": "answer_file"
            }
          ]
        },
        "options": {}
      },
      "id": "a953fe4a-f8c2-416f-bb1b-043beea33ac7",
      "name": "Mistral Upload",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -64,
        224
      ],
      "typeVersion": 4.2,
      "credentials": {
        "mistralCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "=https://api.mistral.ai/v1/files/{{ $json.id }}/url",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "mistralCloudApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "expiry",
              "value": "24"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "612b35b4-baab-45ad-9ec5-af8a6239a08b",
      "name": "Mistral Signed URL",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        160,
        224
      ],
      "typeVersion": 4.2,
      "credentials": {
        "mistralCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.mistral.ai/v1/ocr",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "mistralCloudApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"mistral-ocr-latest\",\n  \"document\": {\n    \"type\": \"document_url\",\n    \"document_url\": \"{{ $json.url }}\"\n  },\n  \"include_image_base64\": false\n}",
        "options": {}
      },
      "id": "223ea895-ae88-4f8b-88ee-c4b00edebc6f",
      "name": "Mistral DOC OCR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        384,
        224
      ],
      "typeVersion": 4.2,
      "credentials": {
        "mistralCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8db56b6d-6f85-4dd4-b1b4-780bcc65f53d",
              "name": "markdown",
              "value": "={{ ($json.pages || []).map(page => page.markdown || '').join('\\n\\n').trim() }}",
              "type": "string"
            },
            {
              "id": "ef803507-f63b-48b6-adc6-fcf0de48805a",
              "name": "page_count",
              "value": "={{ ($json.pages || []).length }}",
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        608,
        224
      ],
      "id": "bf293835-5dd0-4b7d-9fa4-dcad2a234656",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Attach Drive uploads').first().json;\nconst ocr = $('Edit Fields').first().json;\nreturn [{ json: { ...meta, markdown: ocr.markdown || '', page_count: Number(ocr.page_count || 0) } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        832,
        224
      ],
      "id": "0d66a219-5636-49e1-a572-1290fcc6e8da",
      "name": "Restore OCR context"
    },
    {
      "parameters": {
        "jsCode": "const source = $('Restore OCR context').first();\nreturn [{ json: { ...source.json } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        224
      ],
      "id": "427576ae-5575-4b05-8ccf-1309f0430d89",
      "name": "Restore extracted markdown"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4.1-mini",
          "mode": "list",
          "cachedResultName": "GPT-4.1-MINI"
        },
        "responses": {
          "values": [
            {
              "content": "=Ban la AI trich xuat dap an va rubric tu OCR markdown cua dap an mau.\n\nMuc tieu:\nTra ve DUY NHAT mot JSON object hop le theo dung contract ben duoi, khong markdown, khong giai thich them.\n\nContract output:\n{\n  \"questions\": [\n    {\n      \"question_no\": \"1\",\n      \"question_text\": \"...\",\n      \"question_type\": \"essay\" | \"multiple_choice\" | \"unknown\",\n      \"options\": [\"A\", \"B\", \"C\", \"D\"],\n      \"expected_answer\": \"...\",\n      \"rubrics\": [\n        {\n          \"key\": \"...\",\n          \"score\": 0.5\n        }\n      ],\n      \"max_score\": 2\n    }\n  ],\n  \"total_max_score\": 10\n}\n\nQuy tac:\n1. Neu la tu luan, options de [] va question_type = \"essay\".\n2. Neu la trac nghiem, tach phuong an vao options va expected_answer la dap an dung neu OCR nhin thay.\n3. Neu khong xac dinh duoc so cau, dat question_no theo thu tu 1, 2, 3...\n4. max_score la so, neu khong ro thi dung 1 cho trac nghiem va 2 cho tu luan.\n5. rubrics la mang cac y cham chinh; neu khong co thi tra ve [].\n6. Bo phan dau mo rong khong lien quan nhu ten truong, ten mon, thoi gian thi, chi dan hanh chinh.\n7. total_max_score la tong max_score cua cac cau neu co the tinh duoc, neu khong thi tra ve null.\n8. Neu OCR rat kem va khong tach duoc cau hoi, tra ve {\"questions\": [], \"total_max_score\": null}.\n\nInput OCR markdown:\n{{ $json.markdown }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        1424,
        112
      ],
      "id": "a754341a-e34c-4eb6-a56a-64620f753eec",
      "name": "Message a model",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8c670303-b61c-4d31-923f-cb5ac2fdcbcc",
              "name": "text",
              "value": "={{ $json.output[0].content[0].text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1728,
        224
      ],
      "id": "b89bf976-c50e-47b8-8a2f-e8cd3433fcab",
      "name": "Edit Fields1"
    },
    {
      "parameters": {
        "jsCode": "const rawText = String($input.first().json.text || '').trim();\nif (!rawText) {\n  throw new Error('Model khong tra ve noi dung extract JSON.');\n}\nlet parsed;\ntry {\n  parsed = JSON.parse(rawText);\n} catch (error) {\n  throw new Error(`Khong parse duoc JSON extract tu model: ${error.message}`);\n}\nif (Array.isArray(parsed)) {\n  parsed = { questions: parsed };\n}\nif (!parsed || typeof parsed !== 'object') {\n  throw new Error('Output extract khong dung dinh dang object.');\n}\nconst rawQuestions = Array.isArray(parsed.questions) ? parsed.questions : [];\nconst normalizedQuestions = rawQuestions.map((question, index) => {\n  const questionText = String(question.question_text || '').trim();\n  const expectedAnswer = String(question.expected_answer || '').trim();\n  const questionNo = String(question.question_no || index + 1).trim();\n  const options = Array.isArray(question.options) ? question.options.map(option => String(option || '').trim()).filter(Boolean) : [];\n  const rubrics = Array.isArray(question.rubrics) ? question.rubrics.map((rubric) => {\n    const key = String(rubric?.key || rubric?.content || '').trim();\n    const score = Number(rubric?.score);\n    return { key, score: Number.isFinite(score) && score >= 0 ? score : null };\n  }).filter((rubric) => rubric.key) : [];\n  const rawMaxScore = Number(question.max_score);\n  const questionType = String(question.question_type || (options.length ? 'multiple_choice' : 'essay')).trim() || 'unknown';\n  const defaultScore = questionType === 'multiple_choice' ? 1 : 2;\n  return {\n    question_no: questionNo || String(index + 1),\n    question_text: questionText,\n    question_type: questionType,\n    options,\n    expected_answer: expectedAnswer,\n    rubrics,\n    max_score: Number.isFinite(rawMaxScore) && rawMaxScore > 0 ? rawMaxScore : defaultScore\n  };\n}).filter((question) => question.question_text || question.expected_answer || question.options.length || question.rubrics.length);\nconst computedTotalMaxScore = normalizedQuestions.reduce((sum, question) => sum + Number(question.max_score || 0), 0);\nconst rawTotalMaxScore = Number(parsed.total_max_score);\nreturn [{ json: { extract_json: { questions: normalizedQuestions, total_max_score: Number.isFinite(rawTotalMaxScore) && rawTotalMaxScore > 0 ? rawTotalMaxScore : normalizedQuestions.length ? Number(computedTotalMaxScore.toFixed(2)) : null }, questions_count: normalizedQuestions.length } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1952,
        224
      ],
      "id": "86304e7e-be7f-424b-a44e-9d38dcad2b81",
      "name": "exam_detail"
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Restore extracted markdown').first().json;\nconst extract = $('exam_detail').first().json.extract_json;\nfunction escapeLiteral(value) { return String(value ?? '').replace(/'/g, \"''\"); }\nfunction sqlNumber(value) { const number = Number(value); return Number.isFinite(number) ? String(number) : 'NULL'; }\nfunction sqlJson(value) { if (value === undefined || value === null) { return 'NULL'; } return `'${escapeLiteral(JSON.stringify(value))}'::jsonb`; }\nconst status = 'active';\nconst sql = `\nUPDATE exams\nSET\n  answer_extract = ${sqlJson(extract)},\n  status = '${status}',\n  updated_at = NOW(),\n  updated_by = ${sqlNumber(meta.teacher_id)}\nWHERE id = ${sqlNumber(meta.exam_id)};`;\nreturn [{ json: { exam_id: meta.exam_id, exam_code: meta.exam_code, answer_extract: extract, status, sql } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2176,
        224
      ],
      "id": "72f724b6-20f0-4773-b6d8-7296a37dc746",
      "name": "Build final SQL"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "{{ $json.sql }}",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2400,
        224
      ],
      "id": "1f90e179-5973-4c54-9a2e-113239a473d4",
      "name": "Execute a SQL query",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "On API submission": {
      "main": [
        [
          {
            "node": "Validate API payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate API payload": {
      "main": [
        [
          {
            "node": "Restore validated payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore validated payload": {
      "main": [
        [
          {
            "node": "Upload question file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload question file": {
      "main": [
        [
          {
            "node": "Restore payload after question upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore payload after question upload": {
      "main": [
        [
          {
            "node": "Upload answer file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload answer file": {
      "main": [
        [
          {
            "node": "Attach Drive uploads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Drive uploads": {
      "main": [
        [
          {
            "node": "Mistral Upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mistral Upload": {
      "main": [
        [
          {
            "node": "Mistral Signed URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mistral Signed URL": {
      "main": [
        [
          {
            "node": "Mistral DOC OCR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mistral DOC OCR": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Restore OCR context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore OCR context": {
      "main": [
        [
          {
            "node": "Restore extracted markdown",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore extracted markdown": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Edit Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields1": {
      "main": [
        [
          {
            "node": "exam_detail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "exam_detail": {
      "main": [
        [
          {
            "node": "Build final SQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build final SQL": {
      "main": [
        [
          {
            "node": "Execute a SQL query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query": {
      "main": [
        []
      ]
    }
  }
}

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

Upload Exam. Uses googleDrive, httpRequest, openAi, postgres. Webhook trigger; 18 nodes.

Source: https://github.com/dungthieuIT98/n8n/blob/fe5ee6e139b528ed23ead2812e72d251d32d9e0c/workflows/upload_exam.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

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

Postgres, OpenAI, HTTP Request +1
AI & RAG

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

Postgres, HTTP Request, OpenAI
AI & RAG

User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow

Postgres, HTTP Request, OpenAI +2
AI & RAG

Transforms provider documentation (URLs) into an auditable, enforceable multicloud security control baseline. It: Fetches and sanitizes HTML Uses AI to extract security requirements* (strict 3-line TX

HTTP Request, OpenAI, Google Drive
AI & RAG

Listens for completed Fireflies transcripts, qualifies whether a proposal is needed using OpenAI, drafts structured proposal content, populates a Google Doc template, converts to PDF, and sends it to

HTTP Request, OpenAI, Google Drive +3