This workflow follows the Gmail Trigger → Google Sheets 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 →
{
"name": "Email to AI to ERP to CRM tracking pipeline (hardened)",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyX",
"value": 10,
"unit": "minutes"
}
]
},
"simple": false,
"filters": {
"q": "has:attachment newer_than:2d",
"readStatus": "unread"
},
"options": {
"downloadAttachments": true
}
},
"id": "bc0fabc1-a633-40bb-b9a9-1f1cd86dd90a",
"name": "Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"typeVersion": 1.3,
"position": [
220,
480
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// GLOBAL base64 budget (shared across ALL items \u2014 per-item caps don't stop OOM)\n// + explicit binary passthrough (omit 'binary' and n8n silently drops attachments).\nconst BUDGET_BYTES = 32 * 1024 * 1024;\nconst PER_FILE_MAX = 10 * 1024 * 1024;\nlet spent = 0;\nconst items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n const binary = items[i].binary || {};\n const kept = {};\n let skippedReason = '';\n for (const key of Object.keys(binary)) {\n const buffer = await this.helpers.getBinaryDataBuffer(i, key);\n const bytes = buffer.length;\n if (bytes > PER_FILE_MAX) { skippedReason = 'too_big'; continue; }\n if (spent + bytes > BUDGET_BYTES) { skippedReason = 'budget'; continue; }\n spent += bytes;\n kept[key] = binary[key];\n }\n out.push({\n json: { ...items[i].json, has_attachments: Object.keys(kept).length > 0, pdf_skipped_reason: skippedReason },\n binary: kept,\n });\n}\nreturn out;"
},
"id": "d7d32bbe-e549-4b23-bc35-e7f313dcccd4",
"name": "Enforce global base64 budget",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
480
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "c_has",
"leftValue": "={{ $json.has_attachments }}",
"operator": {
"type": "boolean",
"operation": "true"
},
"rightValue": ""
}
]
}
},
"id": "1fb7f572-1797-4bd8-9f8a-7902f542f8ea",
"name": "Has attachments?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
700,
480
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Build a generic vision-extraction request from the kept attachments.\nreturn $input.all().map((item) => {\n const keys = Object.keys(item.binary || {});\n return {\n json: {\n ...item.json,\n vision_request: { model: 'gemini-2.5-flash', attachments: keys },\n attachment_count: keys.length,\n },\n binary: item.binary,\n };\n});"
},
"id": "d73b1a27-f324-42e6-8c33-2ccb99d6c52f",
"name": "Build vision request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
340
]
},
{
"parameters": {
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
"authentication": "genericCredentialType",
"genericAuthType": "httpQueryAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ $json.vision_request }}"
},
"id": "a5a61e56-9910-4c49-b084-7c0a1de61b43",
"name": "Extract fields from PDF (Gemini vision)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1180,
340
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.groq.com/openai/v1/chat/completions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ { model: \"llama-3.3-70b\", messages: [{ role: \"user\", content: $json.subject }] } }}"
},
"id": "7686ce09-8939-47f3-b30e-86effa31d566",
"name": "Parse AI response (Groq)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1420,
480
]
},
{
"parameters": {
"method": "POST",
"url": "https://your-instance.odoo.com/jsonrpc",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ { jsonrpc: \"2.0\", method: \"call\", params: { model: \"sale.order\", domain: [[\"name\", \"=\", $json.order_ref]] } } }}"
},
"id": "ccd87ff1-fc2b-4dd9-bd7e-c681637ecc57",
"name": "Validate order in ERP (Odoo)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1660,
480
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Normalize the tracking id BEFORE it becomes a document key (strip carrier prefixes/dashes),\n// and never fabricate a status \u2014 unknown stays 'pending', not 'transit'.\nreturn $input.all().map((item) => {\n const j = item.json || {};\n const trackingNumber = String(j.tracking_number || '').replace(/[^A-Za-z0-9]/g, '');\n return {\n json: {\n doc_id: trackingNumber || ('no-track-' + (j.order_ref || 'unknown')),\n tracking_number: trackingNumber,\n carrier: j.carrier || 'unknown',\n status: j.status && j.status !== 'notfound' ? j.status : 'pending',\n order_ref: j.order_ref || '',\n has_attachments: Boolean(j.has_attachments),\n },\n };\n});"
},
"id": "44b9cbd4-349e-4d84-b84d-58793a4a72c5",
"name": "Shape tracking document",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1900,
480
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "c_order",
"leftValue": "={{ $json.order_ref }}",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"rightValue": ""
}
]
}
},
"id": "4b1d1100-e59b-400d-8406-45b67357f86a",
"name": "Order & tracking found?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2140,
480
]
},
{
"parameters": {
"resource": "document",
"operation": "upsert",
"authentication": "serviceAccount",
"projectId": "your-firebase-project-id",
"database": "(default)",
"collection": "shipments",
"updateKey": "doc_id",
"columns": "doc_id,tracking_number,carrier,status,order_ref,has_attachments"
},
"id": "13e2bc24-4ca0-4260-af7f-bb2d21f82bbc",
"name": "Upsert to Firestore",
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
"typeVersion": 1.1,
"position": [
2380,
340
]
},
{
"parameters": {
"method": "PATCH",
"url": "https://api.hubapi.com/crm/v3/objects/deals/{dealId}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ { properties: { tracking_number: $json.tracking_number, tracking_status: $json.status } } }}"
},
"id": "0c8d9b9e-cd00-45d1-8999-68d2fe60c241",
"name": "Update deal in CRM (HubSpot)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
2620,
340
]
},
{
"parameters": {
"resource": "sheet",
"operation": "appendOrUpdate",
"authentication": "oAuth2",
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"id": "8139e31e-d36c-4d03-8d8f-0d751987fe15",
"name": "Log to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2860,
340
]
},
{
"parameters": {},
"id": "3943608d-93a5-45e1-9428-16a2bf4744e1",
"name": "Park unmatched for review",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
2380,
620
]
},
{
"parameters": {
"content": "## Email \u2192 AI \u2192 ERP \u2192 CRM tracking pipeline (hardened, sanitized)\nReconstructed from a real production workflow \u2014 every company/client detail, domain, endpoint and key removed. Same node shape, generic labels, credentials left for you to add.\n\n**Hardening baked in:** a GLOBAL base64 budget so a backlog can't OOM the box, explicit binary passthrough so attachments aren't silently dropped, LLM/ERP/CRM keys in credentials (never inline in Code nodes), and tracking ids normalized before they become document keys.\n\nWire your own Gmail, LLM, Odoo, Firestore, HubSpot and Sheets and it runs.",
"color": 5,
"width": 820,
"height": 200
},
"id": "90841034-d3e2-468d-8c6e-6cd20493ab25",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
170,
250
]
}
],
"connections": {
"Gmail Trigger": {
"main": [
[
{
"node": "Enforce global base64 budget",
"type": "main",
"index": 0
}
]
]
},
"Enforce global base64 budget": {
"main": [
[
{
"node": "Has attachments?",
"type": "main",
"index": 0
}
]
]
},
"Has attachments?": {
"main": [
[
{
"node": "Build vision request",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse AI response (Groq)",
"type": "main",
"index": 0
}
]
]
},
"Build vision request": {
"main": [
[
{
"node": "Extract fields from PDF (Gemini vision)",
"type": "main",
"index": 0
}
]
]
},
"Extract fields from PDF (Gemini vision)": {
"main": [
[
{
"node": "Parse AI response (Groq)",
"type": "main",
"index": 0
}
]
]
},
"Parse AI response (Groq)": {
"main": [
[
{
"node": "Validate order in ERP (Odoo)",
"type": "main",
"index": 0
}
]
]
},
"Validate order in ERP (Odoo)": {
"main": [
[
{
"node": "Shape tracking document",
"type": "main",
"index": 0
}
]
]
},
"Shape tracking document": {
"main": [
[
{
"node": "Order & tracking found?",
"type": "main",
"index": 0
}
]
]
},
"Order & tracking found?": {
"main": [
[
{
"node": "Upsert to Firestore",
"type": "main",
"index": 0
}
],
[
{
"node": "Park unmatched for review",
"type": "main",
"index": 0
}
]
]
},
"Upsert to Firestore": {
"main": [
[
{
"node": "Update deal in CRM (HubSpot)",
"type": "main",
"index": 0
}
]
]
},
"Update deal in CRM (HubSpot)": {
"main": [
[
{
"node": "Log to Google Sheets",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"meta": {
"templateCredsSetupCompleted": false
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Email to AI to ERP to CRM tracking pipeline (hardened). Uses gmailTrigger, httpRequest, googleFirebaseCloudFirestore, googleSheets. Event-driven trigger; 14 nodes.
Source: https://github.com/FryFr/n8n-production-minefield/blob/main/templates/email-tracking-pipeline.workflow.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.
This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch
AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.
An automated n8n workflow that monitors your Gmail inbox, classifies job application emails using a local AI (Ollama), and logs every application — with company, role, and status — to a Google Sheet i
Automatically transform resume submissions into comprehensive candidate profiles with AI-powered parsing, GitHub analysis, and instant team notifications. Monitors Gmail for incoming resume attachment
AI Email Summarizer. Uses gmailTrigger, httpRequest, slack, googleSheets. Event-driven trigger; 14 nodes.