{
  "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
  }
}