Workflow: Gmail to Vector Embeddings with PGVector and Ollama
Integrations: code, gmail, gmailTrigger, if, manualTrigger, noOp, postgres, set, splitInBatches, splitOut, stickyNote, stopAndError

{"id": "ZiIoKEClTk83g1Jt", "meta": {"instanceId": "workflow-8bfd2626", "versionId": "1.0.0", "createdAt": "2025-09-29T07:07:54.451560", "updatedAt": "2025-09-29T07:07:54.451597", "owner": "n8n-user", "license": "MIT", "category": "automation", "status": "active", "priority": "high", "environment": "production"}, "name": "Gmail to Vector Embeddings with PGVector and Ollama", "tags": ["automation", "n8n", "production-ready", "excellent", "optimized"], "nodes": [{"id": "162b1a8b-2471-4880-9fcb-7f2dcfe175a8", "name": "Embeddings Ollama", "type": "n8n-nodes-base.noOp", "position": [1920, -100], "parameters": {"model": "nomic-embed-text:latest"}, "credentials": {}, "typeVersion": 1, "notes": "This embeddingsOllama node performs automated tasks as part of the workflow."}, {"id": "49eb04b0-3b54-499c-ba46-3251102a4017", "name": "Default Data Loader", "type": "n8n-nodes-base.noOp", "position": [2040, -97.5], "parameters": {"options": {"metadata": {"metadataValues": [{"name": "emails_metadata.id", "value": "={{ $('Extract email fields').item.json.email_id }}"}, {"name": "emails_metadata.thread_id", "value": "={{ $('Extract email fields').item.json.thread_id }}"}]}}, "jsonData": "={{ $('Extract email fields').item.json.email_text }}", "jsonMode": "expressionData"}, "typeVersion": 1, "notes": "This documentDefaultDataLoader node performs automated tasks as part of the workflow."}, {"id": "b4853472-6ac7-4da5-97b3-b22950ddff06", "name": "Recursive Character Text Splitter", "type": "n8n-nodes-base.noOp", "position": [2128, 100], "parameters": {"options": {}, "chunkSize": 2000, "chunkOverlap": 50}, "typeVersion": 1, "notes": "This textSplitterRecursiveCharacterTextSplitter node performs automated tasks as part of the workflow."}, {"id": "b189f134-f78e-438f-9189-2f2b276b487d", "name": "Gmail Trigger", "type": "n8n-nodes-base.gmailTrigger", "position": [1260, 280], "parameters": {"simple": false, "filters": {"labelIds": ["INBOX"]}, "options": {"downloadAttachments": true}, "pollTimes": {"item": [{"mode": "everyMinute"}]}}, "credentials": {}, "typeVersion": 1.2, "notes": "This gmailTrigger node performs automated tasks as part of the workflow."}, {"id": "81cba4c5-7762-483d-a076-3fa8799f70ce", "name": "Loop Over Items", "type": "n8n-nodes-base.splitInBatches", "position": [840, 40], "parameters": {"options": {}}, "typeVersion": 3, "notes": "This splitInBatches node performs automated tasks as part of the workflow."}, {"id": "f82243ad-6efd-4be2-bf4e-5001870ae854", "name": "Split Out", "type": "n8n-nodes-base.splitOut", "position": [640, 40], "parameters": {"options": {"destinationFieldName": "after"}, "fieldToSplitOut": "weeks"}, "typeVersion": 1, "notes": "This splitOut node performs automated tasks as part of the workflow."}, {"id": "2163d5ec-416f-4299-8a9d-10c26eaef32f", "name": "Was manually triggered?", "type": "n8n-nodes-base.if", "position": [2416, -145], "parameters": {"options": {}, "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "3cbc77e7-1796-4e1b-bbff-6391dd131336", "operator": {"type": "boolean", "operation": "false", "singleValue": true}, "leftValue": "={{ $('Manual Trigger').isExecuted }}", "rightValue": ""}]}}, "typeVersion": 2.2, "notes": "This if node performs automated tasks as part of the workflow."}, {"id": "76557325-c94e-47a9-9384-e6cbea94f67e", "name": "Manual Trigger", "type": "n8n-nodes-base.manualTrigger", "position": [0, 40], "parameters": {}, "typeVersion": 1, "notes": "This manualTrigger node performs automated tasks as part of the workflow."}, {"id": "b9400906-a458-4305-8805-bb6bea17396b", "name": "No Operation, do nothing", "type": "n8n-nodes-base.noOp", "position": [2636, -145], "parameters": {}, "typeVersion": 1, "notes": "This noOp node performs automated tasks as part of the workflow."}, {"id": "5f0aa7c2-85b3-4585-8c8c-727af27de61c", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [-60, -540], "parameters": {"color": 6, "width": 1440, "height": 780, "content": "## Bulk e-mail import\n\nPress the `Test workflow` button to run this once, and bulk import of all your e-mail\n\n### IMPORTANT\nSpecify your Gmail account creation date by editing the code node"}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "2b85d362-d40d-49c0-b3a7-27fdbee8e90b", "name": "Sticky Note1", "type": "n8n-nodes-base.stickyNote", "position": [360, -100], "parameters": {"width": 220, "height": 300, "content": "## Edit this \u2b07\ufe0f\nAnd specify your Gmail account creation date"}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "05a9dd25-ae36-4e3c-a249-787ee1047bff", "name": "Sticky Note2", "type": "n8n-nodes-base.stickyNote", "position": [740, 260], "parameters": {"color": 4, "width": 640, "height": 180, "content": "## Activate the workflow\nAnd this trigger will check for new mail, every minute"}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "3f19abc5-a165-49e8-b97e-233c47949e68", "name": "Set before and after dates", "type": "n8n-nodes-base.set", "position": [1040, -320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "48e8d703-e52a-46cc-bd72-9b0d3352091b", "name": "after", "type": "string", "value": "={{ $json.after }}"}, {"id": "a515cf56-9bc6-4724-a0ef-01a6159606f7", "name": "before", "type": "string", "value": "={{ DateTime.fromISO($json.after).plus(1, 'week').toISODate() }}"}]}}, "typeVersion": 3.4, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "af742b17-2086-4698-af3a-32cb7260f380", "name": "Extract email fields", "type": "n8n-nodes-base.set", "position": [1480, -320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "f818bad8-b000-499c-b137-de22dff4a343", "name": "email_text", "type": "string", "value": "={{ $json.text }}"}, {"id": "68c16520-4a26-4ea9-95f7-ee89b9f53c4f", "name": "email_from", "type": "string", "value": "={{ $json.from?.text ?? '' }}"}, {"id": "981f1f5b-ba2f-4153-966c-45bb6b535794", "name": "email_to", "type": "string", "value": "={{ $json.to?.text ?? '' }}"}, {"id": "b528dd23-a743-4a55-98df-e1ae823b29b3", "name": "date", "type": "string", "value": "={{ DateTime.fromISO($json.date).toISO() }}"}, {"id": "39081032-e503-470b-8d83-b5064238d037", "name": "email_id", "type": "string", "value": "={{ $json.id }}"}, {"id": "146e8e72-3c2c-4320-b93a-b109d2e46139", "name": "thread_id", "type": "string", "value": "={{ $json.threadId }}"}, {"id": "a49333a5-c565-4d46-8398-d423072b1e4d", "name": "email_subject", "type": "string", "value": "={{ $json.subject }}"}, {"id": "806cf930-450e-4221-8061-a71ec8bf9bbe", "name": "attachments", "type": "array", "value": "={{ Object.keys($binary).map(item => $binary[item].fileName).filter(item => !!item) }}"}, {"id": "30a38aaf-04c2-4286-99c9-8bb60ae8b317", "name": "email_cc", "type": "string", "value": "={{ $json.cc?.text ?? ''}}"}]}}, "typeVersion": 3.4, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "a51f5d5f-69c7-4153-be7f-492a8694629a", "name": "Sticky Note3", "type": "n8n-nodes-base.stickyNote", "position": [1640, -540], "parameters": {"width": 720, "height": 780, "content": "## Magic here \ud83e\ude84\n#### (not really, just statistics)\nE-mail is stored in a `emails_metadata` structured table, and also fed to the [`nomic-embed-text`]({{ $env.WEBHOOK_URL }} model to be stored in a `emails_embeddings` table as [vector embeddings]({{ $env.WEBHOOK_URL }} so similarity searches are possible.\n\nThe `email_id` field can be used to make the relation between the structured records and the vector embeddings, as it's stored in their metadata as `emails_metadata.id`.\nThis is also the case for `thread_id`."}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "809e9269-1275-4c87-8c7f-1840c76f5b22", "name": "Store structured", "type": "n8n-nodes-base.postgres", "onError": "continueErrorOutput", "position": [1700, -320], "parameters": {"table": {"__rl": true, "mode": "name", "value": "emails_metadata"}, "schema": {"__rl": true, "mode": "list", "value": "public"}, "columns": {"value": {}, "schema": [{"id": "email_id", "type": "string", "display": true, "removed": false, "required": true, "displayName": "email_id", "defaultMatch": false, "canBeUsedToMatch": true}, {"id": "thread_id", "type": "string", "display": true, "removed": false, "required": false, "displayName": "thread_id", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "email_from", "type": "string", "display": true, "removed": false, "required": false, "displayName": "email_from", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "email_to", "type": "string", "display": true, "removed": false, "required": false, "displayName": "email_to", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "email_cc", "type": "string", "display": true, "removed": false, "required": false, "displayName": "email_cc", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "date", "type": "dateTime", "display": true, "removed": false, "required": true, "displayName": "date", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "email_subject", "type": "string", "display": true, "removed": false, "required": false, "displayName": "email_subject", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "attachments", "type": "array", "display": true, "removed": false, "required": false, "displayName": "attachments", "defaultMatch": false, "canBeUsedToMatch": false}, {"id": "email_text", "type": "string", "display": true, "removed": false, "required": false, "displayName": "email_text", "defaultMatch": false, "canBeUsedToMatch": false}], "mappingMode": "autoMapInputData", "matchingColumns": ["email_id"], "attemptToConvertTypes": false, "convertFieldsToString": false}, "options": {"outputColumns": ["*"]}, "operation": "upsert"}, "credentials": {}, "typeVersion": 2.6, "notes": "This postgres node performs automated tasks as part of the workflow."}, {"id": "1c3dca79-381c-411b-8727-baa297e1ceda", "name": "Store vectorized", "type": "n8n-nodes-base.noOp", "onError": "continueRegularOutput", "position": [1936, -320], "parameters": {"mode": "insert", "options": {}, "tableName": "emails_embeddings"}, "credentials": {}, "typeVersion": 1.1, "notes": "This vectorStorePGVector node performs automated tasks as part of the workflow."}, {"id": "3b7e13b2-73e9-42e7-900c-59611fe5af32", "name": "Create the table", "type": "n8n-nodes-base.postgres", "position": [200, 40], "parameters": {"query": "CREATE TABLE IF NOT EXISTS public.emails_metadata (\n    email_id character varying(64) NOT NULL,\n    thread_id character varying(64),\n    email_from text,\n    email_to text,\n    email_cc text,\n    date timestamp with time zone NOT NULL,\n    email_subject text,\n    email_text text,\n    attachments text[]\n);\n", "options": {}, "operation": "executeQuery"}, "credentials": {}, "typeVersion": 2.6, "notes": "This postgres node performs automated tasks as part of the workflow."}, {"id": "19c55312-d1da-4d1e-8637-c5b08a9c1a2d", "name": "Explode interval into weeks", "type": "n8n-nodes-base.code", "position": [420, 40], "parameters": {"mode": "runOnceForEachItem", "jsCode": "// Edit this\nlet whenDidICreateMyGmailAccount = DateTime.fromISO('2013-11-01')\n\n// (don't edit further down)\nwhenDidICreateMyGmailAccount = whenDidICreateMyGmailAccount.set({day: 1})\nlet now = $now.set({day: 1})\nconst weeks = []\nwhile (Math.floor(Interval.fromDateTimes(whenDidICreateMyGmailAccount, now).length('weeks')) > -1) {\n  weeks.push(now.toISODate())\n  now = now.minus({weeks: 1})\n}\n\nreturn {json: { weeks }};"}, "typeVersion": 2, "notes": "This code node performs automated tasks as part of the workflow."}, {"id": "aed43a77-6d58-41ba-b0b0-fdd3e9fe777a", "name": "Get a batch of messages", "type": "n8n-nodes-base.gmail", "position": [1260, -320], "webhookId": "bace3678-df5b-4a9c-a1ef-1c219e3fd07b", "parameters": {"simple": false, "filters": {"receivedAfter": "={{ $json.after }}", "receivedBefore": "={{ $json.before }}"}, "options": {"downloadAttachments": true}, "operation": "getAll", "returnAll": true}, "credentials": {}, "typeVersion": 2.1, "notes": "This gmail node performs automated tasks as part of the workflow."}, {"id": "error-6587d4d5", "name": "Error Handler", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [1000, 400], "parameters": {"message": "Workflow execution error", "options": {}}}], "active": false, "pinData": {"Manual Trigger": [{"json": {}}]}, "settings": {"executionOrder": "v1", "saveManualExecutions": true, "callerPolicy": "workflowsFromSameOwner", "errorWorkflow": null, "timezone": "UTC", "executionTimeout": 3600, "maxExecutions": 1000, "retryOnFail": true, "retryCount": 3, "retryDelay": 1000}, "versionId": "3c337d42-a3bb-4b71-ac36-deaf0cdf6019", "connections": {}, "description": "Automated workflow: Gmail to Vector Embeddings with PGVector and Ollama. This workflow integrates 15 different services: stickyNote, textSplitterRecursiveCharacterTextSplitter, code, gmailTrigger, splitOut. It contains 20 nodes and follows best practices for error handling and security.", "notes": "Excellent quality workflow: Gmail to Vector Embeddings with PGVector and Ollama. This workflow has been optimized for production use with comprehensive error handling, security, and documentation."}