{
  "id": "IyJJlqKhDAIacooh",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Decodo Google Maps Lead Enrichment",
  "tags": [],
  "nodes": [
    {
      "id": "c54c9e66-ed51-41b3-9be0-c83fafd13965",
      "name": "Parse & Normalize Data",
      "type": "n8n-nodes-base.code",
      "notes": "Normalizes Decodo response into clean lead objects. Handles different response formats.",
      "position": [
        -2160,
        368
      ],
      "parameters": {
        "jsCode": "// Helper function to extract text with Regex\nfunction extractWithRegex(text, regex) {\n  try {\n    const match = text.match(regex);\n    // [0] is full match, [1] is capture group first\n    return match && match[1] ? match[1].trim() : '';\n  } catch (e) {\n    return '';\n  }\n}\n\n// Helper function to extract text between two strings\nfunction extractBetween(text, startStr, endStr) {\n  try {\n    const startIndex = text.indexOf(startStr);\n    if (startIndex === -1) return '';\n    \n    const textAfterStart = text.substring(startIndex + startStr.length);\n    const endIndex = textAfterStart.indexOf(endStr);\n    if (endIndex === -1) return '';\n    \n    return textAfterStart.substring(0, endIndex).trim();\n  } catch (e) {\n    return '';\n  }\n}\n\n\nconst allLeads = [];\n\n// 1. Loop through the incoming n8n items\nfor (const item of items) {\n  try {\n    // item.json is the OBJECT: { \"results\": [...] }\n    const dataObject = item.json; \n\n    // 2. Check if the \"results\" array exists on this object\n    if (dataObject && dataObject.results && Array.isArray(dataObject.results)) {\n        \n      // 3. Loop through the \"results\" array\n      for (const [index, result] of dataObject.results.entries()) {\n        \n        const html = result.content;\n        const searchQuery = result.query || 'unknown_query';\n\n        if (!html || typeof html !== 'string') {\n          continue;\n        }\n\n        // *** FIX: Split by the parent container 'jscontroller=\"AtSb\"' ***\n        // This keeps the lat/lng div grouped with its business info\n        const chunks = html.split('<div jscontroller=\"AtSb\"');\n        chunks.shift(); // Remove the part before the first listing\n\n        if (chunks.length === 0) {\n          continue; \n        }\n\n        // Loop through each business HTML chunk\n        for (const chunk of chunks) {\n          \n          const businessName = extractBetween(chunk, 'class=\"dbg0pd\" aria-level=\"3\" role=\"heading\"><span class=\"OSrXXb\">', '</span></div>');\n          \n          if (!businessName) {\n            continue; // Not a valid listing\n          }\n\n          const detailsDiv = extractBetween(chunk, '</span></div><div>', '</div><div>');\n          let address = detailsDiv;\n          let phone = '';\n          if (detailsDiv.includes('\u00b7')) {\n            const parts = detailsDiv.split('\u00b7');\n            address = parts[0] ? parts[0].trim() : '';\n            phone = parts[1] ? parts[1].trim() : '';\n          }\n          \n          const openingHours = extractBetween(chunk, detailsDiv + '</div><div>', '</div>');\n          const description = extractBetween(chunk, '<span class=\"uDyWh OSrXXb btbrud\">\"', '\"</span>');\n\n          // *** FIX: Made Regex more specific to get the \"Rute\" (Route) link ***\n          let googleMapsUrl = extractWithRegex(chunk, /href=\"(\\/maps\\/dir\\/[^\"]*)\" style=\"cursor:pointer\"/);\n          if (googleMapsUrl) {\n             googleMapsUrl = 'https://www.google.com' + googleMapsUrl;\n          }\n          const website = extractWithRegex(chunk, /href=\"(http[^\"]*)\" data-ved=\"[^\"]*\">\\s*<div class=\"wLAgVc\"[^>]*>.*?<span class=\"BSaJxc\">Situs<\\/span>/);\n          const orderUrl = extractWithRegex(chunk, /data-url=\"(https:\\/\\/www\\.google\\.com\\/viewer\\/chooseprovider[^\"]*)\"/);\n\n          // *** FIX: Lat/Lng are now inside the 'chunk' because we split correctly ***\n          const lat = extractWithRegex(chunk, /data-lat=\"(-?[0-9.]*)\"/);\n          const lng = extractWithRegex(chunk, /data-lng=\"(-?[0-9.]*)\"/);\n          const cid = extractWithRegex(chunk, /data-cid=\"([0-9]*)\"/);\n\n          const lead = {\n            id: cid || `lead_${searchQuery}_${index}`,\n            businessName: businessName,\n            category: 'Restoran',\n            address: address,\n            phone: phone,\n            website: website,\n            rating: 0,\n            reviewCount: 0,\n            latitude: lat ? parseFloat(lat) : null,\n            longitude: lng ? parseFloat(lng) : null,\n            openingHours: openingHours.replace(/<[^>]*>/g, ''),\n            priceLevel: '',\n            description: description,\n            googleMapsUrl: googleMapsUrl,\n            orderUrl: orderUrl,\n            scrapedAt: new Date().toISOString(),\n            searchQuery: searchQuery\n          };\n          \n          allLeads.push({ json: lead });\n        }\n      }\n    } else {\n      allLeads.push({ json: { error: 'Skipped item: Input did not contain a .results array.', input: item.json } });\n    }\n  } catch (error) {\n    allLeads.push({ json: { error: `Gagal parse item: ${error.message}`, input: item.json } });\n  }\n}\n\n// 6. Return all extracted leads\nif (allLeads.length === 0) {\n  throw new Error('No valid leads were extracted from the input data.');\n}\n\nreturn allLeads;"
      },
      "typeVersion": 2
    },
    {
      "id": "ef0b0073-f13a-4125-ba73-ba00218a82c3",
      "name": "Split Into Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "notes": "Process leads in batches to avoid API rate limits and timeout issues.",
      "position": [
        -1840,
        368
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "915d6026-d339-491f-a5c7-a0a680c9db30",
      "name": "Save to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Saves enriched leads to Google Sheets. Use appendOrUpdate to avoid duplicates.",
      "position": [
        -1024,
        256
      ],
      "parameters": {
        "columns": {
          "value": {
            "id": "={{ $json.id }}",
            "phone": "={{ $json.phone }}",
            "rating": "={{ $json.rating }}",
            "address": "={{ $json.address }}",
            "website": "={{ $json.website }}",
            "category": "={{ $json.category }}",
            "leadScore": "={{ $json.leadScore }}",
            "scrapedAt": "={{ $json.scrapedAt }}",
            "enrichedAt": "={{ $now.toDateTime().format('yyyy-MM-dd HH:mm:ss') }}",
            "painPoints": "={{ $json.painPoints }}",
            "reviewCount": "={{ $json.reviewCount }}",
            "businessName": "={{ $json.businessName }}",
            "outreachHook": "={{ $json.outreachHook }}",
            "googleMaprUrl": "={{ $json.googleMapsUrl }}",
            "valueProposition": "={{ $json.valueProposition }}",
            "engagementStrategy": "={{ $json.engagementStrategy }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "leadsCategory",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "leadsCategory",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "businessName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "businessName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "address",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "website",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "website",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviewCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviewCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "valueProposition",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "valueProposition",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "painPoints",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "painPoints",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "outreachHook",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "outreachHook",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "leadScore",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "leadScore",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "engagementStrategy",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "engagementStrategy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "googleMaprUrl",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "googleMaprUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "scrapedAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "scrapedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrichedAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "enrichedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "outreachMessage",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "outreachMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs/edit?usp=drivesdk",
          "cachedResultName": "Google Maps Outreach"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "f480c7b4-1c3b-46ed-af0d-c34a00e7bbf8",
      "name": "Filter Hot Leads",
      "type": "n8n-nodes-base.if",
      "notes": "Filters for high-quality leads (score \u22657) with contact info for immediate outreach.",
      "position": [
        -800,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "filter-high-quality",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.leadScore }}",
              "rightValue": 7
            },
            {
              "id": "has-contact",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.phone || $json.website }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "e4efe4ba-94d7-4596-85d0-ef04972884a0",
      "name": "Error Handler",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -3024,
        672
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a148649b-de2f-4709-a9ed-b586aa71d595",
      "name": "Result Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -1440,
        384
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"valueProposition\": \"...\",\n  \"painPoints\": [\"...\", \"...\", \"...\"],\n  \"outreachHook\": \"...\",\n  \"leadScore\": 8,\n  \"engagementStrategy\": \"...\",\n  \"reasoning\": \"...\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "2974dcdf-0ff7-42d3-b5cb-3ce094f6b45b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2496,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 304,
        "content": "## Data Collection\n\nScrapes Google Maps via Decodo Maps Scrapper API, then parses HTML into structured data including business details, contact info, and coordinates."
      },
      "typeVersion": 1
    },
    {
      "id": "83a319fe-e5ab-4c1b-9744-60faf5ae357f",
      "name": "2.5 Flash",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1600,
        384
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "be7d26d0-b124-486e-bb16-02cd8537ea6d",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3072,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 288,
        "content": "## Error Handling\n\nCatches workflow failures, formats error details, sends Telegram alerts to admin."
      },
      "typeVersion": 1
    },
    {
      "id": "39612ec8-37a8-489a-ba6a-983a95aece58",
      "name": "Lead Enrichment",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1600,
        256
      ],
      "parameters": {
        "text": "=You are an expert B2B lead enrichment analyst specializing in local business intelligence. Your task is to analyze business data and create actionable insights for cold outreach campaigns.\n\nBUSINESS DATA:\n- Name: {{ $json.businessName }}\n- Category: {{ $json.category }}\n- Rating: {{ $json.rating }}/5 ({{ $json.reviewCount }} reviews)\n- Website: {{ $json.website || 'No website available' }}\n- Location: {{ $json.address }}\n- Description: {{ $json.description || 'No description available' }}\n- Phone: {{ $json.phone || 'Not provided' }}\n- Opening Hours: {{ $json.openingHours || 'Not available' }}\n\nANALYSIS REQUIREMENTS:\n\n1. VALUE PROPOSITION (2-3 sentences):\n   - Create a compelling, personalized value proposition for cold outreach\n   - Focus on specific, measurable benefits relevant to their business category\n   - Address their likely business goals (growth, efficiency, customer retention)\n   - Make it conversational and not overly salesy\n\n2. PAIN POINTS (Exactly 3 items):\n   - Identify three specific operational or business challenges they likely face\n   - Base this on their category, location, online presence, and customer feedback indicators\n   - Consider: digital transformation needs, competitive pressure, operational inefficiencies, customer acquisition costs, retention issues\n   - Be specific and actionable, not generic\n\n3. OUTREACH HOOK (1-2 sentences):\n   - Write a compelling opening line for a cold email that immediately captures attention\n   - Reference specific details from their business (rating, reviews, location, category)\n   - Create curiosity or demonstrate value without being pushy\n   - Avoid clich\u00e9s like \"I noticed you don't have...\" or \"Are you tired of...\"\n   - Make it about THEM, not your product\n\n4. LEAD SCORE (1-10 scale):\n   Calculate based on these weighted criteria:\n   - Business reputation: Rating \u22654.5 stars (+2), 4.0-4.4 (+1), <4.0 (0)\n   - Social proof: Reviews \u2265100 (+2), 50-99 (+1.5), 20-49 (+1), <20 (0)\n   - Digital maturity: Professional website (+2), basic website (+1), no website (0)\n   - Contact availability: Phone + website (+1), phone only (+0.5), neither (-1)\n   - Market position: High-end category/location (+1), mid-market (+0.5), budget (0)\n   - Engagement potential: Active business with recent reviews (+1), stagnant (0)\n   \n   CRITICAL: If missing critical data (name unknown, no contact info), cap score at 5\n\n5. ENGAGEMENT STRATEGY (2-3 sentences):\n   - Recommend the best initial contact method: email, phone call, LinkedIn, or in-person visit\n   - Specify timing recommendations (best day/time to reach out)\n   - Suggest follow-up approach and cadence\n   - Consider their business type, location, and decision-maker accessibility\n\n6. REASONING (1-2 sentences):\n   - Briefly explain the lead score rationale\n   - Highlight the strongest opportunity or biggest challenge\n   - Note any data gaps that might affect assessment accuracy\n\nOUTPUT FORMAT:\nReturn ONLY valid JSON with no additional text, markdown, or explanations:\n{\n  \"valueProposition\": \"string (2-3 compelling sentences)\",\n  \"painPoints\": [\"string (specific pain point 1)\", \"string (specific pain point 2)\", \"string (specific pain point 3)\"],\n  \"outreachHook\": \"string (1-2 attention-grabbing sentences)\",\n  \"leadScore\": number (integer 1-10),\n  \"engagementStrategy\": \"string (2-3 sentences with specific recommendations)\",\n  \"reasoning\": \"string (1-2 sentences explaining score and opportunities)\"\n}\n\nIMPORTANT GUIDELINES:\n- Be specific and actionable, avoid generic business advice\n- Tailor everything to their specific business category and location\n- If data is incomplete, acknowledge uncertainty in reasoning but still provide useful insights\n- Focus on what you CAN determine rather than what's missing\n- Use natural language that sounds like a knowledgeable consultant, not a robot\n- Never fabricate information - if you don't have data, say so in reasoning but provide educated estimates",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "a2406796-1658-428a-aeec-f88320a6fe3c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 432,
        "content": "## Hot Leads Processing\n\nSaves all enriched leads to Sheets. Filters high-quality prospects (score \u22657 with contact info), generates personalized email templates, updates status as \"HOT\"."
      },
      "typeVersion": 1
    },
    {
      "id": "a292f2b0-da79-40dc-8049-3be70559b52f",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 432,
        "content": "## AI Enrichment Loop\n\nProcesses leads in batches. Gemini 2.5 Flash analyzes each business to generate value propositions, identify pain points, create outreach hooks, and assign quality scores (1-10)."
      },
      "typeVersion": 1
    },
    {
      "id": "a814d273-c43d-46b5-92f0-387f68b8240f",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2352,
        560
      ],
      "parameters": {
        "color": 6,
        "width": 528,
        "height": 288,
        "content": "## Create Decodo Credentials\nService: Decodo Maps Scrapper\nNode: HTTP Request\nURL: https://scraper-api.decodo.com/v2/scrape\nCredential Type: HTTP Header Auth\nHeader Name: Authorization\nHeader Value: Basic [YOUR_DECODO_API_KEY]\n\nGet API key: https://dashboard.decodo.com/web-scraping-api/scraper?target=google_maps\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "40f012cf-ffcb-4ab8-b375-1c58a8bac292",
      "name": "Decodo Maps Scraper",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Scrapes Google Maps using Decodo API. Set up HTTP Header Auth with your API key.",
      "position": [
        -2400,
        368
      ],
      "parameters": {
        "url": "https://scraper-api.decodo.com/v2/scrape",
        "method": "POST",
        "options": {
          "timeout": 120000
        },
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "target",
              "value": "google_maps"
            },
            {
              "name": "query",
              "value": "={{ $json.searchQuery }}"
            },
            {
              "name": "page_from",
              "value": "1"
            },
            {
              "name": "headless",
              "value": "html"
            },
            {
              "name": "google_results_language",
              "value": "={{ $json.targetLanguage }}"
            },
            {
              "name": "geo",
              "value": "={{ $json.country }}"
            },
            {
              "name": "page_to",
              "value": "={{ Math.ceil(parseInt($json.resultsLimit) / 10) }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "7b927ca8-69f0-4565-bc1b-631e52b0f0f3",
      "name": "Merge Enrichment Data",
      "type": "n8n-nodes-base.set",
      "position": [
        -1248,
        256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0a0809e9-db9d-48d9-813f-e2d9d4d8f1cb",
              "name": "id",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.id }}"
            },
            {
              "id": "f0e5fc35-1303-4db8-ab31-dc0015e82eb9",
              "name": "businessName",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.businessName }}"
            },
            {
              "id": "63d25c3c-6274-4dee-bf7c-f8fefe8dd33e",
              "name": "category",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.category }}"
            },
            {
              "id": "651f590e-c256-4cf3-8f53-e44c23d0f7c7",
              "name": "valueProposition",
              "type": "string",
              "value": "={{ $json.output.valueProposition }}"
            },
            {
              "id": "eeeb5842-be49-4596-9a4a-a03af7d1169b",
              "name": "painPoints",
              "type": "string",
              "value": "={{ $json.output.painPoints[0] }}"
            },
            {
              "id": "14db2aca-8d00-48b2-80c7-b832c4c8070b",
              "name": "outreachHook",
              "type": "string",
              "value": "={{ $json.output.outreachHook }}"
            },
            {
              "id": "1f10742c-cc68-46fb-a2bb-882d17e06ff4",
              "name": "leadScore",
              "type": "number",
              "value": "={{ $json.output.leadScore }}"
            },
            {
              "id": "2ad4e2ed-67b1-42ce-9fea-6982905e3707",
              "name": "engagementStrategy",
              "type": "string",
              "value": "={{ $json.output.engagementStrategy }}"
            },
            {
              "id": "87ff456c-ef7a-4c21-9e1d-b2bd71d7b6a0",
              "name": "reasoning",
              "type": "string",
              "value": "={{ $json.output.reasoning }}"
            },
            {
              "id": "89e1d25e-61ee-46a2-95d4-cc4c34671d32",
              "name": "address",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.address }}"
            },
            {
              "id": "fae08fca-6227-45b6-9002-516057b4f237",
              "name": "phone",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.phone }}"
            },
            {
              "id": "3aaf1d60-bf59-4ef3-8e90-2fd57cf0a605",
              "name": "website",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.website }}"
            },
            {
              "id": "dc6ebf0a-5e43-4f8e-abe6-404d16f6d995",
              "name": "rating",
              "type": "number",
              "value": "={{ $('Split Into Batches').item.json.rating }}"
            },
            {
              "id": "1f17191e-e5b4-4e7a-90b0-1d7f9a12f902",
              "name": "reviewCount",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.reviewCount }}"
            },
            {
              "id": "f76837b3-734d-48d3-86d4-77e6081c9431",
              "name": "googleMapsUrl",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.googleMapsUrl }}"
            },
            {
              "id": "8bd36ee5-bd43-4da6-8409-e3a6039a4e1d",
              "name": "scrapedAt",
              "type": "string",
              "value": "={{ $('Split Into Batches').item.json.scrapedAt.toDateTime().format('yyyy-MM-dd HH:mm:ss') }}"
            },
            {
              "id": "a3b4d978-a640-4925-8490-7abb5aa73867",
              "name": "statusOutreach",
              "type": "string",
              "value": "not send"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "da4b3434-5fba-4c5d-b239-4770286acdbd",
      "name": "Prepare Outreach Message",
      "type": "n8n-nodes-base.set",
      "notes": "Prepares personalized outreach message using AI-generated hooks.",
      "position": [
        -592,
        336
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "email-subject",
              "name": "emailSubject",
              "type": "string",
              "value": "=Quick question about {{ $json.businessName }}"
            },
            {
              "id": "email-body",
              "name": "emailBody",
              "type": "string",
              "value": "=Hi {{ $json.businessName }} team,\n\n{{ $json.outreachHook }}\n\n{{ $json.valueProposition }}\n\nWould you be open to a 15-minute chat this week?\n\nBest regards,\n[Your Name]\n[Your Company]"
            },
            {
              "id": "recipient",
              "name": "recipientEmail",
              "type": "string",
              "value": "={{ $json.email || 'MANUAL_LOOKUP_NEEDED' }}"
            },
            {
              "id": "17aea5ed-3c5c-40ec-8ecf-cb1db7eb5a52",
              "name": "leadScore",
              "type": "string",
              "value": "={{ $json.leadScore }}"
            },
            {
              "id": "fbaf37ba-95a3-4ff5-8a11-18633831d6ba",
              "name": "id",
              "type": "string",
              "value": "={{ $json.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "919d9446-e033-42b6-a2fe-8eb7f0180b11",
      "name": "Save Outreach To Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Saves enriched leads to Google Sheets. Use appendOrUpdate to avoid duplicates.",
      "position": [
        -400,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "id": "={{ $json.id }}",
            "enrichedAt": "={{ $now.toDateTime().format('yyyy-MM-dd HH:mm:ss') }}",
            "leadsCategory": "HOT",
            "outreachMessage": "=subject: {{ $json.emailSubject }}\n\n{{ $json.emailBody }}\n"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "leadsCategory",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "leadsCategory",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "businessName",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "businessName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "address",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "website",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "website",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviewCount",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "reviewCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "valueProposition",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "valueProposition",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "painPoints",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "painPoints",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "outreachHook",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "outreachHook",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "leadScore",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "leadScore",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "engagementStrategy",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "engagementStrategy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "googleMaprUrl",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "googleMaprUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "scrapedAt",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "scrapedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrichedAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "enrichedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "outreachMessage",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "outreachMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1on82bf0niAySDtRUfXrhFR8aTcrVGdgCDZS5L1RAvcs/edit?usp=drivesdk",
          "cachedResultName": "Google Maps Outreach"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "8e072a72-0042-4fef-912f-fe2a4ac1fc90",
      "name": "Format Error Message",
      "type": "n8n-nodes-base.code",
      "position": [
        -2800,
        672
      ],
      "parameters": {
        "jsCode": "// Optimized error handler - clean & informative\nconst errorData = $input.first().json;\n\nfunction getNestedValue(obj, path, defaultValue = 'Unknown') {\n  return path.split('.').reduce((curr, prop) => \n    curr?.[prop], obj) || defaultValue;\n}\n\nconst workflowName = getNestedValue(errorData, 'workflow.name', 'Unknown Workflow');\nconst nodeName = getNestedValue(errorData, 'execution.error.node.name', 'Unknown Node');\nconst errorMessage = getNestedValue(errorData, 'execution.error.message', 'No error message');\nconst timestamp = getNestedValue(errorData, 'execution.startedAt', new Date().toISOString());\nconst executionId = getNestedValue(errorData, 'execution.id', 'Unknown');\n\n// Tangkap error details\nconst errorObj = errorData.execution?.error || {};\nconst errorDescription = errorObj.description || '';\nconst errorHttpCode = errorObj.httpCode || '';\n\n// Stack trace - ambil HANYA baris pertama yang paling penting\nconst errorStack = errorObj.stack || '';\nconst mainError = errorStack.split('\\n')[0] || '';\n\n// Format detail yang clean\nlet errorDetails = [];\nif (errorDescription && errorDescription !== errorMessage) {\n  errorDetails.push(errorDescription);\n}\nif (errorHttpCode) {\n  errorDetails.push(`HTTP ${errorHttpCode}`);\n}\n\nconst detailText = errorDetails.length > 0 \n  ? errorDetails.join(' | ') \n  : 'No additional details';\n\n// Main error dari stack (tanpa \"NodeApiError: \" prefix)\nconst cleanMainError = mainError.replace(/^(NodeApiError|Error):\\s*/, '').trim();\n\n// Build message - COMPACT VERSION\nconst message = `\ud83d\udea8 <b>WORKFLOW ERROR</b>\\n\\n` +\n  `\ud83d\udccb <b>${workflowName}</b>\\n` +\n  `\u2699\ufe0f Node: <code>${nodeName}</code>\\n\\n` +\n  `\u274c <b>${errorMessage}</b>\\n` +\n  `\ud83d\udca1 ${detailText}\\n\\n` +\n  `\ud83d\udd50 ${new Date(timestamp).toLocaleString('id-ID', { \n    day: '2-digit', \n    month: 'short', \n    hour: '2-digit', \n    minute: '2-digit' \n  })}\\n` +\n  `\ud83d\udd17 Exec: <code>${executionId}</code>`;\n\nreturn {\n  json: {\n    message: message\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2ea4e411-5e49-4d4f-8cf4-ba6dd4868912",
      "name": "Send Error Notification",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -2576,
        672
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "YOUR-CHAT-ID",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1dc9988f-ff99-46b7-9558-c94e41be740a",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3072,
        -368
      ],
      "parameters": {
        "width": 1104,
        "height": 544,
        "content": "# Workflow Overview\n## How it works\n\nScrapes Google Maps via Decodo API, enriches each business with AI Agent, scores lead quality (1-10), and generates personalized outreach emails. Hot leads (score \u22657) get saved to Google Sheets with ready to send messages.\n\n## Setup steps\n\n1. **Decodo API:** Get key from [decodo.com](https://dashboard.decodo.com/web-scraping-api/scraper?target=google_maps) Add as HTTP Header Auth credential (Authorization: Basic YOUR_KEY)\n\n2. **Google Gemini:** Add API key in \"Gemini 2.5 Flash\" from ai.dev\n\n3. **Configure search:** Update \"Set Search Parameters\" with your query (e.g., \"coffee shops in Austin\"), country, and resultsLimit\n\n4. **Test:** Start with resultsLimit: 5 to verify connections\n\n5. **Optional:** Add Telegram bot token for error notifications"
      },
      "typeVersion": 1
    },
    {
      "id": "052457f2-99d4-4291-9122-584615f68992",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -2992,
        368
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "b108d3b9-4224-452d-a89a-ce007761cc40",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2832,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 304,
        "content": "## Input Configuration\n\nSet search query, target country, language, and results limit. Adjust these parameters for different markets or niches."
      },
      "typeVersion": 1
    },
    {
      "id": "9095d650-d056-4406-a787-a78187e842a9",
      "name": "Set Search Parameters",
      "type": "n8n-nodes-base.set",
      "notes": "Configure your search query here. Update searchQuery for different niches.",
      "position": [
        -2736,
        368
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "95a5b940-2ea4-4511-80d5-237a33d9c411",
              "name": "searchQuery",
              "type": "string",
              "value": "YOUR-QUERY_HERE"
            },
            {
              "id": "a6a2004b-daa2-43e1-ad8c-e0b51f42c272",
              "name": "targetLanguage",
              "type": "string",
              "value": "en"
            },
            {
              "id": "b4abd52f-16a4-492c-bec7-a95fadfce2ba",
              "name": "country",
              "type": "string",
              "value": "Country Name"
            },
            {
              "id": "757c413f-7385-4de5-8368-9bc4016e40a3",
              "name": "resultsLimit",
              "type": "number",
              "value": 5
            }
          ]
        }
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3e060442-edc4-4b19-97e2-c8cdf39404d9",
  "connections": {
    "2.5 Flash": {
      "ai_languageModel": [
        [
          {
            "node": "Lead Enrichment",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Error Handler": {
      "main": [
        [
          {
            "node": "Format Error Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Result Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Lead Enrichment",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Search Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Enrichment": {
      "main": [
        [
          {
            "node": "Merge Enrichment Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Hot Leads": {
      "main": [
        [
          {
            "node": "Prepare Outreach Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Into Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Batches": {
      "main": [
        [],
        [
          {
            "node": "Lead Enrichment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decodo Maps Scraper": {
      "main": [
        [
          {
            "node": "Parse & Normalize Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Error Message": {
      "main": [
        [
          {
            "node": "Send Error Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Enrichment Data": {
      "main": [
        [
          {
            "node": "Save to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Google Sheets": {
      "main": [
        [
          {
            "node": "Filter Hot Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Search Parameters": {
      "main": [
        [
          {
            "node": "Decodo Maps Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Normalize Data": {
      "main": [
        [
          {
            "node": "Split Into Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Outreach To Sheets": {
      "main": [
        [
          {
            "node": "Split Into Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Outreach Message": {
      "main": [
        [
          {
            "node": "Save Outreach To Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}