This workflow corresponds to n8n.io template #gemini-ingestion-engine-v2 — 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 →
{
"name": "Gemini File Search Ingestion Engine v2",
"nodes": [
{
"parameters": {},
"id": "trigger-manual",
"name": "Manual Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
240,
200
],
"notes": "For direct use: Click 'Test Workflow' to upload a document."
},
{
"parameters": {
"httpMethod": "POST",
"path": "gemini-ingestion",
"responseMode": "responseNode",
"options": {}
},
"id": "trigger-webhook",
"name": "Webhook Start",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
400
],
"notes": "For automation: Called by Drive Watcher workflow.\nExpects: file_binary (base64), file_name, doc_type, api_key, store_id"
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"id": "api-key",
"name": "api_key",
"value": "YOUR_GEMINI_API_KEY_HERE",
"type": "string"
},
{
"id": "store-id",
"name": "store_id",
"value": "fileSearchStores/YOUR_STORE_ID_HERE",
"type": "string"
},
{
"id": "file-path",
"name": "file_path",
"value": "/path/to/your/document.pdf",
"type": "string"
},
{
"id": "doc-type",
"name": "doc_type",
"value": "general",
"type": "string"
}
]
}
},
"id": "set-config-manual",
"name": "Your Settings (Manual)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
460,
200
],
"notes": "EDIT THIS NODE for manual uploads:\n\n1. api_key: Get from aistudio.google.dev\n2. store_id: Your Gemini File Search store ID\n3. file_path: Full path to your document\n4. doc_type: One of: policy, faq, procedure, product, or general"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// Process webhook input and extract config\nconst input = $input.first().json;\n\n// Extract config from webhook body\nconst config = {\n api_key: input.api_key || input.body?.api_key,\n store_id: input.store_id || input.body?.store_id,\n doc_type: input.doc_type || input.body?.doc_type || 'general',\n file_name: input.file_name || input.body?.file_name || 'uploaded_document',\n // Flag to indicate this came from webhook\n source: 'webhook'\n};\n\n// Get binary data from webhook\nconst binaryData = input.file_binary || input.body?.file_binary;\nconst mimeType = input.mime_type || input.body?.mime_type || 'application/octet-stream';\n\nif (!binaryData) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'NO_FILE_DATA',\n message: 'No file data received in webhook request.',\n troubleshooting: [\n 'Ensure file_binary is included in the request body',\n 'File should be base64 encoded'\n ]\n }\n }];\n}\n\nreturn [{\n json: config,\n binary: {\n data: {\n data: binaryData,\n mimeType: mimeType,\n fileName: config.file_name\n }\n }\n}];"
},
"id": "code-webhook-config",
"name": "Process Webhook Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
400
],
"notes": "Extracts configuration and binary data from webhook request."
},
{
"parameters": {
"fileSelector": "={{ $json.file_path }}",
"options": {}
},
"id": "read-file",
"name": "Read Your Document",
"type": "n8n-nodes-base.readBinaryFiles",
"typeVersion": 1,
"position": [
680,
200
],
"notes": "Reads the file from your computer (manual mode only)."
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"id": "merge-inputs",
"name": "Merge Inputs",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
900,
300
],
"notes": "Combines manual and webhook inputs into single flow."
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// Gemini File Search Ingestion Engine v2\n// Supports both manual file path input AND webhook binary input\n\nconst items = $input.all();\n\n// Find the item with binary data and config\nlet binaryData = null;\nlet config = {};\n\nfor (const item of items) {\n // Check for config from manual path\n if (item.json.api_key && item.json.file_path && !item.json.source) {\n config = {\n api_key: item.json.api_key,\n store_id: item.json.store_id,\n doc_type: item.json.doc_type || 'general',\n source: 'manual'\n };\n }\n \n // Check for config from webhook\n if (item.json.source === 'webhook') {\n config = {\n api_key: item.json.api_key,\n store_id: item.json.store_id,\n doc_type: item.json.doc_type || 'general',\n file_name: item.json.file_name,\n source: 'webhook'\n };\n }\n \n // Check for binary data\n if (item.binary && item.binary.data) {\n binaryData = item.binary.data;\n }\n}\n\nconst apiKey = config.api_key;\nconst storeId = config.store_id;\nconst docType = config.doc_type || 'general';\n\n// Get file from binary data\nif (!binaryData) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'NO_FILE',\n message: 'No file data found. Check that your file path is correct.',\n troubleshooting: [\n 'Verify the file exists at the specified path',\n 'Make sure the path is absolute (starts with /)',\n 'Check file permissions'\n ]\n }\n }];\n}\n\nconst fileBuffer = Buffer.from(binaryData.data, 'base64');\nconst mimeType = binaryData.mimeType;\nconst fileName = config.file_name || binaryData.fileName || 'document';\nconst fileSize = fileBuffer.length;\n\n// Validate file type\nconst validTypes = {\n 'application/pdf': 'PDF',\n 'text/plain': 'TXT',\n 'text/markdown': 'Markdown',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX'\n};\n\nif (!validTypes[mimeType]) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'INVALID_FILE_TYPE',\n message: `File type not supported: ${mimeType}`,\n supported_types: 'PDF, TXT, MD, DOCX',\n troubleshooting: [\n 'Convert your file to one of the supported formats',\n 'PDF is recommended for best results'\n ]\n }\n }];\n}\n\n// Validate file size (100MB limit)\nconst maxSize = 100 * 1024 * 1024;\nif (fileSize > maxSize) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'FILE_TOO_LARGE',\n message: `File is ${(fileSize / 1024 / 1024).toFixed(2)}MB. Maximum is 100MB.`,\n troubleshooting: [\n 'Split the document into smaller files',\n 'Remove images or compress the PDF'\n ]\n }\n }];\n}\n\n// Validate API key format\nif (!apiKey || apiKey === 'YOUR_GEMINI_API_KEY_HERE' || apiKey.length < 20) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'INVALID_API_KEY',\n message: 'Please add your Gemini API key in the \"Your Settings\" node.',\n troubleshooting: [\n 'Go to aistudio.google.dev to get your API key',\n 'Copy the full API key into the api_key field'\n ]\n }\n }];\n}\n\n// Validate store ID format\nif (!storeId || !storeId.startsWith('fileSearchStores/') || storeId === 'fileSearchStores/YOUR_STORE_ID_HERE') {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'INVALID_STORE_ID',\n message: 'Please add your File Search Store ID in the \"Your Settings\" node.',\n troubleshooting: [\n 'Store ID should start with \"fileSearchStores/\"',\n 'Create a store first using the setup instructions',\n 'Example format: fileSearchStores/abc123xyz'\n ]\n }\n }];\n}\n\n// Prepare metadata\nconst uploadDate = new Date().toISOString().split('T')[0];\nconst metadata = [\n { key: 'version', string_value: '1.0' },\n { key: 'status', string_value: 'current' },\n { key: 'doc_type', string_value: docType },\n { key: 'upload_date', string_value: uploadDate },\n { key: 'file_name', string_value: fileName }\n];\n\ntry {\n // Step 1: Initiate resumable upload\n const initUrl = `https://generativelanguage.googleapis.com/upload/v1beta/${storeId}:uploadToFileSearchStore?key=${apiKey}`;\n \n const initResponse = await fetch(initUrl, {\n method: 'POST',\n headers: {\n 'X-Goog-Upload-Protocol': 'resumable',\n 'X-Goog-Upload-Command': 'start',\n 'X-Goog-Upload-Header-Content-Length': String(fileSize),\n 'X-Goog-Upload-Header-Content-Type': mimeType,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n file_search_store_file: {\n display_name: fileName,\n custom_metadata: metadata\n }\n })\n });\n\n if (!initResponse.ok) {\n const errorText = await initResponse.text();\n let errorCode = 'API_ERROR';\n let message = `Gemini API error: ${errorText}`;\n let troubleshooting = ['Check your API key is valid', 'Verify your store ID is correct'];\n \n if (initResponse.status === 401 || initResponse.status === 403) {\n errorCode = 'AUTH_FAILURE';\n message = 'Authentication failed. Your API key may be invalid or expired.';\n troubleshooting = [\n 'Go to aistudio.google.dev and verify your API key',\n 'Generate a new API key if needed',\n 'Make sure the Gemini API is enabled for your project'\n ];\n } else if (initResponse.status === 404) {\n errorCode = 'STORE_NOT_FOUND';\n message = 'File Search Store not found. Check your store ID.';\n troubleshooting = [\n 'Verify your store_id starts with \"fileSearchStores/\"',\n 'Make sure the store exists (create one if needed)',\n 'Check for typos in the store ID'\n ];\n } else if (initResponse.status === 429) {\n errorCode = 'QUOTA_EXCEEDED';\n message = 'Rate limit reached. Wait a moment and try again.';\n troubleshooting = [\n 'Wait 60 seconds before retrying',\n 'Check your API quota in Google Cloud Console',\n 'Consider upgrading your plan if you hit limits frequently'\n ];\n }\n \n return [{\n json: {\n status: 'ERROR',\n error_code: errorCode,\n message: message,\n http_status: initResponse.status,\n troubleshooting: troubleshooting\n }\n }];\n }\n\n const uploadUrl = initResponse.headers.get('x-goog-upload-url');\n if (!uploadUrl) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'NO_UPLOAD_URL',\n message: 'Failed to get upload URL from Gemini API.',\n troubleshooting: [\n 'This is unusual - try again in a few minutes',\n 'Check Gemini API status page for outages'\n ]\n }\n }];\n }\n\n // Step 2: Upload file bytes\n const uploadResponse = await fetch(uploadUrl, {\n method: 'PUT',\n headers: {\n 'Content-Length': String(fileSize),\n 'X-Goog-Upload-Offset': '0',\n 'X-Goog-Upload-Command': 'upload, finalize'\n },\n body: fileBuffer\n });\n\n if (!uploadResponse.ok) {\n const errorText = await uploadResponse.text();\n return [{\n json: {\n status: 'ERROR',\n error_code: 'UPLOAD_FAILED',\n message: `Upload failed: ${errorText}`,\n http_status: uploadResponse.status,\n troubleshooting: [\n 'Try with a smaller file',\n 'Check your internet connection',\n 'Try again in a few minutes'\n ]\n }\n }];\n }\n\n const result = await uploadResponse.json();\n\n // Return success with all relevant info\n return [{\n json: {\n status: 'SUCCESS',\n message: 'Document uploaded successfully!',\n file_id: result.name,\n file_name: fileName,\n file_type: validTypes[mimeType],\n mime_type: mimeType,\n size_bytes: fileSize,\n size_formatted: `${(fileSize / 1024).toFixed(1)} KB`,\n store_id: storeId,\n source: config.source || 'manual',\n metadata: {\n version: '1.0',\n status: 'current',\n doc_type: docType,\n upload_date: uploadDate\n },\n // Pass to verification step\n _verification: {\n api_key: apiKey,\n store_id: storeId,\n file_id: result.name\n }\n }\n }];\n\n} catch (error) {\n return [{\n json: {\n status: 'ERROR',\n error_code: 'UNEXPECTED_ERROR',\n message: error.message || 'An unexpected error occurred',\n troubleshooting: [\n 'Check your internet connection',\n 'Verify your API key and store ID',\n 'Try again in a few minutes',\n 'If the problem persists, check the N8N execution logs'\n ]\n }\n }];\n}"
},
"id": "code-upload",
"name": "Upload to Gemini",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
300
],
"notes": "This node validates your file and uploads it to Gemini File Search.\n\nIt handles:\n- File type validation (PDF, TXT, MD, DOCX)\n- File size check (max 100MB)\n- API authentication\n- Resumable upload protocol\n- Error handling with clear messages\n\nSupports both manual (file path) and webhook (binary) inputs."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "condition-success",
"leftValue": "={{ $json.status }}",
"rightValue": "SUCCESS",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
},
"id": "if-success",
"name": "Upload OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1340,
300
]
},
{
"parameters": {
"method": "GET",
"url": "=https://generativelanguage.googleapis.com/v1beta/{{ $json._verification.store_id }}/files?key={{ $json._verification.api_key }}",
"options": {}
},
"id": "http-verify",
"name": "Verify Upload",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
200
],
"notes": "Queries the store to confirm the file was added successfully."
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"id": "final-status",
"name": "status",
"value": "={{ $('Upload to Gemini').item.json.status }}",
"type": "string"
},
{
"id": "final-message",
"name": "message",
"value": "={{ $('Upload to Gemini').item.json.message }}",
"type": "string"
},
{
"id": "final-file-id",
"name": "file_id",
"value": "={{ $('Upload to Gemini').item.json.file_id }}",
"type": "string"
},
{
"id": "final-file-name",
"name": "file_name",
"value": "={{ $('Upload to Gemini').item.json.file_name }}",
"type": "string"
},
{
"id": "final-file-type",
"name": "file_type",
"value": "={{ $('Upload to Gemini').item.json.file_type }}",
"type": "string"
},
{
"id": "final-size",
"name": "size",
"value": "={{ $('Upload to Gemini').item.json.size_formatted }}",
"type": "string"
},
{
"id": "final-store",
"name": "store_id",
"value": "={{ $('Upload to Gemini').item.json.store_id }}",
"type": "string"
},
{
"id": "final-source",
"name": "source",
"value": "={{ $('Upload to Gemini').item.json.source }}",
"type": "string"
},
{
"id": "final-metadata",
"name": "metadata",
"value": "={{ $('Upload to Gemini').item.json.metadata }}",
"type": "object"
},
{
"id": "final-verified",
"name": "verified",
"value": "={{ $json.files ? $json.files.some(f => f.name === $('Upload to Gemini').item.json.file_id) : false }}",
"type": "boolean"
},
{
"id": "final-next",
"name": "next_steps",
"value": "Your document is now in your knowledge base! The Librarian agent can now search and retrieve it.",
"type": "string"
}
]
}
},
"id": "set-success",
"name": "Success Result",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1780,
200
],
"notes": "Formats the final success response with verification status."
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"id": "error-status",
"name": "status",
"value": "={{ $json.status }}",
"type": "string"
},
{
"id": "error-code",
"name": "error_code",
"value": "={{ $json.error_code }}",
"type": "string"
},
{
"id": "error-message",
"name": "message",
"value": "={{ $json.message }}",
"type": "string"
},
{
"id": "error-troubleshooting",
"name": "troubleshooting",
"value": "={{ $json.troubleshooting }}",
"type": "array"
}
]
}
},
"id": "set-error",
"name": "Error Result",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1560,
400
],
"notes": "Formats the error response with troubleshooting guidance."
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "webhook-response-success",
"name": "Webhook Response (Success)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2000,
200
],
"notes": "Returns success response to webhook caller."
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 400
}
},
"id": "webhook-response-error",
"name": "Webhook Response (Error)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1780,
400
],
"notes": "Returns error response to webhook caller."
}
],
"connections": {
"Manual Start": {
"main": [
[
{
"node": "Your Settings (Manual)",
"type": "main",
"index": 0
}
]
]
},
"Webhook Start": {
"main": [
[
{
"node": "Process Webhook Input",
"type": "main",
"index": 0
}
]
]
},
"Your Settings (Manual)": {
"main": [
[
{
"node": "Read Your Document",
"type": "main",
"index": 0
}
]
]
},
"Process Webhook Input": {
"main": [
[
{
"node": "Merge Inputs",
"type": "main",
"index": 1
}
]
]
},
"Read Your Document": {
"main": [
[
{
"node": "Merge Inputs",
"type": "main",
"index": 0
}
]
]
},
"Merge Inputs": {
"main": [
[
{
"node": "Upload to Gemini",
"type": "main",
"index": 0
}
]
]
},
"Upload to Gemini": {
"main": [
[
{
"node": "Upload OK?",
"type": "main",
"index": 0
}
]
]
},
"Upload OK?": {
"main": [
[
{
"node": "Verify Upload",
"type": "main",
"index": 0
}
],
[
{
"node": "Error Result",
"type": "main",
"index": 0
}
]
]
},
"Verify Upload": {
"main": [
[
{
"node": "Success Result",
"type": "main",
"index": 0
}
]
]
},
"Success Result": {
"main": [
[
{
"node": "Webhook Response (Success)",
"type": "main",
"index": 0
}
]
]
},
"Error Result": {
"main": [
[
{
"node": "Webhook Response (Error)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateId": "gemini-ingestion-engine-v2"
},
"versionId": "v2-2025-11-27",
"tags": [
{
"name": "Gemini",
"createdAt": "2025-11-27T00:00:00.000Z",
"updatedAt": "2025-11-27T00:00:00.000Z"
},
{
"name": "RAG",
"createdAt": "2025-11-27T00:00:00.000Z",
"updatedAt": "2025-11-27T00:00:00.000Z"
},
{
"name": "Week-3",
"createdAt": "2025-11-27T00:00:00.000Z",
"updatedAt": "2025-11-27T00:00:00.000Z"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Gemini File Search Ingestion Engine v2. Uses readBinaryFiles, httpRequest. Event-driven trigger; 13 nodes.
Source: https://github.com/8Dvibes/mindvalley-ai-mastery-students/blob/main/workflows/gemini-ingestion-engine-v2-2025-11-27.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.
Gemini File Search Ingestion Engine v2. Uses readBinaryFiles, httpRequest. Event-driven trigger; 13 nodes.
This pipeline is the first part of "Hybrid Search with Qdrant & n8n, Legal AI"*. The second part, "Hybrid Search with Qdrant & n8n, Legal AI: Retrieval", covers retrieval and simple evaluation.*
[1/3 - anomaly detection] [1/2 - KNN classification] Batch upload dataset to Qdrant (crops dataset). Uses manualTrigger, googleCloudStorage, httpRequest, stickyNote. Event-driven trigger; 25 nodes.
[1/3 - anomaly detection] [1/2 - KNN classification] Batch upload dataset to Qdrant (crops dataset). Uses manualTrigger, googleCloudStorage, httpRequest, stickyNote. Event-driven trigger; 25 nodes.
Workflows from the webinar "Build production-ready AI Agents with Qdrant and n8n".