AutomationFlowsData & Sheets › N8nworkflow

N8nworkflow

N8Nworkflow. Uses stopAndError, httpRequest, mySql. Webhook trigger; 22 nodes.

Webhook trigger★★★★☆ complexity22 nodesStop And ErrorHTTP RequestMySQL
Data & Sheets Trigger: Webhook Nodes: 22 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Stopanderror 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
{
  "name": "My workflow",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "`search`",
        "responseMode": "lastNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        4240,
        8576
      ],
      "id": "bcbd7779-5f36-43c2-b189-6048ef13f5bf",
      "name": "Webhook"
    },
    {
      "parameters": {
        "errorMessage": "400 Simple Error "
      },
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        4912,
        8624
      ],
      "id": "18a14323-8769-466f-95a7-dde5bb307f0b",
      "name": "Stop and Error"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst keywords = items[0].json.keywords;\nconst area = items[0].json.area || '';\nconst region = items[0].json.region || '';\nconst searchId = items[0].json.searchId;\n\n// Arrays de t\u00e9rminos que mejoran la b\u00fasqueda de contactos\nconst contactTerms = [\n  'email contacto',\n  'directorio profesionales',\n  'perfil profesional',\n  'curriculum vitae'\n];\n\nconst queries = [];\n\n// Generar m\u00faltiples queries con diferentes enfoques\ncontactTerms.forEach(term => {\n  queries.push({\n    query: `${keywords} ${region} ${term} site:.cl OR site:linkedin.com OR site:researchgate.net`,\n    searchTerm: keywords,\n    area: area,\n    region: region,\n    searchId: searchId,\n    queryType: term\n  });\n});\n\n// Query espec\u00edfica para colegios profesionales\nif (area && area.toLowerCase().includes('salud')) {\n  queries.push({\n    query: `${keywords} ${region} site:colegiomedicochile.cl OR site:minsal.cl contacto`,\n    searchTerm: keywords,\n    area: area,\n    region: region,\n    searchId: searchId,\n    queryType: 'colegio_profesional'\n  });\n}\n\nreturn queries.map(q => ({ json: q }));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4912,
        8432
      ],
      "id": "34fc3a5b-e66a-4949-9bef-c19305fdf730",
      "name": "Build Search Query"
    },
    {
      "parameters": {
        "url": "https://serpapi.com/search",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{$json.query}}"
            },
            {
              "name": "api_key",
              "value": "d401a6726f70ad6a019aafe5a4a4496171d4e50becb4f7ec1047346d4c8b1036"
            },
            {
              "name": "location",
              "value": "chile"
            },
            {
              "name": "gl",
              "value": "cl"
            },
            {
              "name": "hl",
              "value": "es"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        5136,
        8432
      ],
      "id": "55246d27-a97b-46ca-9960-7b4c639e827d",
      "name": "Google Custom Search API",
      "retryOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "id": "aa00dba6-61ac-4f88-9dc2-8d9d2793170d",
              "leftValue": "={{$json.keywords}}",
              "rightValue": "undefined",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            },
            {
              "id": "d82c94fd-6e96-4ee7-b2ca-8fbb5d96ab43",
              "leftValue": "={{$json.keywords.length}}",
              "rightValue": 3,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            },
            {
              "id": "f1e94d69-83b8-4c3a-8ea0-0df622386d07",
              "leftValue": "={{$json.searchId}}",
              "rightValue": 0,
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4688,
        8576
      ],
      "id": "403711b2-b561-467d-9204-bc9edf739c40",
      "name": "IF Data is Valid",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst body = items[0].json.body;\n\nreturn [{\n  json: {\n    searchId: body.search_id,\n    keywords: body.keywords,\n    area: body.area,\n    region: body.region,\n    timestamp: new Date().toISOString(),\n    startTime: Date.now()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4464,
        8576
      ],
      "id": "31b91048-124b-439b-ae0a-3e9d8d45175e",
      "name": "Data Structure"
    },
    {
      "parameters": {
        "jsCode": "// ==================== PARSER MEJORADO - AUTO-DETECT ESTRUCTURA ====================\n\ntry {\n  let inputData = $input.all();\n  \n  // Si inputData est\u00e1 vac\u00edo o no tiene items, intentar extraer del primer objeto\n  if (!inputData || inputData.length === 0) {\n    return [{ json: { items: [] } }];\n  }\n\n  // DEBUG: Ver qu\u00e9 estructura tiene el input\n  const firstItem = inputData[0];\n  let itemsToProcess = [];\n\n  // Intenta detectar d\u00f3nde est\u00e1n realmente los datos\n  if (firstItem.json && Array.isArray(firstItem.json)) {\n    // Si json es directamente un array\n    itemsToProcess = firstItem.json;\n  } else if (firstItem.json && Array.isArray(firstItem.json.items)) {\n    // Si json tiene un campo \"items\"\n    itemsToProcess = firstItem.json.items;\n  } else if (firstItem.json && Array.isArray(firstItem.json.organic_results)) {\n    // Si es de SerpAPI directamente\n    itemsToProcess = firstItem.json.organic_results;\n  } else if (Array.isArray(firstItem.json)) {\n    // Intenta directamente\n    itemsToProcess = firstItem.json;\n  } else {\n    // Si firstItem.json es un objeto individual\n    itemsToProcess = inputData.map(item => item.json);\n  }\n\n  // Si a\u00fan est\u00e1 vac\u00edo\n  if (!itemsToProcess || itemsToProcess.length === 0) {\n    return [{ json: { items: [], debug: \"No se encontraron datos para procesar\" } }];\n  }\n\n  // ==================== MAPEO REGIONES CHILE ====================\n  const regionKeywords = {\n    \"Arica y Parinacota\": [\"arica y parinacota\", \"arica\", \"parinacota\"],\n    \"Tarapac\u00e1\": [\"tarapac\u00e1\", \"tarapaca\", \"iquique\"],\n    \"Antofagasta\": [\"antofagasta\"],\n    \"Atacama\": [\"atacama\", \"copiap\u00f3\", \"copiapo\"],\n    \"Coquimbo\": [\"coquimbo\", \"la serena\"],\n    \"Valpara\u00edso\": [\"valpara\u00edso\", \"valparaiso\", \"vi\u00f1a\", \"quilpu\u00e9\", \"conc\u00f3n\"],\n    \"Santiago\": [\"santiago\", \"metropolitana\", \"rm\", \"\u00f1u\u00f1oa\"],\n    \"O'Higgins\": [\"o'higgins\", \"ohiggins\", \"rancagua\"],\n    \"Maule\": [\"maule\", \"talca\"],\n    \"\u00d1uble\": [\"\u00f1uble\", \"nuble\", \"chill\u00e1n\", \"chillan\"],\n    \"Biob\u00edo\": [\"biob\u00edo\", \"biobio\", \"b\u00edo b\u00edo\", \"bio bio\", \"concepci\u00f3n\", \"concepcion\"],\n    \"La Araucan\u00eda\": [\"la araucan\u00eda\", \"la araucania\", \"araucan\u00eda\", \"araucania\", \"temuco\"],\n    \"Los R\u00edos\": [\"los r\u00edos\", \"los rios\", \"valdivia\"],\n    \"Los Lagos\": [\"los lagos\", \"puerto montt\"],\n    \"Ays\u00e9n\": [\"ays\u00e9n\", \"aysen\", \"coihaique\", \"coyhaique\"],\n    \"Magallanes\": [\"magallanes\", \"punta arenas\"],\n  };\n\n  const results = [];\n\n  // Procesar cada item\n  for (let idx = 0; idx < itemsToProcess.length; idx++) {\n    const item = itemsToProcess[idx];\n\n    // Validar datos b\u00e1sicos\n    if (!item.link && !item.title) {\n      continue;\n    }\n\n    // Detectar regi\u00f3n\n    let regionInferred = \"Sin especificar\";\n    const searchText = ((item.snippet || \"\") + \" \" + (item.title || \"\") + \" \" + (item.link || \"\")).toLowerCase();\n\n    for (const [region, regionKeywordsList] of Object.entries(regionKeywords)) {\n      let found = false;\n      for (const keyword of regionKeywordsList) {\n        if (searchText.includes(keyword.toLowerCase())) {\n          regionInferred = region;\n          found = true;\n          break;\n        }\n      }\n      if (found) break;\n    }\n\n    // Construir resultado\n    results.push({\n      metadata: {\n        title: item.title || null,\n        link: item.link || null,\n        snippet: item.snippet || null,\n        source: item.source || null,\n        position: item.position || null,\n      },\n      region: regionInferred,\n      search_id: null,\n      keywords: null,\n      processed: false,\n      html_content: null,\n      contact_data: null,\n    });\n  }\n\n  return [{ json: { items: results, count: results.length } }];\n\n} catch (err) {\n  let errorMsg = \"Unknown error\";\n  if (typeof err === 'string') {\n    errorMsg = err;\n  } else if (err && typeof err === 'object' && 'message' in err) {\n    errorMsg = String(err.message);\n  }\n  return [{ json: { items: [], error: errorMsg } }];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5360,
        8432
      ],
      "id": "f2fbe179-3b03-4054-9895-ef9cb14c8711",
      "name": "Parser1"
    },
    {
      "parameters": {
        "jsCode": "try {\n  // Obtener todos los items\n  const input = $input.first().json;\n  let items = [];\n  \n  // Si viene con estructura de Parser (items array), procesar todos\n  if (input.items && Array.isArray(input.items) && input.items.length > 0) {\n    items = input.items;\n  } else if (input.metadata) {\n    // Si ya es un item individual\n    items = [input];\n  } else {\n    throw new Error(\"Invalid input structure: no metadata found\");\n  }\n  \n  // Procesar cada item\n  return items.map(item => {\n    try {\n      const metadata = item.metadata;\n      const link = metadata.link.toLowerCase();\n      const title = metadata.title.toLowerCase();\n\n  // ==================== PATRONES A IGNORAR ====================\n  const ignorePatterns = {\n    // Marketplaces de servicios\n    marketplace: [\n      \"cronoshare.cl\",\n      \"starofservice.cl\",\n      \"uber.com\",\n      \"airbnb.com\",\n      \"fiverr.com\",\n      \"upwork.com\",\n      \"freelancer.com\",\n    ],\n    // Bolsas de empleo\n    jobsites: [\n      \"computrabajo.cl\",\n      \"linkedin.com/jobs\",\n      \"trabajos.com\",\n      \"buscojob.com\",\n      \"indeed.com\",\n      \"infojobs.com\",\n    ],\n    // Instituciones educativas\n    education: [\n      \"culinary.cl\",\n      \".edu.cl\",\n      \"universidad\",\n      \"escuela\",\n      \"curso\",\n      \"clase en l\u00ednea\",\n      \"diplomado\",\n    ],\n    // Otros no deseados\n    other: [\n      \"google.com\",\n      \"facebook.com\",\n      \"instagram.com\",\n      \"tiktok.com\",\n      \"youtube.com\",\n      \"wikipedia.org\",\n      \"wix.com\",\n      \"squarespace.com\",\n    ],\n  };\n\n  // ==================== CLASIFICACI\u00d3N ====================\n\n  let shouldIgnore = false;\n  let ignoreReason = null;\n\n  // Verificar si es oferta de trabajo\n  if (title.includes(\"oferta\") || title.includes(\"trabajo\") || title.includes(\"empleo\")) {\n    if (!link.includes(\"linkedin.com\")) { // LinkedIn profiles s\u00ed son v\u00e1lidos\n      shouldIgnore = true;\n      ignoreReason = \"job_listing\";\n    }\n  }\n\n  // Verificar patrones a ignorar\n  for (const [category, patterns] of Object.entries(ignorePatterns)) {\n    for (const pattern of patterns) {\n      if (link.includes(pattern.toLowerCase())) {\n        shouldIgnore = true;\n        ignoreReason = category;\n        break;\n      }\n    }\n    if (shouldIgnore) break;\n  }\n\n  // ==================== VALIDACI\u00d3N POSITIVA ====================\n  // Si pas\u00f3 los filtros negativos, verificar que tenga datos de profesional\n\n  const validSources = [\n    \"linkedin.com\", // LinkedIn profiles\n    \"superprof.cl\", // Directorio profesionales\n    \"doctoralia.cl\", // M\u00e9dicos\n    \"topdoctors.es\", // M\u00e9dicos\n    \".cl\", // Sitios chilenos generales\n  ];\n\n  let isValidSource = false;\n  for (const source of validSources) {\n    if (link.includes(source)) {\n      isValidSource = true;\n      break;\n    }\n  }\n\n  // Si no es marketplace/bolsa/escuela Y es fuente v\u00e1lida\n  shouldIgnore = shouldIgnore || !isValidSource;\n\n  // ==================== RESULTADO ====================\n\n      return {\n        json: {\n          ...item,\n          classification: {\n            should_process: !shouldIgnore,\n            ignore_reason: ignoreReason,\n            is_linkedin: link.includes(\"linkedin.com\"),\n            source_type: ignoreReason || \"professional\",\n          },\n        },\n      };\n    } catch (itemErr) {\n      return {\n        json: {\n          ...item,\n          classification: {\n            should_process: false,\n            ignore_reason: \"error\",\n            error: String(itemErr.message || itemErr),\n          },\n        },\n      };\n    }\n  });\n\n} catch (err) {\n  // Error general\n  let errorMsg = \"Unknown error\";\n  if (typeof err === 'string') {\n    errorMsg = err;\n  } else if (err && typeof err === 'object' && 'message' in err) {\n    errorMsg = String(err.message);\n  } else {\n    errorMsg = String(err);\n  }\n  \n  return [{ json: { \n    classification: {\n      should_process: false,\n      ignore_reason: \"error\",\n      error: errorMsg,\n    }\n  }}];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5584,
        8432
      ],
      "id": "43286d55-6c54-4367-9c0e-dad43d509988",
      "name": "Link Classifier"
    },
    {
      "parameters": {
        "jsCode": "try {\n  // Procesar TODOS los items que vienen del IF node\n  const allItems = $input.all();\n  \n  return allItems.map(itemWrapper => {\n    const item = itemWrapper.json;\n    const metadata = item.metadata;\n    const title = metadata.title;\n    const snippet = metadata.snippet;\n    const link = metadata.link;\n\n  // ==================== EXTRACCI\u00d3N NOMBRE ====================\n  // Formato t\u00edpico LinkedIn: \"Nombre Apellido - Cargo, experiencia...\"\n  \n  let name = null;\n  const nameMatch = title.match(/^([A-Za-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00c1\u00c9\u00cd\u00d3\u00da\u00f1\u00d1\\s]+?)(?:\\s*-|$)/);\n  if (nameMatch) {\n    name = nameMatch[1].trim();\n    // Limpiar si a\u00fan tiene ruido\n    if (name.length < 3 || name.toLowerCase().includes(\"error\")) {\n      name = null;\n    }\n  }\n\n  // ==================== EXTRACCI\u00d3N CARGO ====================\n  // Buscar despu\u00e9s del primer gui\u00f3n\n  let position = null;\n  const posMatch = title.match(/\\s*-\\s*([^,]+)/);\n  if (posMatch) {\n    position = posMatch[1].trim();\n    // Tomar solo la primera parte (antes de a\u00f1os de experiencia)\n    position = position.split(/[,\u00b7]/)[0].trim();\n    // Limitar a 50 caracteres\n    if (position.length > 50) {\n      position = position.split(/\\s+/).slice(0, 3).join(\" \");\n    }\n  }\n\n  // ==================== EMAIL & TEL\u00c9FONO ====================\n  const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n  const emails = snippet.match(emailRegex) || [];\n  const email = emails.length > 0 ? emails[0] : null;\n\n  const phoneRegex = /(\\+?56|0)?[\\s]?9[\\s]?[\\d]{4}[\\s]?[\\d]{4}|(\\+?56|0)?[\\s]?2[\\s]?[\\d]{4}[\\s]?[\\d]{4}/g;\n  const phones = snippet.match(phoneRegex) || [];\n  const phone = phones.length > 0 ? phones[0] : null;\n\n  // ==================== VALIDACI\u00d3N ====================\n  // is_valid: true solo si tiene NOMBRE + (CARGO O EMAIL O TEL\u00c9FONO)\n  const isValid = !!(name && (position || email || phone));\n  \n  // validation_score ser\u00e1 calculado en el Backend seg\u00fan criterios finales\n  // Aqu\u00ed solo guardamos la informaci\u00f3n extra\u00edda\n    const validationScore = isValid ? 1 : 0; // Score temporal, Backend lo recalcula\n\n    return {\n      json: {\n        ...item,\n        processed: true,\n        contact_data: {\n          name: name || \"Desconocido\",\n          email: email,\n          phone: phone,\n          position: position,\n          organization: \"LinkedIn\",\n          region: item.region,\n          source_url: link,\n          source_type: \"LinkedIn\",\n          is_valid: isValid,\n          validation_score: validationScore,\n        },\n      },\n    };\n  });\n\n} catch (err) {\n  // Error general\n  let errorMsg = \"Unknown error\";\n  if (typeof err === 'string') {\n    errorMsg = err;\n  } else if (err && typeof err === 'object' && 'message' in err) {\n    errorMsg = String(err.message);\n  } else {\n    errorMsg = String(err);\n  }\n  \n  return [{ json: { \n    contact_data: {\n      is_valid: false,\n      validation_score: 0,\n      scraping_source: \"error\",\n      scraping_error: errorMsg,\n    }\n  }}];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6928,
        8336
      ],
      "id": "ccd244f5-26bf-4fe7-88a7-e3ba4d9e2fb5",
      "name": "Code in JavaScript6"
    },
    {
      "parameters": {
        "url": "={{ $json.metadata.link }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
            },
            {
              "name": "Accept",
              "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
            },
            {
              "name": "Accept-Language",
              "value": "es-ES,es;q=0.9"
            },
            {
              "name": "Referer",
              "value": "https://www.google.com/"
            }
          ]
        },
        "options": {
          "allowUnauthorizedCerts": false,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        6256,
        8624
      ],
      "id": "564b16c8-fabb-4764-9741-96ce032e2a09",
      "name": "HTTP Request3",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Combina respuesta HTTP con datos originales\n// Mantiene estructura completa aunque HTTP falle\n// Detecta correctamente cuando HTTP devuelve error\n\ntry {\n  const allItems = $input.all();\n  \n  return allItems.map(itemWrapper => {\n    const item = itemWrapper.json;\n    \n    // Detectar si fue error (tiene status error) o \u00e9xito\n    let htmlContent = null;\n    let statusCode = null;\n    let fetchError = null;\n\n    // ==================== DETECTAR ESTRUCTURA DE RESPUESTA ====================\n    \n    // Si viene con error (HTTP Request con Continue on Error)\n    if (item.error) {\n      statusCode = item.error.status || item.error.statusCode || 403;\n      fetchError = item.error.message || \"HTTP Error\";\n      htmlContent = null;\n    }\n    // Si viene con statusCode y body\n    else if (item.statusCode === 200 && item.body) {\n      htmlContent = item.body;\n      statusCode = 200;\n      fetchError = null;\n    }\n    // Si viene con error status\n    else if (item.statusCode === 403) {\n      statusCode = 403;\n      fetchError = \"CAPTCHA/Blocked (403)\";\n      htmlContent = null;\n    }\n    else if (item.statusCode >= 500) {\n      statusCode = item.statusCode;\n      fetchError = `Server error (${statusCode})`;\n      htmlContent = null;\n    }\n    else if (item.statusCode) {\n      statusCode = item.statusCode;\n      fetchError = `HTTP ${statusCode}`;\n      htmlContent = null;\n    }\n    else {\n      fetchError = \"Unknown error\";\n      statusCode = null;\n    }\n\n    // ==================== CONSTRUIR RESPUESTA LIMPIA ====================\n    // Preservar TODO del item original EXCEPTO el campo \"error\"\n    // (destructuring elimina error, spread incluye todo lo dem\u00e1s)\n    const { error, ...cleanItem } = item;\n\n    return {\n      json: {\n        ...cleanItem,\n        html_fetch: {\n          success: htmlContent ? true : false,\n          status_code: statusCode,\n          error: fetchError,\n          html_content: htmlContent,\n        },\n      },\n    };\n  });\n\n} catch (err) {\n  let errorMsg = \"Unknown error\";\n  if (typeof err === 'string') {\n    errorMsg = err;\n  } else if (err && typeof err === 'object' && 'message' in err) {\n    errorMsg = String(err.message);\n  } else {\n    errorMsg = String(err);\n  }\n\n  return [{ json: {\n    html_fetch: {\n      success: false,\n      error: errorMsg,\n      html_content: null,\n      status_code: null,\n    }\n  }}];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6704,
        8528
      ],
      "id": "7f605f0f-8d2b-4123-b968-ec42ed9e2a91",
      "name": "Combine Data"
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        6480,
        8528
      ],
      "id": "69ecf90d-92c0-4425-b53f-e407e79e407e",
      "name": "Prepare Data"
    },
    {
      "parameters": {
        "jsCode": "try {\n  const allItems = $input.all();\n  \n  return allItems.map(itemWrapper => {\n    const item = itemWrapper.json;\n    const metadata = item.metadata;\n    \n    // Obtener HTML de la estructura de 3B-1B\n    let htmlContent = null;\n    let source = \"snippet\";\n    let extractionNotes = \"\";\n\n    // Verificar estructura html_fetch limpia de 3B-1B\n    if (item.html_fetch) {\n      if (item.html_fetch.success && item.html_fetch.html_content) {\n        htmlContent = item.html_fetch.html_content;\n        source = \"html\";\n        extractionNotes = \"HTML obtenido exitosamente\";\n      } else if (item.html_fetch.error) {\n        extractionNotes = `HTTP ${item.html_fetch.status_code}: ${item.html_fetch.error} - usando snippet`;\n      } else {\n        extractionNotes = \"Ning\u00fan HTML disponible - usando snippet\";\n      }\n    } else {\n      extractionNotes = \"Sin html_fetch - usando snippet\";\n    }\n\n    // Usar HTML si lo tenemos, sino snippet\n    const textToSearch = htmlContent || metadata.snippet || \"\";\n\n    const genericNameTerms = new Set([\n      \"contacto\", \"inicio\", \"nombre empresa\", \"empresa\", \"servicios\", \"servicio\",\n      \"masajes\", \"masajes providencia\", \"masajes corporales\", \"home\", \"about\"\n    ]);\n\n    const toTitleCase = (value) => value\n      .split(\" \")\n      .filter(Boolean)\n      .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n      .join(\" \" );\n\n    const cleanCandidate = (value) => (value || \"\")\n      .replace(/\\s*[-|\u2022]\\s*.*/, \"\")\n      .replace(/[\\d()]/g, \"\")\n      .replace(/\\s+/g, \" \")\n      .trim();\n\n    const isGenericCandidate = (value) => {\n      const normalized = (value || \"\").toLowerCase().trim();\n      if (!normalized || normalized.length < 3) return true;\n      if (genericNameTerms.has(normalized)) return true;\n      if (/^(contacto|inicio|servicios?|home|masajes?)$/.test(normalized)) return true;\n      return false;\n    };\n\n    const domainToName = (domainValue) => toTitleCase(\n      (domainValue || \"\")\n        .replace(/^https?:\\/\\//i, \"\")\n        .replace(/^www\\./i, \"\")\n        .split(\"/\")[0]\n        .replace(/\\.(cl|com|net|org|io|app|co|es)$/i, \"\")\n        .replace(/[-_]+/g, \" \")\n        .trim()\n    );\n\n    const normalizeOrganization = (sourceValue, linkValue, titleValue) => {\n      let org = (sourceValue || \"\").replace(/^LinkedIn\\s*\u00b7\\s*/i, \"\").trim();\n\n      if (!org || org.includes(\".\")) {\n        try {\n          const host = new URL(linkValue || \"\").hostname;\n          if (host) {\n            org = domainToName(host);\n          }\n        } catch (_) {}\n      } else {\n        org = cleanCandidate(org);\n      }\n\n      if ((!org || isGenericCandidate(org)) && titleValue) {\n        const titleOrg = cleanCandidate(titleValue);\n        if (!isGenericCandidate(titleOrg)) {\n          org = toTitleCase(titleOrg);\n        }\n      }\n\n      return org && !isGenericCandidate(org) ? org : null;\n    };\n\n    // ==================== NOMBRE ====================\n    let name = cleanCandidate(metadata.title);\n    const personMatch = (metadata.title || \"\").match(/\\b([A-Z\u00c1\u00c9\u00cd\u00d3\u00da\u00d1][a-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1]+(?:\\s+[A-Z\u00c1\u00c9\u00cd\u00d3\u00da\u00d1][a-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1]+){1,3})\\b/);\n    if (personMatch) {\n      name = personMatch[1].trim();\n    }\n    if (isGenericCandidate(name)) {\n      name = null;\n    }\n\n    // ==================== CARGO ====================\n    let position = null;\n    const positionKeywords = [\n      \"chef\", \"cocinero\", \"pastelero\", \"panadero\", \"sommelier\",\n      \"profesor\", \"instructor\", \"tutor\", \"capacitador\", \"consultor\",\n      \"m\u00e9dico\", \"doctor\", \"ingeniero\", \"arquitecto\", \"abogado\", \"contador\",\n      \"electricista\", \"plomero\", \"carpintero\", \"pintor\", \"mec\u00e1nico\",\n      \"gerente\", \"director\", \"administrador\", \"coordinador\", \"supervisor\",\n      \"dise\u00f1ador\", \"desarrollador\", \"programador\", \"analista\", \"t\u00e9cnico\",\n    ];\n\n    for (const keyword of positionKeywords) {\n      const regex = new RegExp(`\\\\b${keyword}\\\\b`, \"i\");\n      if (regex.test(textToSearch)) {\n        position = keyword.charAt(0).toUpperCase() + keyword.slice(1);\n        break;\n      }\n    }\n\n    // ==================== EMAIL ====================\n    const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n    const emails = textToSearch.match(emailRegex) || [];\n    const email = emails.length > 0 ? emails[0] : null;\n\n    // ==================== TEL\u00c9FONO ====================\n    const phoneRegex = /(\\+?56|0)?[\\s]?9[\\s]?[\\d]{4}[\\s]?[\\d]{4}|(\\+?56|0)?[\\s]?2[\\s]?[\\d]{4}[\\s]?[\\d]{4}/g;\n    const phones = textToSearch.match(phoneRegex) || [];\n    let phone = phones.length > 0 ? phones[0] : null;\n\n    if (phone) {\n      phone = phone.replace(/[\\s\\-().]/g, \"\");\n      if (!phone.startsWith(\"+56\")) {\n        if (phone.startsWith(\"0\")) {\n          phone = \"+56\" + phone.substring(1);\n        } else if (!phone.startsWith(\"56\")) {\n          phone = \"+56\" + phone;\n        } else {\n          phone = \"+\" + phone;\n        }\n      }\n    }\n\n    // ==================== ORGANIZACI\u00d3N ====================\n    let organization = normalizeOrganization(metadata.source, metadata.link, metadata.title);\n\n    if (!name && organization) {\n      name = organization;\n    }\n\n    // ==================== VALIDACI\u00d3N ====================\n    // Para Superprof: requiere NOMBRE + (EMAIL O TEL\u00c9FONO)\n    // Si no hay email/tel\u00e9fono en snippet \u2192 is_valid = false\n    const isValid = !!(name && (email || phone));\n    const validationScore = isValid ? 1 : 0;\n\n    return {\n      json: {\n        ...item,\n        processed: true,\n        contact_data: {\n          name: name || \"Desconocido\",\n          email: email,\n          phone: phone,\n          position: position,\n          organization: organization,\n          region: item.region,\n          source_url: metadata.link,\n          source_type: metadata.source,\n          is_valid: isValid,\n          validation_score: validationScore,\n        },\n        extraction_info: {\n          source_used: source,\n          notes: extractionNotes,\n          http_status: item.html_fetch?.status_code || null,\n        },\n      },\n    };\n  });\n\n} catch (err) {\n  let errorMsg = \"Unknown error\";\n  if (typeof err === 'string') {\n    errorMsg = err;\n  } else if (err && typeof err === 'object' && 'message' in err) {\n    errorMsg = String(err.message);\n  } else {\n    errorMsg = String(err);\n  }\n  \n  return [{ json: { \n    contact_data: {\n      is_valid: false,\n      validation_score: 0,\n      error: errorMsg,\n    }\n  }}];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6928,
        8528
      ],
      "id": "03ee11a1-43c2-433e-88e8-d3fa8445c2d4",
      "name": "Code in JavaScript8"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "id": "2cd8af3f-f9dd-4dfb-971d-dea61bd52d7a",
              "leftValue": "={{ !!$json.contact_data }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        7824,
        8384
      ],
      "id": "9632a364-8bc5-433c-a727-959dd8f4ee66",
      "name": "Valid?"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        7152,
        8384
      ],
      "id": "b7e78d2c-ff2b-42e9-8f30-1716ce3742c7",
      "name": "Merge Branches"
    },
    {
      "parameters": {
        "jsCode": "const genericTerms = new Set(['contacto','inicio','empresa','servicio','servicios','home','about','nosotros','quienes somos','sitio oficial']);\nconst companyTerms = new Set(['spa','ltda','limitada','sa','s.a','eirl','sociedad','empresa','group','grupo','holding','consultora','consultores','clinica','cl\u00ednica']);\nconst connectorWords = new Set(['de','del','la','las','los','y']);\n\nconst clean = (value) => (value || '').toString().replace(/\\s+/g, ' ').replace(/\\s*[-|\u2022]\\s*.*/, '').trim();\nconst titleCase = (value) => clean(value).split(' ').filter(Boolean).map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');\nconst isGeneric = (value) => {\n  const v = clean(value).toLowerCase();\n  return !v || v.length < 3 || genericTerms.has(v) || /^(contacto|inicio|servicios?|home|masajes?)$/.test(v);\n};\nconst fromUrl = (rawUrl) => {\n  try {\n    const host = new URL(rawUrl || '').hostname.replace(/^www\\./i, '');\n    return titleCase(host.replace(/\\.(cl|com|net|org|io|app|co|es)$/i, '').replace(/[-_]+/g, ' '));\n  } catch (_) {\n    return null;\n  }\n};\nconst isLikelyPerson = (value) => {\n  const candidate = clean(value);\n  if (isGeneric(candidate) || /\\d/.test(candidate)) return false;\n  const words = candidate.split(/\\s+/).filter(Boolean);\n  const core = words.filter(w => !connectorWords.has(w.toLowerCase()));\n  if (core.length < 2 || core.length > 4) return false;\n  return core.every(w => /^[A-Z\u00c1\u00c9\u00cd\u00d3\u00da\u00d1][a-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1'\u2019-]+$/.test(w) && !companyTerms.has(w.toLowerCase().replace(/\\./g, '')));\n};\n\nreturn $input.all().map((itemWrapper) => {\n  const item = itemWrapper.json;\n  const contact = item.contact_data || {};\n  const title = clean(item.metadata?.title || item.title || '');\n  const source = clean(item.metadata?.source || contact.source_type || '');\n  const url = contact.source_url || item.metadata?.link || '';\n\n  let name = clean(contact.name);\n  let organization = clean(contact.organization);\n\n  if (!isLikelyPerson(name)) {\n    const match = title.match(/\\b([A-Z\u00c1\u00c9\u00cd\u00d3\u00da\u00d1][a-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1]+(?:\\s+[A-Z\u00c1\u00c9\u00cd\u00d3\u00da\u00d1][a-z\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1]+){1,3})\\b/);\n    const titlePerson = match ? clean(match[1]) : '';\n    name = isLikelyPerson(titlePerson) ? titlePerson : null;\n  }\n\n  if (!organization || isGeneric(organization) || isLikelyPerson(organization) || organization.includes('.')) {\n    if (source && !isGeneric(source) && !isLikelyPerson(source) && !source.includes('.')) {\n      organization = titleCase(source);\n    }\n  }\n\n  if (!organization || isGeneric(organization) || isLikelyPerson(organization)) {\n    const domainOrg = fromUrl(url);\n    if (domainOrg && !isGeneric(domainOrg) && !isLikelyPerson(domainOrg)) {\n      organization = domainOrg;\n    }\n  }\n\n  if (!name && organization) {\n    name = organization;\n  }\n\n  let score = Number(contact.validation_score);\n  const isValid = !!contact.is_valid;\n  if (!Number.isFinite(score)) score = isValid ? 1 : 0.3;\n  if (!isValid && score < 0.3) score = 0.3;\n\n  contact.name = name || contact.name || organization || 'Desconocido';\n  contact.organization = organization || null;\n  contact.is_valid = isValid;\n  contact.validation_score = score;\n\n  item.contact_data = contact;\n  return { json: item };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        7376,
        8384
      ],
      "id": "4de8e1e3-4938-4461-a02e-6d535a9cbd94",
      "name": "Contact Final Validation"
    },
    {
      "parameters": {
        "jsCode": "const regionRules = [\n  { region: 'Arica y Parinacota', keys: ['arica y parinacota','arica','parinacota'] },\n  { region: 'Tarapac\u00e1', keys: ['tarapac\u00e1','tarapaca','iquique'] },\n  { region: 'Antofagasta', keys: ['antofagasta'] },\n  { region: 'Atacama', keys: ['atacama','copiap\u00f3','copiapo'] },\n  { region: 'Coquimbo', keys: ['coquimbo','la serena'] },\n  { region: 'Valpara\u00edso', keys: ['valpara\u00edso','valparaiso','vi\u00f1a del mar','vina del mar','quilpu\u00e9','quilpue','conc\u00f3n','concon'] },\n  { region: 'Santiago', keys: ['santiago','metropolitana','rm','providencia','las condes','\u00f1u\u00f1oa','nunoa'] },\n  { region: \"O'Higgins\", keys: [\"o'higgins\",'ohiggins','rancagua'] },\n  { region: 'Maule', keys: ['maule','talca'] },\n  { region: '\u00d1uble', keys: ['\u00f1uble','nuble','chill\u00e1n','chillan'] },\n  { region: 'Biob\u00edo', keys: ['biob\u00edo','biobio','b\u00edo b\u00edo','bio bio','concepci\u00f3n','concepcion'] },\n  { region: 'La Araucan\u00eda', keys: ['la araucan\u00eda','la araucania','araucan\u00eda','araucania','temuco'] },\n  { region: 'Los R\u00edos', keys: ['los r\u00edos','los rios','valdivia'] },\n  { region: 'Los Lagos', keys: ['los lagos','puerto montt'] },\n  { region: 'Ays\u00e9n', keys: ['ays\u00e9n','aysen','coihaique','coyhaique'] },\n  { region: 'Magallanes', keys: ['magallanes','punta arenas'] }\n];\n\nreturn $input.all().map((itemWrapper) => {\n  const item = itemWrapper.json;\n  const contact = item.contact_data || {};\n  const text = [\n    contact.source_url || '',\n    contact.organization || '',\n    item.metadata?.title || '',\n    item.metadata?.snippet || '',\n    item.snippet || ''\n  ].join(' ').toLowerCase();\n\n  let inferred = null;\n  for (const rule of regionRules) {\n    if (rule.keys.some(key => text.includes(key))) {\n      inferred = rule.region;\n      break;\n    }\n  }\n\n  if (inferred) {\n    contact.region = inferred;\n    item.region = inferred;\n  }\n\n  item.contact_data = contact;\n  return { json: item };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        7600,
        8384
      ],
      "id": "e0052a9f-9eda-4a5f-a215-1d4e4da592f1",
      "name": "Region Correction"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=http://backend:8081/api/v1/searches/{{ Number($(\"Webhook\").first().json.body.search_id) }}/callback",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-N8N-API-KEY",
              "value": "=Unab.2026"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "search_id",
              "value": "={{$json.search_id}}"
            },
            {
              "name": "status",
              "value": "={{$json.status}}"
            },
            {
              "name": "results_count",
              "value": "={{$json.results_count}}"
            },
            {
              "name": "duplicates_count",
              "value": "={{$json.duplicates_count}}"
            },
            {
              "name": "execution_time_ms",
              "value": "={{$json.execution_time_ms}}"
            },
            {
              "name": "avg_validation_score",
              "value": "={{$json.avg_validation_score}}"
            },
            {
              "name": "stats",
              "value": "={{$json.stats}}"
            },
            {
              "name": "failed_searches",
              "value": "={{$json.failed_searches}}"
            },
            {
              "name": "high_quality_contacts",
              "value": "={{$json.high_quality_contacts}}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        8496,
        8384
      ],
      "id": "f8e58d8f-3c14-4d37-aa5d-8718e8a78604",
      "name": "Notify Backend"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SET @sid = CAST('{{ \n  $items(\"Webhook\")[0].json.search_id \n  || $items(\"Webhook\")[0].json.body?.search_id \n  || 0 \n}}' AS UNSIGNED);\n\nINSERT INTO contacts\n(name, email, phone, position, organization, region, source_url, source_type, research_lines, is_valid, validation_score)\nVALUES\n(\n  '{{$json.contact_data.name.replace(/'/g, \"''\")}}',\n  {{$json.contact_data.email ? `'${$json.contact_data.email.replace(/'/g, \"''\")}'` : 'NULL'}},\n  {{$json.contact_data.phone ? `'${$json.contact_data.phone.replace(/'/g, \"''\")}'` : 'NULL'}},\n  {{$json.contact_data.position ? `'${$json.contact_data.position.replace(/'/g, \"''\")}'` : 'NULL'}},\n  {{$json.contact_data.organization ? `'${$json.contact_data.organization.replace(/'/g, \"''\")}'` : 'NULL'}},\n  {{$json.contact_data.region ? `'${$json.contact_data.region.replace(/'/g, \"''\")}'` : 'NULL'}},\n  '{{$json.contact_data.source_url.replace(/'/g, \"''\")}}',\n  {{$json.contact_data.source_type ? `'${$json.contact_data.source_type.replace(/'/g, \"''\")}'` : 'NULL'}},\n  NULL,\n  1,\n  {{ Number($json.contact_data.validation_score || 1) }}\n)\nON DUPLICATE KEY UPDATE\n  id = LAST_INSERT_ID(id),\n  updated_at = CURRENT_TIMESTAMP;\n\nSET @cid = LAST_INSERT_ID();\n\nINSERT INTO search_results (search_id, contact_id, relevance_score)\nSELECT\n  @sid,\n  @cid,\n  COALESCE((SELECT validation_score FROM contacts WHERE id = @cid), 1)\nWHERE @sid > 0\nON DUPLICATE KEY UPDATE\n  relevance_score = VALUES(relevance_score);\n\nSELECT @sid AS search_id, @cid AS contact_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.5,
      "position": [
        8048,
        8384
      ],
      "id": "fe5726ea-8a31-4e10-979a-606d1e84235c",
      "name": "Insert Valid Contacts",
      "credentials": {
        "mySql": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const webhookData = $(\"Webhook\").first().json;\nconst inserted = $(\"Insert Valid Contacts\").all().length;\n\n// En Webhook de n8n normalmente viene en body\nconst rawSearchId = webhookData?.body?.search_id ?? webhookData?.search_id;\n\nconst searchId = Number(rawSearchId);\nif (!Number.isInteger(searchId) || searchId <= 0) {\n  throw new Error(`search_id inv\u00e1lido. Valor recibido: ${JSON.stringify(rawSearchId)}`);\n}\n\nreturn [{\n  json: {\n    search_id: searchId,\n    status: \"completed\",\n    results_count: inserted,\n    duplicates_count: 0,\n    execution_time_ms: 0,\n    avg_validation_score: 1,\n    stats: {},\n    failed_searches: 0,\n    high_quality_contacts: inserted\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        8272,
        8384
      ],
      "id": "061530b4-cf1d-4db0-a195-43285ad7f416",
      "name": "Build Callback"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "id": "f44c22a0-7f19-420e-a27b-3ebf11cc17e3",
              "leftValue": "={{ $json.classification.should_process }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        5808,
        8432
      ],
      "id": "1b15e3f3-014e-4aae-a4cd-1a76bb9770ae",
      "name": "Data Valid?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "id": "523301ab-f2f3-4d0b-9cdc-0528dd2b1d6d",
              "leftValue": "={{ $json.classification.is_linkedin }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        6032,
        8432
      ],
      "id": "ac59c7a5-7502-400c-a0d1-3c115e270b07",
      "name": "Linkedin?"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Data Structure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Search Query": {
      "main": [
        [
          {
            "node": "Google Custom Search API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Custom Search API": {
      "main": [
        [
          {
            "node": "Parser1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Data is Valid": {
      "main": [
        [
          {
            "node": "Build Search Query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Structure": {
      "main": [
        [
          {
            "node": "IF Data is Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parser1": {
      "main": [
        [
          {
            "node": "Link Classifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Link Classifier": {
      "main": [
        [
          {
            "node": "Data Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request3": {
      "main": [
        [
          {
            "node": "Prepare Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare Data": {
      "main": [
        [
          {
            "node": "Combine Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Data": {
      "main": [
        [
          {
            "node": "Code in JavaScript8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript8": {
      "main": [
        [
          {
            "node": "Merge Branches",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code in JavaScript6": {
      "main": [
        [
          {
            "node": "Merge Branches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid?": {
      "main": [
        [
          {
            "node": "Insert Valid Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Branches": {
      "main": [
        [
          {
            "node": "Contact Final Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contact Final Validation": {
      "main": [
        [
          {
            "node": "Region Correction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Region Correction": {
      "main": [
        [
          {
            "node": "Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Valid Contacts": {
      "main": [
        [
          {
            "node": "Build Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Callback": {
      "main": [
        [
          {
            "node": "Notify Backend",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Valid?": {
      "main": [
        [
          {
            "node": "Linkedin?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Linkedin?": {
      "main": [
        [
          {
            "node": "Code in JavaScript6",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "HTTP Request3",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false
  },
  "versionId": "56043686-1c91-4f2c-bfe6-71dfe9240ca5",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "Mng5-VRYT5d6W3iWV0o6H",
  "tags": []
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

N8Nworkflow. Uses stopAndError, httpRequest, mySql. Webhook trigger; 22 nodes.

Source: https://github.com/Joaquinamz/ExpertWebScraping/blob/01151a2081bd9f50e4aea049a95461d3fc8938d4/n8n-workflows/n8nworkflow.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

BP_check. Uses googleSheets, @n-octo-n/n8n-nodes-json-database, httpRequest, itemLists. Webhook trigger; 99 nodes.

Google Sheets, @N Octo N/N8N Nodes Json Database, HTTP Request +2
Data & Sheets

v25.1.3. Uses httpRequest, mySql, n8n-nodes-zohozeptomail. Webhook trigger; 98 nodes.

HTTP Request, MySQL, N8N Nodes Zohozeptomail
Data & Sheets

This solution enables you to manage all your Notion and Todoist tasks from different workspaces as well as your calendar events in a single place. This is 2 way sync with partial support for recurring

Redis, Notion, Todoist +6
Data & Sheets

Notion to Clockify Sync Template. Uses scheduleTrigger, clockify, compareDatasets, stopAndError. Webhook trigger; 68 nodes.

Clockify, Stop And Error, Notion +1
Data & Sheets

This workflow synchronizes three entities from Notion to Clockify, allowing tracked time to be linked to client-related projects or tasks.

Clockify, Stop And Error, Notion +1