{
  "nodes": [
    {
      "id": "vl-manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -16,
        320
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "vl-read-sheet",
      "name": "Read Leads from Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        512,
        320
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "mode": "id",
          "value": "88028505"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1B12w6a3wAykGugGCiXdt2Yc0spF-aM1BkXR3B5wvpAE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "vl-pick-row",
      "name": "Select Lead Row",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        736,
        320
      ],
      "parameters": {
        "jsCode": "\nconst batchSize = $('Configure Batch Size').first().json.BATCH_SIZE || 1;\nconst allItems = $input.all();\n\n// Filter for rows where Status doesn't contain \"Done\"\nconst pendingItems = allItems.filter(item => {\n  const status = item.json['Status'] || '';\n  return !status.includes('Done');\n});\n\nif (pendingItems.length === 0) {\n  throw new Error('No pending leads found. All rows are marked as Done.');\n}\n\n// Take the first batchSize items\nconst batchItems = pendingItems.slice(0, batchSize);\n\n// Map Google Sheet column names to clean field names\nreturn batchItems.map((item) => {\n  const row = item.json;\n  return {\n    json: {\n      _runTimestamp: new Date().toISOString(),\n      first_name:         row['First Name']         || '',\n      last_name:          row['Last Name']           || '',\n      title:              row['Title']               || '',\n      company_name:       row['Company Name']        || '',\n      email:              row['Email']               || '',\n      seniority:          row['Seniority']           || '',\n      departments:        row['Departments']         || '',\n      sub_departments:    row['Sub Departments']     || '',\n      corporate_phone:    row['Corporate Phone']     || '',\n      num_employees:      row['# Employees']         || '',\n      industry:           row['Industry']            || '',\n      keywords:           row['Keywords']            || '',\n      person_linkedin_url:  row['Person Linkedin Url']  || '',\n      website:              row['Website']              || '',\n      company_linkedin_url: row['Company Linkedin Url'] || '',\n      facebook_url:         row['Facebook Url']         || '',\n      twitter_url:          row['Twitter Url']          || '',\n      city:                 row['City']                 || '',\n      state:                row['State']                || '',\n      country:              row['Country']              || '',\n      company_address:      row['Company Address']      || '',\n      company_city:         row['Company City']         || '',\n      company_state:        row['Company State']        || '',\n      company_country:      row['Company Country']      || '',\n      company_phone:        row['Company Phone']        || '',\n      annual_revenue:       row['Annual Revenue']       || ''\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "vl-research-agent",
      "name": "Research Lead Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1344,
        112
      ],
      "parameters": {
        "text": "={{ 'Research this business lead for hyper-personalised cold outreach from [Your Company] (AI automation & n8n workflow development):\\n\\n\ud83d\udc64 LEAD DETAILS:\\n- Name: ' + $('Select Lead Row').item.json.first_name + ' ' + $('Select Lead Row').item.json.last_name + '\\n- Title: ' + ($('Select Lead Row').item.json.title || 'N/A') + '\\n- Company: ' + ($('Select Lead Row').item.json.company_name || 'N/A') + '\\n- Industry: ' + ($('Select Lead Row').item.json.industry || 'N/A') + '\\n- Employees: ' + ($('Select Lead Row').item.json.num_employees || 'N/A') + '\\n- Person LinkedIn: ' + ($('Select Lead Row').item.json.person_linkedin_url || 'N/A') + '\\n- Company LinkedIn: ' + ($('Select Lead Row').item.json.company_linkedin_url || 'N/A') + '\\n\\n\ud83d\udd0d SEARCH INSTRUCTIONS:\\nCall the search tool EXACTLY ONCE. Query = full name + company name + source signals. Example: Noah Devereux The Strive Collective LinkedIn news blog. No year in query.\\n\\n\ud83d\udccb ANALYSIS INSTRUCTIONS:\\nFrom results (last 60 days only):\\n1. Find the SINGLE most recently dated piece of content \u2014 LinkedIn post, tweet, blog, news, podcast, press release, anything. Whatever is most recent wins.\\n2. Write a one-sentence personalizationHook based on that specific fact. The recipient must be able to recognise it immediately as something they recently said, did, or were featured in.\\n3. If absolutely nothing specific found in last 60 days: write exactly: No recent hook found\\n\\n\u26a0\ufe0f CRITICAL \u2014 Return ONLY a raw JSON object with EXACTLY these 6 keys:\\n{ \"researchSummary\": \"2-3 sentences: what this company does, their model, their market\", \"recentNews\": \"Most recent news, post, or announcement found \u2014 or: Nothing found in last 60 days\", \"painPoints\": \"Top 2 specific manual processes AI automation could eliminate for their role\", \"whyNow\": \"Why AI automation is timely for them specifically\", \"personalizationHook\": \"One sentence referencing the single most recent specific thing found \u2014 or: No recent hook found\", \"linkedinInsight\": \"Most recent LinkedIn post or activity found \u2014 or: No LinkedIn activity found in last 60 days\" }' }}",
        "options": {
          "maxIterations": 3,
          "systemMessage": "You are a B2B research analyst for [Your Company]. Your job is to find the single most recent public activity about this person or their company \u2014 from ANY source: LinkedIn post, tweet, blog, news article, podcast, press release, website update, or interview. RULES: (1) Call the search tool EXACTLY ONCE. Query = full name + company + broad source terms. No year in query. (2) From results, identify the SINGLE most recently dated piece of content. (3) Use that specific recent fact as personalizationHook \u2014 something the recipient will recognise as genuinely recent. (4) If nothing specific found in last 60 days, write exactly: No recent hook found in personalizationHook field. Never invent a hook. (5) Return ONLY a raw JSON object with exactly the 6 keys. No markdown. No code fences."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "vl-gemini-research",
      "name": "Gemini Lead Research",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1392,
        336
      ],
      "parameters": {
        "options": {
          "temperature": 0.2
        },
        "modelName": "models/gemini-2.5-flash"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "vl-serpapi-tool",
      "name": "Activate Search Tool",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        1552,
        352
      ],
      "parameters": {
        "url": "https://api.tavily.com/search",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"query\": \"{{ $fromAI('search_query', 'Full name + company name + broad source signals: LinkedIn blog news. No year.') }}\",\n  \"max_results\": 6,\n  \"search_depth\": \"advanced\",\n  \"include_answer\": true,\n  \"include_raw_content\": true,\n  \"days\": 60\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "toolDescription": "Search for the most recent public activity from a person or company. Build ONE query: full name + company name + broad signals (LinkedIn blog news podcast press). Do NOT include a year in the query \u2014 results are already filtered to the last 60 days. Look for: LinkedIn posts, tweets, blog articles, news, podcasts, press releases, website updates. Use this tool exactly once.",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "vl-research-output-parser",
      "name": "Parse Research Output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1248,
        336
      ],
      "parameters": {
        "jsonSchemaExample": "{ \"researchSummary\": \"CloudRetail SaaS operates an e-commerce enablement platform...\", \"recentNews\": \"CloudRetail announced a Series A...\", \"painPoints\": \"1. Manual order reconciliation...\", \"whyNow\": \"As a recently funded e-commerce SaaS...\", \"personalizationHook\": \"Saw the Series A announcement...\", \"linkedinInsight\": \"Priya posted last week...\" }"
      },
      "typeVersion": 1.3
    },
    {
      "id": "vl-strip-markdown",
      "name": "Remove Markdown from Research",
      "type": "n8n-nodes-base.code",
      "position": [
        1968,
        112
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const raw = $input.item.json.output || '';\nconst stripped = raw.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/, '').trim();\nlet parsed;\ntry {\n  parsed = JSON.parse(stripped);\n} catch(e) {\n  parsed = {\n    researchSummary: raw,\n    recentNews: 'N/A',\n    painPoints: 'N/A',\n    whyNow: 'N/A',\n    personalizationHook: 'N/A',\n    linkedinInsight: 'N/A',\n    suggestedFreeWorkflow: 'business process automation workflow'\n  };\n}\nreturn { json: { output: parsed } };"
      },
      "typeVersion": 2
    },
    {
      "id": "vl-email-drafter",
      "name": "Draft Email Content",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2192,
        112
      ],
      "parameters": {
        "text": "={{ 'CRITICAL INSTRUCTION: Respond with ONLY a raw JSON object. No markdown. No code fences. Start with { end with }.\\n\\nWrite a cold outreach email on behalf of {put your name here}, Founder of {your company name here} builds custom AI automations and n8n workflows that eliminate repetitive manual work \u2014 saving businesses hours every week.\\n\\n\ud83d\udc64 LEAD:\\n- First Name: ' + $('Select Lead Row').item.json.first_name + '\\n- Title: ' + ($('Select Lead Row').item.json.title || 'N/A') + '\\n- Seniority: ' + ($('Select Lead Row').item.json.seniority || 'N/A') + '\\n- Company: ' + ($('Select Lead Row').item.json.company_name || 'their company') + '\\n- Industry: ' + ($('Select Lead Row').item.json.industry || 'N/A') + '\\n- Employees: ' + ($('Select Lead Row').item.json.num_employees || 'N/A') + '\\n\\n\ud83d\udd0d RESEARCH FINDINGS:\\n- What they do: ' + (($json.output || {}).researchSummary || 'N/A') + '\\n- Their pain points: ' + (($json.output || {}).painPoints || 'N/A') + '\\n- Why they care now: ' + (($json.output || {}).whyNow || 'N/A') + '\\n- Personalization hook: ' + (($json.output || {}).personalizationHook || 'N/A') + '\\n- LinkedIn insight: ' + (($json.output || {}).linkedinInsight || 'N/A') + '\\n\\n\ud83c\udfaf EMAIL STRATEGY:\\nYou are writing a standard B2B cold email that highlights a specific pain point based on recent research, and offers a soft call to action to discuss further.\\n\\n\ud83d\udcdd EMAIL RULES (follow strictly):\\n- Length: 90\u2013120 words MAXIMUM\\n- Opening: FIRST NAME + comma. No Dear, Hi, Hello.\\n- Line 1\u20132: Reference personalizationHook or linkedinInsight naturally \u2014 must feel genuinely researched, not templated\\n- Line 3\u20134: One concrete sentence connecting their specific pain point to what [Your Company] does \u2014 name their industry/role specifically\\n- Line 5: Ask a soft, low-friction question related to their pain point (e.g. \"Curious how you are handling this at the moment?\" or \"Open to learning more?\")\\n- Final line: Exactly \u2192 \"Worth a quick 15-min call?\"\\n- Sign off: [Your Name] | Founder, [Your Company] Pvt. Ltd.\\n- Tone: Peer-to-peer, direct, curious \u2014 fellow operator, NOT a sales pitch\\n- Subject: Short, curiosity-driven, specific to their company/role \u2014 max 8 words\\n- FORBIDDEN: \"I hope this finds you\", \"I wanted to reach out\", \"just following up\", \"synergy\", \"solutions\", \"leverage\", \"cutting-edge\", \"revolutionary\", \"game-changer\", \"excited to\", \"touch base\"\\n\\n\u26a0\ufe0f HOOK RULE: If personalizationHook is \"No recent hook found\", do NOT mention any specific event or post. Instead open line 1-2 with a sharp, specific observation about a known manual bottleneck in their exact industry and role \u2014 make it feel researched, not generic. Never invent a fake recent event.\\n\\n\u26a0\ufe0f Output ONLY this JSON:\\n{\"subject\": \"subject here\",\"body\": \"email body here\"}' }}",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "vl-gemini-email",
      "name": "Gemini Email Generation",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        2272,
        336
      ],
      "parameters": {
        "options": {
          "temperature": 0.5
        },
        "modelName": "models/gemini-2.5-flash"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "vl-parse-email",
      "name": "Extract Email Details",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        112
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const raw = $input.item.json.output || $input.item.json.text || '';\nlet result = { subject: '', body: '' };\n\nif (typeof raw === 'object' && raw !== null) {\n  result.subject = raw.subject || (raw.output || {}).subject || '';\n  result.body = raw.body || (raw.output || {}).body || '';\n} else if (typeof raw === 'string') {\n  let cleaned = raw.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/\\s*```$/, '').trim();\n  try {\n    const parsed = JSON.parse(cleaned);\n    const inner = parsed.output || parsed;\n    result.subject = inner.subject || '';\n    result.body = inner.body || '';\n  } catch(e) {\n    result.subject = 'Quick question'; // Default fallback subject\n    result.body = raw; // Just use the raw text as the body\n  }\n}\nreturn { json: { output: result } };"
      },
      "typeVersion": 2
    },
    {
      "id": "vl-create-contact",
      "name": "Create Contact in HubSpot",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2896,
        208
      ],
      "parameters": {
        "url": "https://api.hubapi.com/crm/v3/objects/contacts",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({\n  properties: {\n    firstname: $('Select Lead Row').item.json.first_name || '',\n    lastname: $('Select Lead Row').item.json.last_name || '',\n    email: $('Select Lead Row').item.json.email || '',\n    jobtitle: $('Select Lead Row').item.json.title || '',\n    company: $('Select Lead Row').item.json.company_name || '',\n    website: $('Select Lead Row').item.json.website || '',\n    phone: $('Select Lead Row').item.json.corporate_phone || '',\n    city: $('Select Lead Row').item.json.city || '',\n    state: $('Select Lead Row').item.json.state || '',\n    country: $('Select Lead Row').item.json.country || '',\n    hs_linkedin_url: $('Select Lead Row').item.json.person_linkedin_url || '',\n    industry: $('Select Lead Row').item.json.industry || '',\n            hs_lead_status: 'NEW',\n    lifecyclestage: 'lead'\n  }\n}) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken"
      },
      "credentials": {
        "hubspotAppToken": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "vl-resolve-contact-id",
      "name": "Identify HubSpot Contact ID",
      "type": "n8n-nodes-base.code",
      "position": [
        3120,
        208
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet contactId = null;\nif (item.id) { contactId = item.id; }\nif (!contactId && item.error) {\n  const desc = (item.error.description || item.error.message || '');\n  const match = desc.match(/Existing ID:\\s*(\\d+)/i);\n  if (match) { contactId = match[1]; }\n}\nif (!contactId) {\n  throw new Error('Could not resolve HubSpot contact ID. Raw: ' + JSON.stringify(item).slice(0, 200));\n}\nreturn { json: { contactId: contactId } };"
      },
      "typeVersion": 2
    },
    {
      "id": "vl-create-note",
      "name": "Add Note to HubSpot Contact",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3344,
        208
      ],
      "parameters": {
        "url": "https://api.hubapi.com/crm/v3/objects/notes",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({\n  properties: {\n    hs_note_body: '\\ud83d\\udd0d VYRAZU LABS LEAD RESEARCH NOTE\\n\\n\\ud83d\\udcc5 Date Processed: ' + new Date().toLocaleDateString('en-GB', {day:'2-digit',month:'short',year:'numeric'}) + '\\n\\ud83c\\udfaf Lead Source: Google Sheet Export\\n\\ud83d\\udc64 LinkedIn: ' + ($('Select Lead Row').item.json.person_linkedin_url || 'N/A') + '\\n\\ud83c\\udfe2 Company LinkedIn: ' + ($('Select Lead Row').item.json.company_linkedin_url || 'N/A') + '\\n\\ud83c\\udf10 Website: ' + ($('Select Lead Row').item.json.website || 'N/A') + '\\n\\ud83c\\udfd7 Industry: ' + ($('Select Lead Row').item.json.industry || 'N/A') + '\\n\\ud83d\\udc65 Employees: ' + ($('Select Lead Row').item.json.num_employees || 'N/A') + '\\n\\ud83d\\udcb0 Annual Revenue: ' + ($('Select Lead Row').item.json.annual_revenue || 'N/A') + '\\n\\ud83d\\udccd Location: ' + ($('Select Lead Row').item.json.city || '') + ', ' + ($('Select Lead Row').item.json.country || '') + '\\n\\n\\ud83d\\udccb Research Summary:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).researchSummary || 'N/A') + '\\n\\n\\ud83d\\udca1 LinkedIn Insight:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).linkedinInsight || 'N/A') + '\\n\\n\\ud83d\\udcf0 Recent News:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).recentNews || 'N/A') + '\\n\\n\\u26a1 Identified Pain Points:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).painPoints || 'N/A') + '\\n\\n\\u23f0 Why AI Automation Matters Now:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).whyNow || 'N/A') + '\\n\\n\\ud83c\\udf81 Free Workflow Offered:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).suggestedFreeWorkflow || 'N/A') + '\\n\\n\\ud83c\\udfaf Email Hook Used:\\n' + ((($('Remove Markdown from Research').item.json.output) || {}).personalizationHook || 'N/A') + '\\n\\n\\ud83d\\udce7 DRAFTED EMAIL:\\n\\nSubject: ' + ((($('Extract Email Details').item.json.output) || {}).subject || '') + '\\n\\n' + ((($('Extract Email Details').item.json.output) || {}).body || ''),\n    hs_timestamp: new Date().toISOString()\n  },\n  associations: [{\n    to: { id: $('Identify HubSpot Contact ID').item.json.contactId },\n    types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 202 }]\n  }]\n}) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken"
      },
      "credentials": {
        "hubspotAppToken": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "vl-hubspot-email-draft",
      "name": "Draft HubSpot Engagement",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        3568,
        208
      ],
      "parameters": {
        "url": "https://api.hubapi.com/engagements/v1/engagements",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({\n  engagement: {\n    active: true,\n    type: 'EMAIL',\n    timestamp: new Date().getTime()\n  },\n  associations: {\n    contactIds: [parseInt($('Identify HubSpot Contact ID').item.json.contactId)],\n    companyIds: [],\n    dealIds: [],\n    ownerIds: []\n  },\n  metadata: {\n    from: {\n      email: 'webprogem1@gmail.com',\n      firstName: 'Vasu',\n      lastName: 'Naman'\n    },\n    to: [{\n      email: $('Select Lead Row').item.json.email || '',\n      firstName: $('Select Lead Row').item.json.first_name || '',\n      lastName: $('Select Lead Row').item.json.last_name || ''\n    }],\n    subject: (($('Extract Email Details').item.json.output) || {}).subject || '',\n    text: (($('Extract Email Details').item.json.output) || {}).body || '',\n    html: '<p>' + ((($('Extract Email Details').item.json.output) || {}).body || '').replace(/\\n/g, '</p><p>') + '</p>',\n    status: 'DRAFT'\n  }\n}) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken"
      },
      "credentials": {
        "hubspotAppToken": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "vl-mark-processed",
      "name": "Update Processed Row in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3856,
        720
      ],
      "parameters": {
        "columns": {
          "value": {
            "Email": "={{ $('Select Lead Row').item.json.email }}",
            "Status": "={{ 'Done \u2705 ' + new Date().toLocaleDateString('en-GB', {day:'2-digit',month:'short',year:'numeric'}) }}"
          },
          "schema": [
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "mode": "id",
          "value": "88028505"
        },
        "documentId": {
          "mode": "id",
          "value": "1Xq7qO_2YEwn0flWX0lGpS8yaVG3o9sIDCJnbbDVExSY"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "vl-batch-config",
      "name": "Configure Batch Size",
      "type": "n8n-nodes-base.set",
      "notes": "Change BATCH_SIZE here.\n1 = test mode (1 lead per run)\n20 = production (20 leads per run)\nRow pointer auto-advances by BATCH_SIZE each run.",
      "position": [
        208,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "batch-size-setting",
              "name": "BATCH_SIZE",
              "type": "number",
              "value": 2
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "vl-loop",
      "name": "Process Leads in Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "notes": "Processes each lead one at a time, even when BATCH_SIZE > 1.\nLoops back from Update Processed Row in Sheets until all leads are done.",
      "position": [
        960,
        320
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "c232d130-ae24-4f86-aa9d-037162df10d4",
      "name": "Sticky - Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 780,
        "content": "## \u25b6\ufe0f 1. Trigger & Batch Settings\n\nManual trigger for on-demand runs. The **BATCH_SIZE** setting in the Configure Batch Size controls how many leads you process at once. \n\n**Why use a Batch Size?** \nInstead of processing hundreds of leads at once (which could cause API timeouts or rate limits), you can safely process them in chunks. The workflow loops through this batch, researches each lead individually, drafts a personalized email, and marks them as \"Done\" so they are automatically skipped next time you run it.\n\n- `1` = test mode (perfect for checking the AI output)\n- `10\u201320` = production run (safely bulk-process leads)\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a0a5d4a7-61ff-4105-b379-718b2019dddd",
      "name": "Sticky - Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 700,
        "height": 586,
        "content": "## \ud83d\udcca 2. Fetch & Filter Pending Leads\n\nReads all sheet rows, filters out anything already marked **Done**, slices the first BATCH_SIZE rows, and normalizes column names for downstream nodes.\n\n## Read and queue leads\n\nReads candidate leads from Google Sheets, selects the lead rows to process, and feeds them into the batch loop.\n\n**Demo Google Sheet:**\n[View Sheet](https://docs.google.com/spreadsheets/d/1B12w6a3wAykGugGCiXdt2Yc0spF-aM1BkXR3B5wvpAE/edit?gid=88028505)"
      },
      "typeVersion": 1
    },
    {
      "id": "575a1035-0092-4021-9ae7-1aeab8bacb36",
      "name": "Sticky - Research",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 776,
        "content": "## \ud83d\udd0e 3. AI Research Lead Agent\n\nGemini agent calls the **Tavily** tool (You can increase it to suit your needs) exactly once per lead to find the most recent public activity (LinkedIn post, news, blog \u2014 last 60 days), returning a structured research summary, pain points, and a personalization hook.\n\n\n## Research each lead\n\nUses an AI research agent with Gemini, structured output parsing, and Tavily search to gather lead-specific context."
      },
      "typeVersion": 1
    },
    {
      "id": "b9345f1d-4b2c-461c-94d0-380428b041ba",
      "name": "Sticky - Email Draft",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 924,
        "height": 664,
        "content": "## \u270d\ufe0f 4. AI Email Drafting Agent\n\nWrites a 90\u2013120 word cold email in Your Voice voice \u2014 opens with the personalization hook, ties it to a real pain point, introduces how [Your Company] solves this, ends with a fixed CTA. you can also customize the email format using Draft Email Content prompt"
      },
      "typeVersion": 1
    },
    {
      "id": "39284c14-bb9d-4f1d-8724-07085fa06e4f",
      "name": "Sticky - HubSpot",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2800,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 952,
        "height": 664,
        "content": "## \ud83d\udd17 5. Sync to HubSpot CRM\n\nCreates/updates the contact, resolves the contact ID, logs a detailed research + email note, and saves the drafted email as a HubSpot engagement (status: **DRAFT** \u2014 nothing sends automatically)."
      },
      "typeVersion": 1
    },
    {
      "id": "bff9abec-3b4d-40f8-9087-63abcb931627",
      "name": "Sticky - Mark Processed",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3776,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 350,
        "height": 458,
        "content": "## \u2705 6. Mark as Processed\n\nUpdates the row's Status to \"Done \u2705 <date>\" (matched by Email) so the next run skips it, then loops back for the next lead in the batch."
      },
      "typeVersion": 1
    },
    {
      "id": "1eb10a02-8c4a-4f13-8ed2-e43f210b9b91",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        -288
      ],
      "parameters": {
        "width": 480,
        "height": 976,
        "content": "# \ud83c\udfaf AI-Personalized Sheet Lead Outreach\n\n\n### How it works\n\nThis workflow manually processes a batch of leads from Google Sheets, researches each lead with an AI agent, and drafts a personalized outreach email. It then creates or updates HubSpot CRM assets by creating a contact, adding a research note, and saving the email draft before marking the lead row as processed. A loop node feeds completed rows back into the batch until the selected leads are finished.\n\n### Setup steps\n\n- Connect Google Sheets credentials and configure the source sheet, lead columns, and processed-status update used by the read and mark-as-processed nodes.\n- Set the desired BATCH_SIZE in the Batch Config node.\n- Configure Google Gemini credentials for both the research agent model and the email drafting model.\n- Configure the Activate Search Tool HTTP/API credentials used by the Research Lead Agent.\n- Add HubSpot API authentication for the contact, note, and email draft HTTP request nodes, and verify the request payloads match your HubSpot property names.\n\n**Demo Google Sheet:**\n[View Sheet](https://docs.google.com/spreadsheets/d/1B12w6a3wAykGugGCiXdt2Yc0spF-aM1BkXR3B5wvpAE/edit?gid=88028505)\n\n\n### Customization\n\nAdjust the batch size, lead selection logic, research prompt/schema, email drafting prompt, and HubSpot field mappings to match your outreach process and CRM structure."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Configure Batch Size",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Lead Row": {
      "main": [
        [
          {
            "node": "Process Leads in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft Email Content": {
      "main": [
        [
          {
            "node": "Extract Email Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Research Lead Agent": {
      "main": [
        [
          {
            "node": "Remove Markdown from Research",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Activate Search Tool": {
      "ai_tool": [
        [
          {
            "node": "Research Lead Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Configure Batch Size": {
      "main": [
        [
          {
            "node": "Read Leads from Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Lead Research": {
      "ai_languageModel": [
        [
          {
            "node": "Research Lead Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email Details": {
      "main": [
        [
          {
            "node": "Create Contact in HubSpot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Research Output": {
      "ai_outputParser": [
        [
          {
            "node": "Research Lead Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Read Leads from Sheets": {
      "main": [
        [
          {
            "node": "Select Lead Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Email Generation": {
      "ai_languageModel": [
        [
          {
            "node": "Draft Email Content",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Draft HubSpot Engagement": {
      "main": [
        [
          {
            "node": "Update Processed Row in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Leads in Batches": {
      "main": [
        [],
        [
          {
            "node": "Research Lead Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Contact in HubSpot": {
      "main": [
        [
          {
            "node": "Identify HubSpot Contact ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Note to HubSpot Contact": {
      "main": [
        [
          {
            "node": "Draft HubSpot Engagement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Identify HubSpot Contact ID": {
      "main": [
        [
          {
            "node": "Add Note to HubSpot Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Markdown from Research": {
      "main": [
        [
          {
            "node": "Draft Email Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Processed Row in Sheets": {
      "main": [
        [
          {
            "node": "Process Leads in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}