AutomationFlowsAI & RAG › Identify Enterprise White Space Opportunities with Salesforce, Gemini and Gmail

Identify Enterprise White Space Opportunities with Salesforce, Gemini and Gmail

ByWeblineIndia @weblineindia on n8n.io

This workflow runs weekly to find cross-sell white space in Salesforce enterprise accounts by comparing Closed Won products to an ERP product catalog, then uses Google Gemini to generate the top opportunities, creates a follow-up Salesforce Task, emails the account owner via…

Cron / scheduled trigger★★★★☆ complexityAI-powered24 nodesSalesforceHTTP RequestGoogle SheetsChain LlmGoogle Gemini ChatGmail
AI & RAG Trigger: Cron / scheduled Nodes: 24 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #18113 — we link there as the canonical source.

This workflow follows the Chainllm → Gmail 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 →

Download .json
{
  "id": "Nyz4Zb1zFCAozaiz",
  "name": "Strategic Account White Space Identifier",
  "tags": [
    {
      "id": "SHOjCQ122RUsnOKj",
      "name": "Sales Intelligence",
      "createdAt": "2026-08-12T10:36:55.267Z",
      "updatedAt": "2026-08-12T10:36:55.267Z"
    },
    {
      "id": "xB8fgdFh0zZP48O5",
      "name": "Enterprise",
      "createdAt": "2026-08-12T10:36:55.287Z",
      "updatedAt": "2026-08-12T10:36:55.287Z"
    }
  ],
  "nodes": [
    {
      "id": "10c17914-f859-4d3f-beab-ce332a0da6f7",
      "name": "Sticky Note \u2013 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -496,
        144
      ],
      "parameters": {
        "width": 1120,
        "height": 948,
        "content": "##  Strategic Account White Space Identifier\n\n**What it does:** Runs weekly to identify untapped revenue within existing enterprise accounts. It compares what each account has already bought (from Salesforce Closed Won opportunities) against your full ERP product catalog, then uses Gemini AI to surface the top 3 expansion opportunities and auto-creates a Salesforce task + sends an email to the account owner.\n\n---\n\n###  Setup Steps\n\n**1. Salesforce**\n- Connect your Salesforce OAuth2 credential\n- Confirm `OpportunityLineItem` has `Product2.Name` populated on Closed Won deals\n\n**2. ERP Product Catalog**\n- `HTTP - ERP Product Catalog` fetches from dummyjson (demo)\n-  Replace URL with your real ERP endpoint\n\n**3. Google Gemini**\n- Add your Google Gemini (PaLM) API credential\n- Model: `gemini-1.5-flash` recommended for production\n\n**4. Google Sheets (Memory)**\n- Sheet ID: `16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U`\n- Tab must have headers: `account_id, processed_week, priority_tier` etc.\n- Prevents duplicate processing \u2014 resets weekly (ISO week)\n\n**5. Gmail**\n- Connect Gmail OAuth2 credential\n- Sends HTML opportunity email to the SF account owner\n\n**6. Schedule**\n- Runs every Monday at 06:00 AM (cron: `0 6 * * 1`)\n- Adjust in `Schedule Trigger` node"
      },
      "typeVersion": 1
    },
    {
      "id": "71febfe6-4d75-49f2-8ec8-a6b241ebc01c",
      "name": "Sticky Note \u2013 Data Ingestion",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 728,
        "height": 672,
        "content": "###  Step 1 \u2014 Data Ingestion\nPulls enterprise accounts (Revenue > $100K) from Salesforce, batches them, then fetches each account's Closed-Won products via SOQL and loads the full ERP catalog in parallel."
      },
      "typeVersion": 1
    },
    {
      "id": "11f1cd1b-6eba-4482-aec2-e581c6b36d5c",
      "name": "Sticky Note \u2013 Enrich & Dedup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 810,
        "height": 672,
        "content": "###  Step 2 \u2014 Enrich & Deduplicate\nMerges purchased products with ERP catalog to compute white space. Reads Google Sheets to skip accounts already processed this ISO week. Adds 10s rate-limit buffer before AI."
      },
      "typeVersion": 1
    },
    {
      "id": "c58e7aad-87b2-45ee-a18c-38837aa2b2b6",
      "name": "Sticky Note \u2013 AI Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2576,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 748,
        "height": 672,
        "content": "### Step 3 \u2014 AI White Space Analysis\nGemini analyses the account's product gaps and returns structured JSON: top 3 opportunities with pitch, value estimate, urgency, and a ready-to-use email + Salesforce task. Parse node validates output."
      },
      "typeVersion": 1
    },
    {
      "id": "33717d62-51b6-412d-8b72-a61e97147c3d",
      "name": "Sticky Note \u2013 Actions & Memory",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3392,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 844,
        "height": 672,
        "content": "### Step 4 \u2014 Act & Remember\nCreates a Salesforce Task linked to the account, sends the AI-generated HTML email to the account owner, then logs the result to Google Sheets \u2014 preventing re-processing until next week."
      },
      "typeVersion": 1
    },
    {
      "id": "894d7c63-b134-49e2-bd45-fda7bd62d82e",
      "name": "SF - Get Enterprise Accounts",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        960,
        720
      ],
      "parameters": {
        "options": {
          "fields": "Id,Name,Industry,AnnualRevenue,NumberOfEmployees,BillingCountry,Owner.Name,Owner.Email,Type",
          "conditionsUi": {
            "conditionValues": [
              {
                "field": "AnnualRevenue",
                "value": "={{ 100000 }}",
                "operation": ">"
              }
            ]
          }
        },
        "resource": "account",
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "3ba97725-87e8-4a60-a21a-fc1a8e64243d",
      "name": "SplitInBatches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1184,
        720
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "652c81d7-7d76-4070-9894-fd55ad7cbe5f",
      "name": "SF SOQL - Get Closed Won Products",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        1424,
        832
      ],
      "parameters": {
        "query": "=SELECT Opportunity.AccountId, OpportunityId, Product2.Name, Product2.ProductCode, Quantity, TotalPrice FROM OpportunityLineItem WHERE Opportunity.StageName = 'Closed Won' AND Opportunity.AccountId = '{{ $json.Id }}'",
        "resource": "search"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "1dcc92e9-7f22-47fc-9fae-98b773813241",
      "name": "HTTP - ERP Product Catalog",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1424,
        608
      ],
      "parameters": {
        "url": "https://dummyjson.com/products?limit=100&select=id,title,category,price",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "227f559d-7756-4e28-8edb-82a72c81b573",
      "name": "Aggregate SF Products",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        720
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Aggregate SF Products \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Input 0: OLI records from SF SOQL (accounts with 0 purchases still arrive due to alwaysOutputData)\n// Input 1: ERP catalog from HTTP node\n\nconst oliRecords = $input.all();\n\n// Safely read ERP catalog from its node\nlet erpRaw = {};\ntry { erpRaw = $('HTTP - ERP Product Catalog').first().json; } catch(e) {}\n\n// Parse ERP catalog\nconst erpProducts = (erpRaw.products || []).map(p => ({\n  name: p.title || p.name || '',\n  price: parseFloat(p.price || 0)\n})).filter(p => p.name);\n\n// Parse SF purchased products\nconst purchased = {};\nlet totalSpend = 0;\nfor (const rec of oliRecords) {\n  // Handle both nested and flat Salesforce SOQL response shapes\n  const name = rec.json?.Product2?.Name\n    || rec.json?.['Product2.Name']\n    || rec.json?.productName\n    || '';\n  if (!name) continue;\n  if (!purchased[name]) purchased[name] = { qty: 0, spend: 0 };\n  purchased[name].qty  += parseFloat(rec.json.Quantity  || 0);\n  purchased[name].spend += parseFloat(rec.json.TotalPrice || 0);\n  totalSpend += parseFloat(rec.json.TotalPrice || 0);\n}\n\nconst purchasedNames = Object.keys(purchased);\nconst purchasedSet   = new Set(purchasedNames.map(n => n.toLowerCase()));\n\n// White space = ERP catalog MINUS what this account already bought\nconst whiteSpaceProducts = erpProducts.filter(p => !purchasedSet.has(p.name.toLowerCase()));\n\n// Metrics\nconst avgPerProduct = purchasedNames.length > 0 ? totalSpend / purchasedNames.length : 0;\nconst estimatedWsv  = Math.round(avgPerProduct * whiteSpaceProducts.length);\nconst totalProducts = purchasedNames.length + whiteSpaceProducts.length;\nconst penetrationPct = totalProducts > 0\n  ? Math.round((purchasedNames.length / totalProducts) * 100) : 0;\n\nlet tier = 'Low';\nif (estimatedWsv > 500000 || whiteSpaceProducts.length > 20) tier = 'High';\nelse if (estimatedWsv > 100000 || whiteSpaceProducts.length > 8)  tier = 'Medium';\n\n// Pull account fields from SplitInBatches (the source of truth for the current account)\nlet sf = {};\ntry { sf = $('SplitInBatches').first().json; } catch(e) {}\n\nconst ownerName  = sf['Owner.Name']  || sf?.Owner?.Name  || '';\nconst ownerEmail = sf['Owner.Email'] || sf?.Owner?.Email || '';\n\n// Build rich LLM context string\nconst ctx = [\n  `ACCOUNT: ${sf.Name} (ID: ${sf.Id})`,\n  `Industry: ${sf.Industry || 'N/A'} | Revenue: $${(sf.AnnualRevenue||0).toLocaleString()} | Employees: ${sf.NumberOfEmployees || 'N/A'} | Country: ${sf.BillingCountry || 'N/A'}`,\n  `Owner: ${ownerName} <${ownerEmail}>`,\n  '',\n  `PRODUCTS ALREADY PURCHASED (${purchasedNames.length}):`,\n  purchasedNames.length > 0\n    ? purchasedNames.map(n => `  - ${n}: qty ${purchased[n].qty}, spend $${purchased[n].spend.toLocaleString()}`).join('\\n')\n    : '  None on record',\n  '',\n  `WHITE SPACE \u2014 PRODUCTS NOT YET PURCHASED (${whiteSpaceProducts.length}):`,\n  whiteSpaceProducts.length > 0\n    ? whiteSpaceProducts.slice(0, 20).map(p => `  - ${p.name} (catalog price: $${p.price.toLocaleString()})`).join('\\n')\n    : '  None \u2014 account has full coverage',\n  whiteSpaceProducts.length > 20 ? `  ... and ${whiteSpaceProducts.length - 20} more` : '',\n  '',\n  'METRICS',\n  `  Total SF Spend (Closed Won):  $${totalSpend.toLocaleString()}`,\n  `  Product Penetration Rate:     ${penetrationPct}%`,\n  `  Catalog Coverage:             ${purchasedNames.length} of ${totalProducts} products`,\n  `  Est. White Space Value:       $${estimatedWsv.toLocaleString()}`,\n  `  Priority Tier:                ${tier}`,\n].join('\\n');\n\nreturn [{\n  json: {\n    account_id:                sf.Id           || '',\n    account_name:              sf.Name         || '',\n    industry:                  sf.Industry     || '',\n    annual_revenue:            sf.AnnualRevenue || 0,\n    billing_country:           sf.BillingCountry || '',\n    owner_name:                ownerName,\n    owner_email:               ownerEmail,\n    products_purchased:        purchasedNames,\n    products_purchased_detail: purchased,\n    products_not_purchased:    whiteSpaceProducts.map(p => p.name),\n    white_space_detail:        whiteSpaceProducts,\n    total_spend:               totalSpend,\n    estimated_white_space_value: estimatedWsv,\n    penetration_rate_pct:      penetrationPct,\n    priority_tier:             tier,\n    context_summary:           ctx\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4159fa8c-bbc8-4e01-92fc-ea4da2b4eae3",
      "name": "Google Sheets - Get Processed IDs",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1984,
        720
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U/edit?usp=drivesdk",
          "cachedResultName": "ProcessedAccounts"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true
    },
    {
      "id": "86104f6b-32f2-49cf-bb94-4c4696eca929",
      "name": "Filter Fresh Accounts",
      "type": "n8n-nodes-base.code",
      "position": [
        2176,
        720
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Filter Fresh Accounts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// FIX: This node handles THREE cases safely:\n//   1. Sheet is EMPTY (0 rows)        \u2192 treat as no processed accounts \u2192 always proceed\n//   2. Sheet has rows, account NOT in it this week \u2192 proceed\n//   3. Sheet has rows, account already processed this week \u2192 skip (return [])\n\nconst getISOWeek = (date) => {\n  const d = new Date(date);\n  const day = d.getUTCDay() || 7;\n  d.setUTCDate(d.getUTCDate() + 4 - day);\n  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n  const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);\n  return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;\n};\n\nconst currentWeek = getISOWeek(new Date());\n\n// $input.all() safely returns [] when the sheet is empty \u2014 no crash\nconst sheetRows = $input.all();\n\n// Build set of account IDs already processed THIS week\nconst processedThisWeek = new Set(\n  sheetRows\n    .filter(r => r.json && r.json.processed_week === currentWeek)\n    .map(r => String(r.json.account_id || '').trim())\n    .filter(Boolean)\n);\n\n// Get the current account data from Aggregate node\nlet accountData;\ntry {\n  accountData = $('Aggregate SF Products').first();\n} catch(e) {\n  // If Aggregate node has no data, skip gracefully\n  console.log('[FilterFresh] Could not read Aggregate SF Products, skipping.');\n  return [];\n}\n\nif (!accountData || !accountData.json) {\n  console.log('[FilterFresh] No account data found, skipping.');\n  return [];\n}\n\nconst accId = String(accountData.json.account_id || '').trim();\n\nif (!accId) {\n  console.log('[FilterFresh] Account ID is empty, skipping.');\n  return [];\n}\n\nif (processedThisWeek.has(accId)) {\n  console.log(`[Dedup] Skipping ${accId} (${accountData.json.account_name}) \u2014 already processed in ${currentWeek}`);\n  return [];\n}\n\nconsole.log(`[FilterFresh] Processing ${accId} (${accountData.json.account_name}) for week ${currentWeek}. Sheet had ${sheetRows.length} row(s).`);\n\nreturn [{ json: { ...accountData.json, current_week: currentWeek } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "63afb20a-9a07-49c0-8fb5-6ed8a861b355",
      "name": "Wait - Rate Limit Buffer",
      "type": "n8n-nodes-base.wait",
      "position": [
        2368,
        720
      ],
      "parameters": {
        "amount": 10
      },
      "typeVersion": 1.1
    },
    {
      "id": "b8745a57-a7d0-4fa3-a6d3-1402e4e9a902",
      "name": "Basic LLM Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2656,
        720
      ],
      "parameters": {
        "text": "=You are a senior B2B revenue intelligence analyst.\n\nAnalyse the account data below. The \"WHITE SPACE\" section lists products from \nthe company catalog that this account has NOT purchased. These are real gaps.\n\nYour job: identify the TOP 3 most valuable opportunities from that white space \nand produce a concrete action plan for the account owner.\n\nSTRICT RULES:\n- Return ONLY valid JSON. No text, no markdown, no backticks.\n- ONLY recommend products from the WHITE SPACE list. Never recommend something already purchased.\n- estimated_value_usd must be realistic (base it on the account's revenue and existing spend).\n- suggested_task_description must be a plain STRING, not an array.\n\nJSON SCHEMA:\n{\n  \"account_id\": \"string\",\n  \"account_name\": \"string\",\n  \"priority_tier\": \"High | Medium | Low\",\n  \"white_space_summary\": \"2-3 sentence executive summary\",\n  \"top_opportunities\": [\n    {\n      \"opportunity_title\": \"string\",\n      \"product_name\": \"exact name from white space list\",\n      \"rationale\": \"why this product fits this specific account\",\n      \"estimated_value_usd\": 50000,\n      \"confidence_score\": 0.8,\n      \"urgency\": \"Immediate | Short-term | Long-term\",\n      \"suggested_pitch\": \"one sentence the sales rep can use to open the conversation\"\n    }\n  ],\n  \"competitor_risk\": \"High | Medium | Low | None\",\n  \"competitor_notes\": \"string\",\n  \"recommended_next_action\": \"specific first step for the account owner\",\n  \"suggested_task_subject\": \"string (max 80 chars)\",\n  \"suggested_task_description\": \"Step 1: ...\\nStep 2: ...\\nStep 3: ...\",\n  \"email_subject\": \"string\",\n  \"email_body\": \"<html string>\"\n}\n\nACCOUNT DATA:\n{{ $json.context_summary }}",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "fd61119c-df66-4ca7-a85e-d390e54b4bf7",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        2656,
        912
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-3.1-flash-lite-preview"
      },
      "typeVersion": 1
    },
    {
      "id": "13f36229-2d48-4898-918a-19895ed27515",
      "name": "Parse JSON - LLM Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2928,
        720
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Parse JSON - LLM Response \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Handles all Gemini/LLM response shapes and merges with account context\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  let rawText = '';\n\n  // Extract text from every possible response shape\n  if (typeof item.json?.text === 'string') {\n    rawText = item.json.text;\n  } else if (item.json?.message?.content) {\n    rawText = item.json.message.content;\n  } else if (item.json?.choices?.[0]?.message?.content) {\n    rawText = item.json.choices[0].message.content;\n  } else if (item.json?.candidates?.[0]?.content?.parts?.[0]?.text) {\n    // Native Gemini REST shape\n    rawText = item.json.candidates[0].content.parts[0].text;\n  } else if (item.json?.content) {\n    rawText = typeof item.json.content === 'string'\n      ? item.json.content\n      : JSON.stringify(item.json.content);\n  } else {\n    rawText = JSON.stringify(item.json);\n  }\n\n  // Strip markdown fences (Gemini sometimes adds them despite instructions)\n  rawText = rawText\n    .replace(/^```json\\s*/i, '')\n    .replace(/^```\\s*/i, '')\n    .replace(/\\s*```$/i, '')\n    .trim();\n\n  let parsed = {};\n  let parseError = false;\n\n  try {\n    parsed = JSON.parse(rawText);\n  } catch (e) {\n    parseError = true;\n    console.error('[ParseJSON] Failed:', e.message);\n    console.error('[ParseJSON] Raw (first 600 chars):', rawText.substring(0, 600));\n  }\n\n  // Get upstream account context\n  let accountCtx = {};\n  try {\n    accountCtx = $('Filter Fresh Accounts').first().json;\n  } catch(e) {\n    try { accountCtx = $('Aggregate SF Products').first().json; } catch(e2) {}\n  }\n\n  const opp  = Array.isArray(parsed.top_opportunities) ? parsed.top_opportunities : [];\n  const opp1 = opp[0] || {};\n  const opp2 = opp[1] || {};\n  const opp3 = opp[2] || {};\n\n  const tierMap = { 'High': 'High', 'Medium': 'Normal', 'Low': 'Low' };\n  const sfPriority = tierMap[parsed.priority_tier || accountCtx.priority_tier] || 'Normal';\n\n  const activityDate = new Date();\n  activityDate.setDate(activityDate.getDate() + 7);\n  const activityDateStr = activityDate.toISOString().split('T')[0];\n\n  results.push({\n    json: {\n      _parse_error: parseError,\n      _raw_llm_output: parseError ? rawText.substring(0, 1000) : undefined,\n\n      // AI output\n      priority_tier:              parsed.priority_tier              || accountCtx.priority_tier || 'Low',\n      white_space_summary:        parsed.white_space_summary        || '',\n      top_opportunities:          opp,\n      competitor_risk:            parsed.competitor_risk            || 'None',\n      competitor_notes:           parsed.competitor_notes           || '',\n      recommended_next_action:    parsed.recommended_next_action    || '',\n      suggested_task_subject:     (parsed.suggested_task_subject    || 'White Space Follow-Up: ' + (accountCtx.account_name || '')).substring(0, 80),\n      suggested_task_description: parsed.suggested_task_description || '',\n      email_subject:              parsed.email_subject              || `White Space Opportunity: ${accountCtx.account_name || ''}`,\n      email_body:                 parsed.email_body                 || '',\n      sf_priority:                sfPriority,\n      activity_date:              activityDateStr,\n\n      // Flat opportunity columns for Sheets\n      opp_1_title:      opp1.opportunity_title   || '',\n      opp_1_product:    opp1.product_name        || '',\n      opp_1_value:      opp1.estimated_value_usd || 0,\n      opp_1_urgency:    opp1.urgency             || '',\n      opp_1_confidence: opp1.confidence_score    || 0,\n      opp_2_title:      opp2.opportunity_title   || '',\n      opp_2_product:    opp2.product_name        || '',\n      opp_2_value:      opp2.estimated_value_usd || 0,\n      opp_3_title:      opp3.opportunity_title   || '',\n      opp_3_product:    opp3.product_name        || '',\n      opp_3_value:      opp3.estimated_value_usd || 0,\n\n      // Pass-through account fields\n      account_id:                accountCtx.account_id                || '',\n      account_name:              accountCtx.account_name              || '',\n      industry:                  accountCtx.industry                  || '',\n      annual_revenue:            accountCtx.annual_revenue            || 0,\n      billing_country:           accountCtx.billing_country           || '',\n      owner_name:                accountCtx.owner_name                || '',\n      owner_email:               accountCtx.owner_email               || '',\n      penetration_rate_pct:      accountCtx.penetration_rate_pct      || 0,\n      est_white_space_value_usd: accountCtx.estimated_white_space_value || 0,\n      total_spend:               accountCtx.total_spend               || 0,\n      current_week:              accountCtx.current_week              || ''\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "f49b8711-c9f3-4e4e-8886-25e6ce112957",
      "name": "IF - Parse OK?",
      "type": "n8n-nodes-base.if",
      "position": [
        3104,
        720
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-parse-ok",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json._parse_error }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "641d39fd-0316-49aa-b232-6ffc7698b603",
      "name": "Salesforce - Create Task",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        3600,
        640
      ],
      "parameters": {
        "status": "Not Started",
        "resource": "task",
        "additionalFields": {
          "type": "Call",
          "whatId": "={{ $json['What id'] }}",
          "priority": "={{ $json.priority }}",
          "description": "={{ $json.Description }}",
          "activityDate": "={{ $json['Activity Date'] }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c17edc0a-c13f-43da-ba27-4d64ef5eb76b",
      "name": "Send Email Alert",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3600,
        800
      ],
      "parameters": {
        "sendTo": "={{ $json.owner_email }}",
        "message": "={{ $json.email_body }}",
        "options": {},
        "subject": "={{ $json.email_subject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "5689bcfa-6a92-482c-8df3-fd99d1ed135c",
      "name": "Merge - Task + Email Done",
      "type": "n8n-nodes-base.merge",
      "position": [
        3808,
        736
      ],
      "parameters": {
        "mode": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "81e935bd-e7a5-438e-a8d4-6d9564d8916f",
      "name": "Google Sheets - Append to Memory",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4032,
        736
      ],
      "parameters": {
        "columns": {
          "value": {
            "industry": "={{ $('Parse JSON - LLM Response').first().json.industry }}",
            "account_id": "={{ $('Parse JSON - LLM Response').first().json.account_id }}",
            "email_sent": "Yes",
            "owner_name": "={{ $('Parse JSON - LLM Response').first().json.owner_name }}",
            "opp_1_title": "={{ $('Parse JSON - LLM Response').first().json.opp_1_title }}",
            "opp_2_title": "={{ $('Parse JSON - LLM Response').first().json.opp_2_title }}",
            "owner_email": "={{ $('Parse JSON - LLM Response').first().json.owner_email }}",
            "total_spend": "={{ $('Parse JSON - LLM Response').first().json.total_spend }}",
            "account_name": "={{ $('Parse JSON - LLM Response').first().json.account_name }}",
            "processed_at": "={{ new Date().toISOString() }}",
            "opp_1_product": "={{ $('Parse JSON - LLM Response').first().json.opp_1_product }}",
            "opp_1_urgency": "={{ $('Parse JSON - LLM Response').first().json.opp_1_urgency }}",
            "priority_tier": "={{ $('Parse JSON - LLM Response').first().json.priority_tier }}",
            "annual_revenue": "={{ $('Parse JSON - LLM Response').first().json.annual_revenue }}",
            "billing_country": "={{ $('Parse JSON - LLM Response').first().json.billing_country }}",
            "competitor_risk": "={{ $('Parse JSON - LLM Response').first().json.competitor_risk }}",
            "opp_1_value_usd": "={{ $('Parse JSON - LLM Response').first().json.opp_1_value }}",
            "opp_2_value_usd": "={{ $('Parse JSON - LLM Response').first().json.opp_2_value }}",
            "sf_task_created": "Yes",
            "competitor_notes": "={{ $('Parse JSON - LLM Response').first().json.competitor_notes }}",
            "opp_1_confidence": "={{ $('Parse JSON - LLM Response').first().json.opp_1_confidence }}",
            "recommended_action": "={{ $('Parse JSON - LLM Response').first().json.recommended_next_action }}",
            "white_space_summary": "={{ $('Parse JSON - LLM Response').first().json.white_space_summary }}",
            "penetration_rate_pct": "={{ $('Parse JSON - LLM Response').first().json.penetration_rate_pct }}",
            "est_white_space_value_usd": "={{ $('Parse JSON - LLM Response').first().json.est_white_space_value_usd }}"
          },
          "schema": [
            {
              "id": "account_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "account_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "account_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "account_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "industry",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "industry",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "annual_revenue",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "annual_revenue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "billing_country",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "billing_country",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "owner_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "owner_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "owner_email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "owner_email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "priority_tier",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "priority_tier",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "white_space_summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "white_space_summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_1_title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_1_title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_1_product",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_1_product",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_1_value_usd",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_1_value_usd",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_1_urgency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_1_urgency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_1_confidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_1_confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_2_title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_2_title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opp_2_value_usd",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opp_2_value_usd",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competitor_risk",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "competitor_risk",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competitor_notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "competitor_notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recommended_action",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "recommended_action",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "penetration_rate_pct",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "penetration_rate_pct",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "est_white_space_value_usd",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "est_white_space_value_usd",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_spend",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_spend",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sf_task_created",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "sf_task_created",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email_sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "email_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "processed_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "processed_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/16mSH_k-L46ySe3wbyO6YrYr4eaU3zxoToH7BXQfHS2U/edit?usp=drivesdk",
          "cachedResultName": "ProcessedAccounts"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "bc097285-1b8b-416e-88f4-82def322fee8",
      "name": "Error Logger",
      "type": "n8n-nodes-base.code",
      "position": [
        3760,
        944
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Error Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Logs parse failures and continues the loop cleanly\nconst item = $input.first();\nconst accountId   = item.json.account_id   || 'unknown';\nconst accountName = item.json.account_name || 'unknown';\nconst rawOutput   = item.json._raw_llm_output || '';\n\nconsole.error(`[ERROR] LLM parse failed \u2014 Account: ${accountName} (${accountId})`);\nconsole.error(`[ERROR] Raw output preview: ${rawOutput}`);\n\n// Return an item so SplitInBatches always gets output and can loop to next account\nreturn [{\n  json: {\n    error_logged:        true,\n    account_id:          accountId,\n    account_name:        accountName,\n    error_reason:        'LLM JSON parse failed',\n    raw_output_preview:  rawOutput.substring(0, 500),\n    logged_at:           new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "45835d77-67f9-48a8-8832-be0a29bbc29c",
      "name": "Map Task Fields for Salesforce",
      "type": "n8n-nodes-base.set",
      "position": [
        3440,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "016a3982-e7af-4803-ac79-25a0d0fbe108",
              "name": "Activity Date",
              "type": "string",
              "value": "={{ $json.activity_date }}"
            },
            {
              "id": "e6f08088-74a5-4a63-8279-254aea0b7807",
              "name": "Description",
              "type": "string",
              "value": "={{ $json.suggested_task_description }}"
            },
            {
              "id": "fde21e12-482a-4619-b2b6-387758da10b9",
              "name": "priority",
              "type": "string",
              "value": "={{ $json.sf_priority }}"
            },
            {
              "id": "8bd284b6-a80a-4c56-b4ab-173e67bc6f0e",
              "name": "What id",
              "type": "string",
              "value": "={{ $json.account_id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b6ce4528-af5b-49ba-be4a-4622306ffc43",
      "name": "Weekly Account Scan",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        720,
        720
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "830358d2-888e-479a-acc4-280d622e08d7",
  "nodeGroups": [],
  "connections": {
    "Error Logger": {
      "main": [
        [
          {
            "node": "SplitInBatches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Parse OK?": {
      "main": [
        [
          {
            "node": "Send Email Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Map Task Fields for Salesforce",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Logger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SplitInBatches": {
      "main": [
        [],
        [
          {
            "node": "SF SOQL - Get Closed Won Products",
            "type": "main",
            "index": 0
          },
          {
            "node": "HTTP - ERP Product Catalog",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Parse JSON - LLM Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Alert": {
      "main": [
        [
          {
            "node": "Merge - Task + Email Done",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Weekly Account Scan": {
      "main": [
        [
          {
            "node": "SF - Get Enterprise Accounts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate SF Products": {
      "main": [
        [
          {
            "node": "Google Sheets - Get Processed IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Fresh Accounts": {
      "main": [
        [
          {
            "node": "Wait - Rate Limit Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Salesforce - Create Task": {
      "main": [
        [
          {
            "node": "Merge - Task + Email Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Rate Limit Buffer": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Task + Email Done": {
      "main": [
        [
          {
            "node": "Google Sheets - Append to Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON - LLM Response": {
      "main": [
        [
          {
            "node": "IF - Parse OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SF - Get Enterprise Accounts": {
      "main": [
        [
          {
            "node": "SplitInBatches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Task Fields for Salesforce": {
      "main": [
        [
          {
            "node": "Salesforce - Create Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Append to Memory": {
      "main": [
        [
          {
            "node": "SplitInBatches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Get Processed IDs": {
      "main": [
        [
          {
            "node": "Filter Fresh Accounts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SF SOQL - Get Closed Won Products": {
      "main": [
        [
          {
            "node": "Aggregate SF Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs weekly to find cross-sell white space in Salesforce enterprise accounts by comparing Closed Won products to an ERP product catalog, then uses Google Gemini to generate the top opportunities, creates a follow-up Salesforce Task, emails the account owner via…

Source: https://n8n.io/workflows/18113/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Categories Content Creation AI Automation Publishing Social Media

Google Docs, HTTP Request, Slack +7
AI & RAG

Automatically identifies overdue sales leads and generates personalized follow-up emails using AI. Runs every weekday Reads leads from Google Sheets Filters leads with no contact for 5+ days Downloads

Google Sheets, Chain Llm, Google Gemini Chat +3
AI & RAG

Automating YouTube Metadata Ai Agent. Uses lmChatGoogleGemini, chainLlm, agent, gmail. Scheduled trigger; 22 nodes.

Google Gemini Chat, Chain Llm, Agent +3
AI & RAG

SMB finance teams, SaaS companies, and accounting professionals who need to automate transaction reconciliation between Stripe payments and their accounting ledgers. Perfect for businesses processing

HTTP Request, Google Sheets, Chain Llm +3
AI & RAG

This workflow runs every morning to pull top BBC News headlines via RSS, today’s open tasks from Google Sheets, and a one-day forecast from Open-Meteo, then uses Google Gemini to write a short morning

RSS Feed Read, Google Sheets, HTTP Request +3