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 →
{
"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.
googleDriveOAuth2ApimistralCloudApiopenAiApipostgres
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Eu Clara – Funil Kiwify Completo. Uses postgres, openAi, httpRequest, gmail. Webhook trigger; 70 nodes.
Lua Nova - Sistema Completo. Uses postgres, httpRequest, openAi. Webhook trigger; 55 nodes.
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
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
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