AutomationFlowsData & Sheets › Hunter

Hunter

02_HUNTER. Uses httpRequest, airtable. Webhook trigger; 24 nodes.

Webhook trigger★★★★☆ complexity24 nodesHTTP RequestAirtable
Data & Sheets Trigger: Webhook Nodes: 24 Complexity: ★★★★☆ Added:

This workflow follows the Airtable → HTTP Request 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
{
  "updatedAt": "2025-12-25T09:49:10.572Z",
  "createdAt": "2025-12-24T16:11:25.919Z",
  "id": "1y6kw0xFymo0y0wu",
  "name": "02_HUNTER",
  "active": false,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "part1",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        672,
        64
      ],
      "id": "d1572427-4dd4-4c64-869b-bc4c862d9b23",
      "name": "Webhook"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        656,
        480
      ],
      "id": "6cd52e52-cb8a-4915-b8a0-a73508e4a591",
      "name": "When clicking \u2018Execute workflow\u2019"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"term\": \"Dentists in Miami\",\n  \"user_id\": \"33333333-3333-3333-3333-333333333333\",\n  \"batch_id\": \"22222222-2222-2222-2222-222222222222\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        896,
        272
      ],
      "id": "049d6eb3-8ba2-4f25-baa5-b9fef2793dee",
      "name": "Map Webhook Data1"
    },
    {
      "parameters": {
        "jsCode": "// Get input from Webhook or the previous node\nconst input = $input.first().json;\n\n// Check for required technical IDs\nif (!input.user_id || !input.batch_id) {\n  throw new Error('Missing required fields: user_id and batch_id');\n}\n\n// Extract term and location logic\nlet rawSearch = input.term || \"\"; \nlet term = rawSearch;\nlet location = input.location || \"\";\n\n// Split logic if phrase contains \"in\" or \"at\"\nconst splitMatch = rawSearch.match(/(.+)\\s+(?:in|at)\\s+(.+)/i);\n\nif (splitMatch) {\n    term = splitMatch[1].trim();\n    location = splitMatch[2].trim();\n}\n\nreturn [{\n  json: {\n    term: term,\n    location: location,\n    search_query: `${term} ${location}`.trim(),\n    batch_id: input.batch_id,\n    user_id: input.user_id,\n    started_at: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        272
      ],
      "id": "ab6aefa5-ac91-4868-a652-b38ab6c281af",
      "name": "Input Validation1"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/nwua9Gu5YrADL7ZDj/runs",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_HEADER_VALUE"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"searchStringsArray\": [\n    \"{{ $json.search_query }}\"\n  ],\n  \"maxCrawledPlacesPerSearch\": 5,\n  \"includeWebsites\": true,\n  \"language\": \"en\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1312,
        272
      ],
      "id": "ee87c166-373d-4815-93bd-55c9d556754c",
      "name": "Start Apify Run1"
    },
    {
      "parameters": {
        "jsCode": "const runId = $input.first().json.data.id;\nconst originalInput = $('Input Validation1').item.json;\nconst apifyToken = 'REDACTED_BY_REGEX'; \nconst maxRetries = 20; \nconst pollInterval = 5000; \n\nfor (let i = 0; i < maxRetries; i++) {\n  const response = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `https://api.apify.com/v2/actor-runs/${runId}?token=${apifyToken}`\n  });\n\n  const status = response.data.status;\n\n  if (status === 'SUCCEEDED') {\n    return [{\n      json: {\n        run_id: runId,\n        dataset_id: response.data.defaultDatasetId,\n        status: 'completed',\n        ...originalInput\n      }\n    }];\n  }\n\n  if (status === 'FAILED' || status === 'ABORTED' || status === 'TIMED-OUT') {\n    throw new Error(`Apify Run failed with status: ${status}`);\n  }\n\n  await new Promise(resolve => setTimeout(resolve, pollInterval));\n}\n\nthrow new Error('Apify timed out.');"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        272
      ],
      "id": "c2aba354-ff9c-4469-bd66-d470fc1ce486",
      "name": "Poll Status1"
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.dataset_id }}/items",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_HEADER_VALUE"
            },
            {
              "name": "clean",
              "value": "true"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1712,
        272
      ],
      "id": "54373c3f-afcb-46fd-868e-b1ba30beab98",
      "name": "Fetch Results1"
    },
    {
      "parameters": {
        "jsCode": "const inputData = $input.all();\nconst originalInput = $node[\"Input Validation1\"].json;\nconst places = inputData.map(item => item.json);\nconst leads = [];\n\nfor (const place of places) {\n  // Extract Domain\n  let domain = null;\n  if (place.website) {\n    try {\n      const url = new URL(place.website.startsWith('http') ? place.website : 'https://' + place.website);\n      domain = url.hostname.replace(/^www\\./, '');\n    } catch (e) {\n      domain = null; \n    }\n  }\n  \n  // Clean Phone\n  let cleanPhone = place.phoneUnformatted || place.phone || null;\n\n  leads.push({\n    json: {\n        user_id: originalInput.user_id,\n        batch_id: originalInput.batch_id,\n        name: place.title || 'Unknown',\n        domain: domain,\n        phone: cleanPhone,\n        address: place.address || null,\n        \n        // ADDED THESE TWO LINES\n        rating: place.totalScore || 0,        // Maps from Apify totalScore\n        reviews_count: place.reviewsCount || 0, // Maps from Apify reviewsCount\n        \n        category: place.categoryName || 'Dentist',\n        status: 'raw',\n        created_at: new Date().toISOString()\n    }\n  });\n}\n\nreturn leads;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        272
      ],
      "id": "861e8cee-4d82-465c-9916-307624451be6",
      "name": "Transform & Validate Leads"
    },
    {
      "parameters": {
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appOiHgTrwpTaLJYN",
          "mode": "list",
          "cachedResultName": "Untitled Base",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN"
        },
        "table": {
          "__rl": true,
          "value": "tblQwFiY5yufTsiJh",
          "mode": "list",
          "cachedResultName": "Leads",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN/tblQwFiY5yufTsiJh"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "name": "={{ $json.name }}",
            "batch_id": "={{ $json.batch_id }}",
            "domain": "={{ $json.domain }}",
            "phone": "={{ $json.phone }}",
            "address": "={{ $json.address }}",
            "category": "={{ $json.category }}",
            "status": "={{ $json.status }}",
            "rating": "={{ $json.rating }}",
            "reviews_count": "={{ $json.reviews_count }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "batch_id",
              "displayName": "batch_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "domain",
              "displayName": "domain",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "address",
              "displayName": "address",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "rating",
              "displayName": "rating",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "reviews_count",
              "displayName": "reviews_count",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2144,
        272
      ],
      "id": "ae7d1f82-0434-4724-9908-8035688f3ed9",
      "name": "Create a record",
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 1. Get all leads processed in this run\nconst insertedLeads = $input.all();\n\n// 2. FIXED: Reference the correct node name 'Input Validation1'\nconst originalInput = $node[\"Input Validation1\"].json;\n\n// 3. Create the summary object for Airtable update\nconst summary = {\n  batch_id: originalInput.batch_id,\n  user_id: originalInput.user_id,\n  total_leads: insertedLeads.length,\n  started_at: originalInput.started_at,\n  completed_at: new Date().toISOString(),\n  search_query: originalInput.search_query,\n  status: 'completed'\n};\n\nreturn [{ json: summary }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2352,
        272
      ],
      "id": "3acb703e-87e0-4dfb-a5f7-8c118d289ec8",
      "name": "Code in JavaScript1"
    },
    {
      "parameters": {
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appOiHgTrwpTaLJYN",
          "mode": "list",
          "cachedResultName": "Untitled Base",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN"
        },
        "table": {
          "__rl": true,
          "value": "tbljbw1L2Z2zpD02x",
          "mode": "list",
          "cachedResultName": "Batches",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN/tbljbw1L2Z2zpD02x"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "batch_id": "={{ $json.batch_id }}",
            "user_id": "={{ $json.user_id }}",
            "search_query": "={{ $json.search_query }}",
            "started_at": "={{ $json.started_at }}",
            "total_leads": "={{ $json.total_leads }}",
            "status": "={{ $json.status }}",
            "id": "={{ $json.user_id }}",
            "Leads": "{{ $node[\"Create a record\"].json.id }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "readOnly": true,
              "removed": false
            },
            {
              "id": "batch_id",
              "displayName": "batch_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "user_id",
              "displayName": "user_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "search_query",
              "displayName": "search_query",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "total_leads",
              "displayName": "total_leads",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "started_at",
              "displayName": "started_at",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "dateTime",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "Leads",
              "displayName": "Leads",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2560,
        272
      ],
      "id": "add72797-fa7e-4b2d-9367-01b95cdb00c0",
      "name": "Create or update a record",
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 1. Access the data inside the 'fields' object\nconst item = $input.first().json;\nconst data = item.fields || {}; \n\n// 2. Calculate duration using the correct fields\n// Note: If completed_at isn't in 'fields', we use the current time\nconst completedAt = data.completed_at || new Date().toISOString();\nconst startedAt = data.started_at;\n\nconst duration = new Date(completedAt) - new Date(startedAt);\nconst durationSeconds = Math.round(duration / 1000) || 0;\n\n// 3. Return the corrected summary\nreturn [{\n  json: {\n    workflow: '02_HUNTER',\n    status: 'success',\n    batch_id: data.batch_id,\n    total_leads: data.total_leads || 0, // FIXED: Now pulls from fields\n    search_query: data.search_query,\n    duration_seconds: durationSeconds,\n    message: `Successfully found ${data.total_leads || 0} leads in ${durationSeconds} seconds`\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2800,
        272
      ],
      "id": "eb2b8e59-e1a6-4832-a457-72b2cefcf341",
      "name": "Success Message1"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "bb37c2f9-6c87-471e-871c-c1c7a0f24b2c",
              "name": "batch_id",
              "value": "={{ $json.body.batch_id }}",
              "type": "string"
            },
            {
              "id": "1fce52f9-5a3b-41f1-a4b8-388e97f8c331",
              "name": "user_id",
              "value": "={{ $json.body.user_id }}",
              "type": "string"
            },
            {
              "id": "cfd877ea-34fe-43a4-9cd2-e79ca2ea7b85",
              "name": "industry",
              "value": "={{ $json.body.industry }}",
              "type": "string"
            },
            {
              "id": "ff9f7a37-3078-4d81-839d-0b8cd15f01b9",
              "name": "location",
              "value": "={{ $json.body.location }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        896,
        688
      ],
      "id": "9649e381-b6b9-4fd9-a051-c7664ccb6e81",
      "name": "Edit Fields1"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"batch_id\": \"a9f9e451-edeb-4d3e-9f5b-4d8535a8bf76\",\n  \"user_id\": \"f1229eb0-7283-4f13-a917-fe1d41e516fd\",\n  \"industry\": \"Restaurants\",\n  \"location\": \"Casablanca\",\n  \"status\": \"raw\",\n  \"started_at\": \"{{ new Date().toISOString() }}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        896,
        480
      ],
      "id": "63f7853d-61e0-4adb-8ebe-5a66074e4fad",
      "name": "TEST JSON"
    },
    {
      "parameters": {
        "jsCode": "// 1. Get the data from the Webhook 'body'\nconst input = $input.first().json.body || $input.first().json;\n\n// 2. Check for required technical IDs\nif (!input.user_id || !input.batch_id) {\n  throw new Error('Missing required fields: user_id and batch_id');\n}\n\n// 3. Extract industry and location from the new input structure\n// Industry replaces 'term' in your new schema\nlet industry = input.industry || \"\"; \nlet location = input.location || \"\";\n\n// 4. Return the formatted data for the next nodes\nreturn [{\n  json: {\n    term: industry,\n    location: location,\n    // Creates the string for Google Maps search, e.g., \"Restaurants Casablanca\"\n    search_query: `${industry} ${location}`.trim(), \n    batch_id: input.batch_id,\n    user_id: input.user_id,\n    started_at: new Date().toISOString(),\n    status: 'raw' // Helpful for your Airtable status column\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        480
      ],
      "id": "712803fc-0a8c-4b6e-b429-51a2d7ccb6ea",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "jsCode": "// 1. Get the data from the Webhook 'body'\nconst input = $input.first().json.body || $input.first().json;\n\n// 2. Check for required technical IDs\nif (!input.user_id || !input.batch_id) {\n  throw new Error('Missing required fields: user_id and batch_id');\n}\n\n// 3. Extract industry and location from the new input structure\n// Industry replaces 'term' in your new schema\nlet industry = input.industry || \"\"; \nlet location = input.location || \"\";\n\n// 4. Return the formatted data for the next nodes\nreturn [{\n  json: {\n    term: industry,\n    location: location,\n    // Creates the string for Google Maps search, e.g., \"Restaurants Casablanca\"\n    search_query: `${industry} ${location}`.trim(), \n    batch_id: input.batch_id,\n    user_id: input.user_id,\n    started_at: new Date().toISOString(),\n    status: 'raw' // Helpful for your Airtable status column\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        688
      ],
      "id": "b26c6616-acdf-448c-8f2d-b8d0ea668cf6",
      "name": "Code in JavaScript2"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/nwua9Gu5YrADL7ZDj/runs",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_HEADER_VALUE"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"searchStringsArray\": [\n    \"{{ $json.search_query }}\"\n  ],\n  \"includeWebsites\": true,\n  \"language\": \"en\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1344,
        480
      ],
      "id": "2132a935-0f30-48c5-b64b-f063c01fed2d",
      "name": "Start Apify Run"
    },
    {
      "parameters": {
        "jsCode": "// 1. Get the runId from the Apify \"Start Run\" node output\nconst inputJson = $input.first().json;\nconst runId = inputJson.data ? inputJson.data.id : inputJson.id;\n\n// 2. FIXED: Reference the Validation node with a fallback\nlet originalInput;\ntry {\n  originalInput = $node[\"Input Validation1\"].json;\n} catch (e) {\n  // Use fallback data for manual testing purposes\n  originalInput = {\n    batch_id: \"testing-fallback-id\",\n    user_id: \"testing-user-id\",\n    industry: \"Restaurants\",\n    location: \"Casablanca\"\n  };\n}\n\nconst apifyToken = 'REDACTED_BY_REGEX'; \nconst maxRetries = 20; \nconst pollInterval = 5000; \n\nfor (let i = 0; i < maxRetries; i++) {\n  const response = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `https://api.apify.com/v2/actor-runs/${runId}?token=${apifyToken}`\n  });\n\n  const status = response.data.status;\n\n  if (status === 'SUCCEEDED') {\n    return [{\n      json: {\n        run_id: runId,\n        dataset_id: response.data.defaultDatasetId,\n        status: 'completed',\n        ...originalInput // Spreads the industry and location\n      }\n    }];\n  }\n\n  if (status === 'FAILED' || status === 'ABORTED' || status === 'TIMED-OUT') {\n    throw new Error(`Apify Run failed with status: ${status}`);\n  }\n\n  await new Promise(resolve => setTimeout(resolve, pollInterval));\n}\n\nthrow new Error('Apify timed out.');"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1552,
        480
      ],
      "id": "a42f8278-1036-4c44-94fe-1b4f1a5603d0",
      "name": "Poll Status"
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.dataset_id }}/items",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_HEADER_VALUE"
            },
            {
              "name": "clean",
              "value": "true"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1744,
        480
      ],
      "id": "b6a11b8a-1d86-4c61-bf93-a4cef11985e1",
      "name": "Fetch Results"
    },
    {
      "parameters": {
        "jsCode": "// 1. Get all raw results from the Apify Dataset node\nconst inputData = $input.all();\n\n// 2. FIXED: Reference the Validation node with a fallback to prevent \"Not Executed\" errors\nlet originalInput;\ntry {\n  originalInput = $node[\"Input Validation1\"].json;\n} catch (e) {\n  // Use fallback data for testing if the previous node hasn't run\n  originalInput = {\n    user_id: \"test-user\",\n    batch_id: \"test-batch\",\n    industry: \"Restaurants\" \n  };\n}\n\nconst places = inputData.map(item => item.json);\nconst leads = [];\n\nfor (const place of places) {\n  // Extract Domain\n  let domain = null;\n  if (place.website) {\n    try {\n      const url = new URL(place.website.startsWith('http') ? place.website : 'https://' + place.website);\n      domain = url.hostname.replace(/^www\\./, '');\n    } catch (e) {\n      domain = null; \n    }\n  }\n  \n  // Clean Phone\n  let cleanPhone = place.phoneUnformatted || place.phone || null;\n\n  leads.push({\n    json: {\n        user_id: originalInput.user_id,\n        batch_id: originalInput.batch_id,\n        name: place.title || 'Unknown',\n        domain: domain,\n        phone: cleanPhone,\n        address: place.address || null,\n        \n        // Performance Metrics\n        rating: place.totalScore || 0,\n        reviews_count: place.reviewsCount || 0,\n        \n        // Updated to use the industry from your new webhook\n        category: originalInput.industry || place.categoryName || 'General',\n        \n        status: 'raw',\n        created_at: new Date().toISOString()\n    }\n  });\n}\n\nreturn leads;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1968,
        480
      ],
      "id": "a3608bcf-989b-4b88-a6b3-1436281aa12e",
      "name": "Transform & Validate Leads1"
    },
    {
      "parameters": {
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appOiHgTrwpTaLJYN",
          "mode": "list",
          "cachedResultName": "Untitled Base",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN"
        },
        "table": {
          "__rl": true,
          "value": "tblQwFiY5yufTsiJh",
          "mode": "list",
          "cachedResultName": "Leads",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN/tblQwFiY5yufTsiJh"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "name": "={{ $json.name }}",
            "batch_id": "={{ $json.batch_id }}",
            "domain": "={{ $json.domain }}",
            "phone": "={{ $json.phone }}",
            "address": "={{ $json.address }}",
            "category": "={{ $json.category }}",
            "status": "={{ $json.status }}",
            "rating": "={{ $json.rating }}",
            "reviews_count": "={{ $json.reviews_count }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "batch_id",
              "displayName": "batch_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "domain",
              "displayName": "domain",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "address",
              "displayName": "address",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "rating",
              "displayName": "rating",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "reviews_count",
              "displayName": "reviews_count",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2176,
        480
      ],
      "id": "9dabbe42-9a5f-47c3-a5b6-b9d62679608c",
      "name": "Create a record1",
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 1. Get all leads processed in this run\nconst insertedLeads = $input.all();\n\n// 2. FIXED: Reference the correct node name with a fallback\nlet originalInput;\ntry {\n  originalInput = $node[\"Input Validation1\"].json;\n} catch (e) {\n  // Fallback for testing individual nodes\n  originalInput = {\n    batch_id: \"test-batch\",\n    user_id: \"test-user\",\n    started_at: new Date().toISOString(),\n    search_query: \"Restaurants Casablanca\"\n  };\n}\n\n// 3. Create the summary object for Airtable update\nconst summary = {\n  batch_id: originalInput.batch_id,\n  user_id: originalInput.user_id,\n  total_leads: insertedLeads.length, // Counts the found restaurants\n  started_at: originalInput.started_at,\n  completed_at: new Date().toISOString(),\n  search_query: originalInput.search_query,\n  status: 'completed'\n};\n\n// 4. Return as a single item for the final Airtable Update node\nreturn [{ json: summary }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2384,
        480
      ],
      "id": "9d7ba053-a0b3-449f-8210-28c8beeafcf5",
      "name": "Code in JavaScript3"
    },
    {
      "parameters": {
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appOiHgTrwpTaLJYN",
          "mode": "list",
          "cachedResultName": "Untitled Base",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN"
        },
        "table": {
          "__rl": true,
          "value": "tbljbw1L2Z2zpD02x",
          "mode": "list",
          "cachedResultName": "Batches",
          "cachedResultUrl": "https://airtable.com/appOiHgTrwpTaLJYN/tbljbw1L2Z2zpD02x"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "batch_id": "={{ $json.batch_id }}",
            "user_id": "={{ $json.user_id }}",
            "search_query": "={{ $json.search_query }}",
            "started_at": "={{ $json.started_at }}",
            "total_leads": "={{ $json.total_leads }}",
            "status": "={{ $json.status }}",
            "id": "={{ $json.user_id }}",
            "Leads": "{{ $node[\"Create a record\"].json.id }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "readOnly": true,
              "removed": false
            },
            {
              "id": "batch_id",
              "displayName": "batch_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "user_id",
              "displayName": "user_id",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "search_query",
              "displayName": "search_query",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "total_leads",
              "displayName": "total_leads",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "started_at",
              "displayName": "started_at",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "dateTime",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "Leads",
              "displayName": "Leads",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2592,
        480
      ],
      "id": "8a90db5c-9e5a-45ca-b915-e41749c30d80",
      "name": "Create or update a record1",
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// 1. Access the data inside the 'fields' object\nconst item = $input.first().json;\nconst data = item.fields || {}; \n\n// 2. Calculate duration using the correct fields\n// Note: If completed_at isn't in 'fields', we use the current time\nconst completedAt = data.completed_at || new Date().toISOString();\nconst startedAt = data.started_at;\n\nconst duration = new Date(completedAt) - new Date(startedAt);\nconst durationSeconds = Math.round(duration / 1000) || 0;\n\n// 3. Return the corrected summary\nreturn [{\n  json: {\n    workflow: '02_HUNTER',\n    status: 'success',\n    batch_id: data.batch_id,\n    total_leads: data.total_leads || 0, // FIXED: Now pulls from fields\n    search_query: data.search_query,\n    duration_seconds: durationSeconds,\n    message: `Successfully found ${data.total_leads || 0} leads in ${durationSeconds} seconds`\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2832,
        480
      ],
      "id": "f86945c7-89c6-4715-b516-995286cff7f3",
      "name": "Success Message"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Map Webhook Data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Map Webhook Data1",
            "type": "main",
            "index": 0
          },
          {
            "node": "TEST JSON",
            "type": "main",
            "index": 0
          },
          {
            "node": "Edit Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Webhook Data1": {
      "main": [
        [
          {
            "node": "Input Validation1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Validation1": {
      "main": [
        [
          {
            "node": "Start Apify Run1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify Run1": {
      "main": [
        [
          {
            "node": "Poll Status1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Status1": {
      "main": [
        [
          {
            "node": "Fetch Results1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Results1": {
      "main": [
        [
          {
            "node": "Transform & Validate Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform & Validate Leads": {
      "main": [
        [
          {
            "node": "Create a record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a record": {
      "main": [
        [
          {
            "node": "Code in JavaScript1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "node": "Create or update a record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or update a record": {
      "main": [
        [
          {
            "node": "Success Message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TEST JSON": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields1": {
      "main": [
        [
          {
            "node": "Code in JavaScript2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify Run": {
      "main": [
        [
          {
            "node": "Poll Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Status": {
      "main": [
        [
          {
            "node": "Fetch Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Results": {
      "main": [
        [
          {
            "node": "Transform & Validate Leads1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform & Validate Leads1": {
      "main": [
        [
          {
            "node": "Create a record1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a record1": {
      "main": [
        [
          {
            "node": "Code in JavaScript3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "node": "Create or update a record1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or update a record1": {
      "main": [
        [
          {
            "node": "Success Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Start Apify Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "c5272d17-4257-4717-9a66-38141f2cc23e",
  "activeVersionId": null,
  "triggerCount": 0,
  "shared": [
    {
      "updatedAt": "2025-12-24T16:11:25.942Z",
      "createdAt": "2025-12-24T16:11:25.942Z",
      "role": "workflow:owner",
      "workflowId": "1y6kw0xFymo0y0wu",
      "projectId": "HHopAZ4lOFgjhBzT"
    }
  ],
  "activeVersion": null,
  "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

02_HUNTER. Uses httpRequest, airtable. Webhook trigger; 24 nodes.

Source: https://github.com/abde0112/n8n_bkv2/blob/main/02_hunter-1y6kw0xFymo0y0wu.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

This premium n8n workflow harnesses the power of DataForSEO's API combined with Airtable's relational database capabilities to transform your keyword research process, providing deeper insights for co

HTTP Request, Airtable
Data & Sheets

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Airtable, HTTP Request, Google Drive +1
Data & Sheets

This workflow automates the entire lifecycle of a service-based client, combining four distinct business flows into a single view: Intake Leads: Receives a webhook from your form builder, validates th

Airtable, Notion, Google Calendar +3
Data & Sheets

It intelligently syncs confirmed sales orders from your Airtable base to QuickBooks, automatically creating new customers if they don't exist before generating a perfectly matched invoice. It then log

Airtable, QuickBooks, HTTP Request
Data & Sheets

Who is this for? Business who manually prep/route DocuSign envelopes and want zero-touch contract signing from form submission.

Airtable, HTTP Request