AutomationFlowsWeb Scraping › Tracerfy Results Handler

Tracerfy Results Handler

Tracerfy Results Handler. Uses httpRequest. Webhook trigger; 35 nodes.

Webhook trigger★★★★★ complexity35 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 35 Complexity: ★★★★★ Added:

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": "Tracerfy Results Handler",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "tracerfy-results",
        "options": {
          "rawBody": false
        }
      },
      "id": "wf2-webhook-trigger",
      "name": "Tracerfy Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        128,
        -208
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Tracerfy Webhook').item.json.body.download_url || $('Tracerfy Webhook').item.json.download_url }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "wf2-download-from-url",
      "name": "Download Results from URL",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        864,
        -16
      ]
    },
    {
      "parameters": {
        "jsCode": "// Process Tracerfy skip trace results - BatchData ONLY\n// STRICT MOBILE-ONLY FILTER: Only import leads with confirmed mobile numbers\n// PHONELESS LEADS: Store landline/unknown/no-contact for future enrichment\nconst webhookData = $('Tracerfy Webhook').item.json.body || $('Tracerfy Webhook').item.json;\nconst jobDataFromConvex = $('Get Job from Convex').item.json;\nconst rawInput = $input.item.json;\n\nconst tracerfyResults = rawInput.data || rawInput;\nconst jobData = jobDataFromConvex.job || jobDataFromConvex || {};\nconst leadData = jobData.leadData || [];\nconst queueId = webhookData.id || jobData.jobId;\n\nlet records = [];\nif (typeof tracerfyResults === 'string') {\n  const lines = tracerfyResults.split('\\n');\n  const headers = lines[0].split(',').map(h => h.trim());\n  for (let i = 1; i < lines.length; i++) {\n    if (!lines[i].trim()) continue;\n    const values = lines[i].split(',');\n    const record = {};\n    headers.forEach((h, idx) => record[h] = values[idx]?.trim() || '');\n    records.push(record);\n  }\n} else {\n  records = tracerfyResults.records || tracerfyResults.results || tracerfyResults.data || [tracerfyResults];\n}\n\nconst formatPhone = (phone) => {\n  if (!phone) return '';\n  let cleaned = String(phone).replace(/\\D/g, '');\n  if (cleaned.startsWith('1') && cleaned.length === 11) cleaned = cleaned.slice(1);\n  cleaned = cleaned.slice(-10);\n  return cleaned.length === 10 ? `+1${cleaned}` : '';\n};\n\nconst isMobileType = (phoneType) => {\n  if (!phoneType) return false;\n  const type = phoneType.toLowerCase().trim();\n  return type === 'mobile' || type === 'wireless' || type === 'cell' || type === 'cellular';\n};\n\nconst processedLeads = [];\nconst emailOnlyLeads = [];\nconst phonelessLeads = [];\nlet leadsWithMobile = 0;\nlet landlineFiltered = 0;\nlet unknownPhoneFiltered = 0;\nlet leadsNoContact = 0;\n\nfor (let i = 0; i < records.length; i++) {\n  const record = records[i];\n  const originalLead = leadData[i] || {};\n  const propertyData = originalLead.propertyData || {};\n  \n  const mobile1 = formatPhone(record['Mobile-1'] || record['mobile_1'] || record['mobile1']);\n  const mobile2 = formatPhone(record['Mobile-2'] || record['mobile_2'] || record['mobile2']);\n  const mobile3 = formatPhone(record['Mobile-3'] || record['mobile_3'] || record['mobile3']);\n  const primaryEmail = record['Email-1'] || record['email_1'] || record['email'] || '';\n  \n  const primaryPhoneType = (record['primary_phone_type'] || record['phone_type'] || '').toLowerCase();\n  const primaryPhone = record['primary_phone'] || record['phone'] || '';\n  \n  if (primaryPhoneType === 'landline') {\n    landlineFiltered++;\n  } else if (primaryPhone && !mobile1 && !isMobileType(primaryPhoneType)) {\n    unknownPhoneFiltered++;\n  }\n  \n  let phone1 = mobile1;\n  if (!phone1 && primaryPhone && isMobileType(primaryPhoneType)) {\n    phone1 = formatPhone(primaryPhone);\n  }\n  \n  const addr = propertyData.address || {};\n  const owner = propertyData.owner || {};\n  const ownerName = owner.names?.[0] || {};\n  const building = propertyData.building || {};\n  const valuation = propertyData.valuation || {};\n  const general = propertyData.general || {};\n  \n  const distressIndicators = propertyData.distressIndicators || [];\n  const distressScore = propertyData.distressScore || 0;\n  const primaryDistress = propertyData.primaryDistress || distressIndicators[0] || 'general';\n  const isAbsentee = propertyData.absenteeOwner === true;\n  const isOutOfState = propertyData.outOfState === true;\n  \n  const stagedLead = {\n    firstName: record['first_name'] || ownerName.first || originalLead.ownerName?.split(' ')[0] || '',\n    lastName: record['last_name'] || ownerName.last || originalLead.ownerName?.split(' ').slice(1).join(' ') || '',\n    email: primaryEmail,\n    address: originalLead.address || record['address'] || addr.street || '',\n    city: originalLead.city || record['city'] || addr.city || '',\n    state: originalLead.state || record['state'] || addr.state || 'OH',\n    zip: originalLead.zip || addr.zip || '',\n    county: addr.county || '',\n    ownerType: owner.type || 'individual',\n    absenteeOwner: isAbsentee,\n    outOfState: isOutOfState,\n    tier: jobData.tier || '',\n    tierName: jobData.tierName || '',\n    listingType: jobData.listingType || '',\n    importId: jobData.importId || '',\n    importDate: jobData.importDate || new Date().toISOString().split('T')[0],\n    status: 'pending',\n    propertyType: general.propertyTypeDetail || general.propertyType || '',\n    bedrooms: Number(building.bedroomCount || 0),\n    bathrooms: Number(building.bathroomCount || 0),\n    sqft: Number(building.livingAreaSquareFeet || 0),\n    yearBuilt: Number(building.yearBuilt || 0),\n    estimatedValue: Number(valuation.estimatedValue || 0),\n    equity: Number(valuation.equity || 0),\n    distressScore: distressScore,\n    primaryDistress: primaryDistress,\n    distressIndicators: distressIndicators\n  };\n  \n  const emailLead = {\n    firstName: record['first_name'] || ownerName.first || originalLead.ownerName?.split(' ')[0] || '',\n    lastName: record['last_name'] || ownerName.last || originalLead.ownerName?.split(' ').slice(1).join(' ') || '',\n    email: primaryEmail,\n    address: originalLead.address || record['address'] || addr.street || '',\n    city: originalLead.city || record['city'] || addr.city || '',\n    state: originalLead.state || record['state'] || addr.state || 'OH',\n    zip: originalLead.zip || addr.zip || '',\n    county: addr.county || '',\n    ownerType: owner.type || 'individual',\n    absenteeOwner: isAbsentee,\n    outOfState: isOutOfState,\n    tier: jobData.tier || '',\n    tierName: jobData.tierName || '',\n    listingType: jobData.listingType || '',\n    importId: jobData.importId || '',\n    importDate: jobData.importDate || new Date().toISOString().split('T')[0],\n    propertyType: general.propertyTypeDetail || general.propertyType || '',\n    bedrooms: Number(building.bedroomCount || 0),\n    bathrooms: Number(building.bathroomCount || 0),\n    sqft: Number(building.livingAreaSquareFeet || 0),\n    yearBuilt: Number(building.yearBuilt || 0),\n    estimatedValue: Number(valuation.estimatedValue || 0),\n    distressScore: distressScore,\n    primaryDistress: primaryDistress,\n    distressIndicators: distressIndicators,\n    source: 'batchdata_weekly'\n  };\n  \n  if (phone1) {\n    leadsWithMobile++;\n    processedLeads.push({ ...stagedLead, phone: phone1, phone2: mobile2, phone3: mobile3 });\n  } else if (primaryEmail) {\n    emailOnlyLeads.push(emailLead);\n  } else {\n    leadsNoContact++;\n    \n    let reason = 'no_contact';\n    let src = 'tracerfy_no_contact';\n    if (primaryPhoneType === 'landline') {\n      reason = 'landline';\n      src = 'tracerfy_landline';\n    } else if (primaryPhone) {\n      reason = 'unknown_type';\n      src = 'tracerfy_unknown';\n    }\n    \n    phonelessLeads.push({\n      firstName: stagedLead.firstName,\n      lastName: stagedLead.lastName,\n      landlinePhone: (reason === 'landline') ? formatPhone(primaryPhone) : undefined,\n      unknownPhone: (reason === 'unknown_type') ? formatPhone(primaryPhone) : undefined,\n      email: primaryEmail || undefined,\n      address: stagedLead.address,\n      city: stagedLead.city,\n      state: stagedLead.state,\n      zip: stagedLead.zip,\n      county: stagedLead.county || undefined,\n      propertyType: stagedLead.propertyType || undefined,\n      bedrooms: stagedLead.bedrooms || undefined,\n      bathrooms: stagedLead.bathrooms || undefined,\n      sqft: stagedLead.sqft || undefined,\n      yearBuilt: stagedLead.yearBuilt || undefined,\n      estimatedValue: stagedLead.estimatedValue || undefined,\n      distressScore: stagedLead.distressScore || undefined,\n      primaryDistress: stagedLead.primaryDistress || undefined,\n      distressIndicators: stagedLead.distressIndicators || undefined,\n      ownerType: stagedLead.ownerType || 'individual',\n      absenteeOwner: stagedLead.absenteeOwner || undefined,\n      outOfState: stagedLead.outOfState || undefined,\n      tier: jobData.tier || '',\n      tierName: jobData.tierName || '',\n      listingType: jobData.listingType || '',\n      importId: jobData.importId || '',\n      importDate: jobData.importDate || new Date().toISOString().split('T')[0],\n      skipTraceJobId: String(queueId),\n      phonelessReason: reason,\n      source: src\n    });\n  }\n}\n\nconsole.log('=== MOBILE-ONLY FILTER STATS ===');\nconsole.log(`Total records: ${records.length}`);\nconsole.log(`Leads with MOBILE: ${leadsWithMobile}`);\nconsole.log(`Landlines filtered: ${landlineFiltered}`);\nconsole.log(`Unknown phone type filtered: ${unknownPhoneFiltered}`);\nconsole.log(`Email-only leads: ${emailOnlyLeads.length}`);\nconsole.log(`Phoneless leads (stored): ${phonelessLeads.length}`);\nconsole.log(`No contact info: ${leadsNoContact}`);\n\nreturn [{ json: { \n  jobId: String(queueId), \n  tier: jobData.tier || '', \n  tierName: jobData.tierName || '', \n  listingType: jobData.listingType || '', \n  importId: jobData.importId || '', \n  importDate: jobData.importDate || new Date().toISOString().split('T')[0], \n  leadsReceived: records.length, \n  leadsWithPhone: leadsWithMobile,\n  leadsEmailOnly: emailOnlyLeads.length, \n  leadsNoContact, \n  leadsPhoneless: phonelessLeads.length,\n  landlineFiltered,\n  unknownPhoneFiltered,\n  leads: processedLeads, \n  emailOnlyLeads,\n  phonelessLeads\n} }];"
      },
      "id": "wf2-process-results",
      "name": "Process Skip Trace Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1072,
        -16
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "check-phone-count",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.leadsWithPhone }}",
              "rightValue": 0
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "wf2-check-phone-leads",
      "name": "Has Phone Leads?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1248,
        -16
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/staged-leads-bulk",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ leads: $('Process Skip Trace Results').first().json.leads, tier: $('Process Skip Trace Results').first().json.tier, tierName: $('Process Skip Trace Results').first().json.tierName, listingType: $('Process Skip Trace Results').first().json.listingType, leadsTarget: $('Process Skip Trace Results').first().json.leadsReceived, leadsReceived: $('Process Skip Trace Results').first().json.leadsReceived, leadsAfterFilter: $('Process Skip Trace Results').first().json.leadsWithPhone, importId: $('Process Skip Trace Results').first().json.importId, importDate: $('Process Skip Trace Results').first().json.importDate }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "wf2-stage-to-convex",
      "name": "Stage Leads to Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1248,
        -208
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "check-email-count",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $('Process Skip Trace Results').first().json.emailOnlyLeads.length }}",
              "rightValue": 0
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "wf2-check-email-leads",
      "name": "Has Email-Only Leads?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1440,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/email-leads",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ leads: $('Process Skip Trace Results').first().json.emailOnlyLeads }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "wf2-stage-email-leads",
      "name": "Stage Email-Only Leads",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        -16
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/skip-trace-jobs/complete",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ jobId: $('Process Skip Trace Results').first().json.jobId, leadsReturned: $('Process Skip Trace Results').first().json.leadsReceived, leadsWithPhone: $('Process Skip Trace Results').first().json.leadsWithPhone, leadsStaged: $json.inserted || $('Process Skip Trace Results').first().json.leadsWithPhone }) }}",
        "options": {}
      },
      "id": "wf2-complete-job",
      "name": "Complete Job in Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2320,
        -208
      ]
    },
    {
      "parameters": {
        "jsCode": "const processedData = $('Process Skip Trace Results').item.json;\nconsole.log(`=== JOB COMPLETED ===`);\nconsole.log(`Job ID: ${processedData.jobId}`);\nconsole.log(`Tier: ${processedData.tier}`);\nconsole.log(`Leads Received: ${processedData.leadsReceived}`);\nconsole.log(`Leads With Phone: ${processedData.leadsWithPhone}`);\nreturn [{ json: { jobId: processedData.jobId, tier: processedData.tier, tierName: processedData.tierName, listingType: processedData.listingType, importId: processedData.importId, status: 'completed', stats: { leadsReceived: processedData.leadsReceived, leadsWithPhone: processedData.leadsWithPhone, landlineFiltered: processedData.landlineFiltered } } }];"
      },
      "id": "wf2-log-job-result",
      "name": "Log Job Completion",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2528,
        -208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/financial-event",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"event_type\": \"tracerfy_return\",\n  \"amount\": {{ $('Process Skip Trace Results').item.json.leadsWithPhone * 0.02 }},\n  \"currency\": \"USD\",\n  \"description\": \"Tracerfy skip trace results returned\",\n  \"metadata\": {\n    \"workflow\": \"Tracerfy Results Handler\",\n    \"job_id\": \"{{ $('Process Skip Trace Results').item.json.jobId }}\",\n    \"total_records\": {{ $('Process Skip Trace Results').item.json.leadsReceived || 0 }},\n    \"with_phone\": {{ $('Process Skip Trace Results').item.json.leadsWithPhone || 0 }},\n    \"with_email\": {{ $('Process Skip Trace Results').item.json.leadsEmailOnly || 0 }},\n    \"cost_per_lead\": 0.02\n  }\n}",
        "options": {}
      },
      "id": "log-tracerfy-return",
      "name": "Log Tracerfy Return Stats",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1008,
        -256
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/log-lead-event",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"event_type\": \"staged\",\n  \"workflow\": \"tracerfy_results_handler\",\n  \"count\": {{ $json.inserted || $('Process Skip Trace Results').first().json.leadsWithPhone || 0 }},\n  \"tier\": \"{{ $('Process Skip Trace Results').first().json.tier }}\"\n}",
        "options": {}
      },
      "id": "log-pipeline-staged",
      "name": "Log Pipeline: Staged",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        -400
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Process Tracerfy results for VoiceSDR - titleCompany table\nvar rawInput = $input.first().json;\nvar records = [];\nif (typeof rawInput === 'string' || rawInput.data) {\n  var csvData = typeof rawInput === 'string' ? rawInput : rawInput.data;\n  var lines = csvData.split('\\n').filter(function(l) { return l.trim(); });\n  if (lines.length < 2) { return [{ json: { error: 'No data rows', processed: 0 } }]; }\n  var headers = lines[0].split(',').map(function(h) { return h.trim().replace(/\\\"/g, ''); });\n  for (var i = 1; i < lines.length; i++) {\n    var line = lines[i].trim();\n    if (!line) continue;\n    var values = []; var current = ''; var inQuotes = false;\n    for (var c = 0; c < line.length; c++) {\n      var ch = line[c];\n      if (ch === '\"') { inQuotes = !inQuotes; }\n      else if (ch === ',' && !inQuotes) { values.push(current.trim()); current = ''; }\n      else { current += ch; }\n    }\n    values.push(current.trim());\n    var record = {};\n    for (var h = 0; h < headers.length; h++) { record[headers[h]] = (values[h] || '').replace(/^\\\"|\\\"$/g, ''); }\n    records.push(record);\n  }\n} else { records = rawInput.records || rawInput.results || [rawInput]; }\nfunction formatPhone(phone) {\n  if (!phone) return '';\n  var cleaned = String(phone).replace(/[^0-9]/g, '');\n  if (cleaned.length === 11 && cleaned[0] === '1') cleaned = cleaned.slice(1);\n  cleaned = cleaned.slice(-10);\n  return cleaned.length === 10 ? '+1' + cleaned : '';\n}\nfunction isMobileType(phoneType) {\n  if (!phoneType) return false;\n  var type = phoneType.toLowerCase().trim();\n  return type === 'mobile' || type === 'wireless' || type === 'cell';\n}\nvar results = [];\nfor (var r = 0; r < records.length; r++) {\n  var rec = records[r];\n  var mobile1 = formatPhone(rec['Mobile-1']);\n  var mobile2 = formatPhone(rec['Mobile-2']);\n  var primaryPhone = rec['primary_phone'] || '';\n  var primaryPhoneType = (rec['primary_phone_type'] || '').toLowerCase();\n  var personalPhone = mobile1 || mobile2;\n  if (!personalPhone && isMobileType(primaryPhoneType)) { personalPhone = formatPhone(primaryPhone); }\n  var hasPhone = !!personalPhone;\n  var firstName = (rec['first_name'] || '').trim();\n  var lastName = (rec['last_name'] || '').trim();\n  console.log(firstName + ' ' + lastName + ': ' + (personalPhone || 'NO MOBILE') + ' (type: ' + primaryPhoneType + ')');\n  results.push({ json: { firstName: firstName, lastName: lastName, state: (rec['state'] || '').trim(), personPhone: personalPhone, hasPhone: hasPhone, phoneSource: 'tracerfy' } });\n}\nvar withPhone = results.filter(function(r) { return r.json.hasPhone; }).length;\nvar noPhone = results.filter(function(r) { return !r.json.hasPhone; }).length;\nconsole.log('Processed ' + results.length + ': ' + withPhone + ' with mobile, ' + noPhone + ' no mobile');\nreturn results.length > 0 ? results : [{ json: { error: 'No records', processed: 0 } }];"
      },
      "id": "tv_process",
      "name": "Process VoiceSDR Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/title-company/update-by-name",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"firstName\": \"{{ $json.firstName }}\",\n  \"lastName\": \"{{ $json.lastName }}\"\n  {{ $json.hasPhone ? ',\"personPhone\": \"' + $json.personPhone + '\",\"phoneSource\": \"tracerfy\"' : '' }}\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "tv_update",
      "name": "Update VoiceSDR Lead",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1088,
        208
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {},
      "id": "tv_done",
      "name": "VoiceSDR Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1312,
        208
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Tracerfy Webhook').item.json.body.download_url }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "tv_download",
      "name": "Download for VoiceSDR",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        208
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "4fe09531-3c0b-4034-b6d9-badc3fb40c80",
              "leftValue": "={{ $json.error }}",
              "rightValue": "Job not found",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        448,
        -16
      ],
      "id": "2d66dc64-9542-4d15-ac16-6a8e5ccd28c7",
      "name": "If"
    },
    {
      "parameters": {
        "url": "=https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/skip-trace-jobs/get?job_id={{ $json.body.id || $json.id }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "wf2-get-job-from-convex",
      "name": "Get Job from Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        -16
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "phoneless-check",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $('Process Skip Trace Results').first().json.leadsPhoneless }}",
              "rightValue": 0
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "wf2-check-phoneless",
      "name": "Has Phoneless Leads?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1888,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/phoneless-leads",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ leads: $('Process Skip Trace Results').first().json.phonelessLeads }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "wf2-stage-phoneless",
      "name": "Stage Phoneless Leads",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2112,
        0
      ]
    },
    {
      "parameters": {
        "url": "=https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/ghost-rider/check-tracerfy-queue?queue_id={{ $json.body.id || $json.id }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "gr_check_queue",
      "name": "Check Ghost Rider Queue",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        304,
        464
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "check-gr",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.isGhostRider }}",
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "gr_is_ghost_rider",
      "name": "Is Ghost Rider?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        464,
        464
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Tracerfy Webhook').item.json.body.download_url }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "gr_download",
      "name": "Download GR Results",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        464
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Process Tracerfy results for Ghost Rider \u2014 MOBILE ONLY\nvar rawInput = $input.first().json;\nvar grData = $('Check Ghost Rider Queue').first().json;\nvar parcelIds = grData.parcelIds || [];\n\nvar records = [];\nif (typeof rawInput === 'string' || rawInput.data) {\n  var csvData = typeof rawInput === 'string' ? rawInput : rawInput.data;\n  var lines = csvData.split('\\n').filter(function(l) { return l.trim(); });\n  if (lines.length < 2) { return [{ json: { error: 'No data rows', processed: 0 } }]; }\n  var headers = lines[0].split(',').map(function(h) { return h.trim().replace(/\"/g, ''); });\n  for (var i = 1; i < lines.length; i++) {\n    var line = lines[i].trim();\n    if (!line) continue;\n    var values = []; var current = ''; var inQuotes = false;\n    for (var c = 0; c < line.length; c++) {\n      var ch = line[c];\n      if (ch === '\"') { inQuotes = !inQuotes; }\n      else if (ch === ',' && !inQuotes) { values.push(current.trim()); current = ''; }\n      else { current += ch; }\n    }\n    values.push(current.trim());\n    var record = {};\n    for (var h = 0; h < headers.length; h++) { record[headers[h]] = (values[h] || '').replace(/^\"|\"$/g, ''); }\n    records.push(record);\n  }\n} else { records = rawInput.records || rawInput.results || [rawInput]; }\n\nfunction formatPhone(phone) {\n  if (!phone) return '';\n  var cleaned = String(phone).replace(/[^0-9]/g, '');\n  if (cleaned.length === 11 && cleaned[0] === '1') cleaned = cleaned.slice(1);\n  cleaned = cleaned.slice(-10);\n  return cleaned.length === 10 ? '+1' + cleaned : '';\n}\n\nfunction isMobileType(phoneType) {\n  if (!phoneType) return false;\n  var t = phoneType.toLowerCase().trim();\n  return t === 'mobile' || t === 'wireless' || t === 'cell';\n}\n\nvar results = [];\nvar withPhone = 0;\nvar noPhone = 0;\nvar skippedLandline = 0;\n\nfor (var r = 0; r < records.length; r++) {\n  var rec = records[r];\n  var parcelId = parcelIds[r] || '';\n  var mobile1 = formatPhone(rec['Mobile-1'] || rec['mobile_1'] || '');\n  var mobile2 = formatPhone(rec['Mobile-2'] || rec['mobile_2'] || '');\n  var mobile3 = formatPhone(rec['Mobile-3'] || rec['mobile_3'] || '');\n  var primaryPhone = rec['primary_phone'] || rec['phone'] || '';\n  var primaryPhoneType = (rec['primary_phone_type'] || rec['phone_type'] || '').toLowerCase();\n\n  // MOBILE ONLY \u2014 use Mobile-1/2/3 columns first\n  var phone = mobile1 || mobile2 || mobile3;\n  var phoneType = 'mobile';\n\n  // Fallback to primary_phone ONLY if it is mobile type\n  if (!phone && primaryPhone && isMobileType(primaryPhoneType)) {\n    phone = formatPhone(primaryPhone);\n    phoneType = 'mobile';\n  }\n\n  // Skip landline/unknown primary phones\n  if (!phone && primaryPhone) {\n    skippedLandline++;\n  }\n\n  var firstName = (rec['first_name'] || '').trim();\n  var lastName = (rec['last_name'] || '').trim();\n\n  if (phone && parcelId) {\n    withPhone++;\n    results.push({ json: { parcelId: parcelId, phone: phone, phoneType: phoneType, firstName: firstName, lastName: lastName, source: 'tracerfy' } });\n  } else {\n    noPhone++;\n  }\n}\n\nconsole.log('=== GHOST RIDER TRACERFY RESULTS ===');\nconsole.log('Total records: ' + records.length);\nconsole.log('With mobile: ' + withPhone + ', No mobile: ' + noPhone + ', Skipped landline: ' + skippedLandline);\nconsole.log('ParcelIds available: ' + parcelIds.length);\n\nreturn results.length > 0 ? results : [{ json: { error: 'No mobile numbers found', processed: records.length } }];"
      },
      "id": "gr_process",
      "name": "Process GR Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        464
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/ghost-rider/save-contact",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ parcelId: $json.parcelId, phone: $json.phone, phoneType: $json.phoneType, firstName: $json.firstName, lastName: $json.lastName, source: $json.source || 'tracerfy' }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "gr_save_contact",
      "name": "Save GR Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1088,
        464
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cm1",
              "leftValue": "={{ $('Get Job from Convex').item.json.source }}",
              "rightValue": "dishcraft",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "cm_check",
      "name": "Is DishCraft?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        464,
        720
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Get Job from Convex').item.json.result_url }}",
        "options": {}
      },
      "id": "cm_download",
      "name": "Download CM Results",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        704
      ]
    },
    {
      "parameters": {
        "jsCode": "// Process Tracerfy results for DishCraft leads\nvar items = $input.all();\nvar jobData = $('Get Job from Convex').first().json;\nvar leadMapping = JSON.parse(jobData.lead_mapping || '[]');\n\nvar results = [];\n\nfor (var i = 0; i < items.length; i++) {\n  var row = items[i].json;\n  var mapping = leadMapping[i] || {};\n  \n  // Skip if no phone found\n  var phone = row.phone1 || row.mobile1 || row.phone2 || '';\n  if (!phone) continue;\n  \n  results.push({json: {\n    leadId: mapping.leadId,\n    restaurantName: mapping.restaurantName,\n    ownerFirstName: row.first_name || '',\n    ownerLastName: row.last_name || '',\n    ownerPhone: phone,\n    ownerEmail: row.email1 || row.email2 || '',\n    phoneType: row.phone1_type || 'unknown'\n  }});\n}\n\nreturn results;"
      },
      "id": "cm_process",
      "name": "Process CM Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        912,
        704
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/dishcraft/leads/enrich",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ id: $json.leadId, ownerFirstName: $json.ownerFirstName, ownerLastName: $json.ownerLastName, ownerPhone: $json.ownerPhone, ownerEmail: $json.ownerEmail || undefined, enrichmentSource: 'tracerfy' }) }}",
        "options": {}
      },
      "id": "cm_update",
      "name": "Update DishCraft Lead",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1136,
        704
      ]
    },
    {
      "parameters": {},
      "id": "cm_done",
      "name": "DishCraft Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1360,
        704
      ]
    },
    {
      "id": "arn_check",
      "name": "Is ARN?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        464,
        960
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "check-arn",
              "leftValue": "={{ $('Tracerfy Webhook').first().json.body.source || $('Tracerfy Webhook').first().json.source || '' }}",
              "rightValue": "arn",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "arn_download",
      "name": "Download ARN Results",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        688,
        944
      ],
      "parameters": {
        "url": "={{ $('Tracerfy Webhook').first().json.body.result_url || $('Tracerfy Webhook').first().json.result_url }}",
        "options": {
          "timeout": 60000
        }
      }
    },
    {
      "id": "arn_process",
      "name": "Process ARN Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        912,
        944
      ],
      "parameters": {
        "jsCode": "// Process Tracerfy results for ARN agents\nvar results = $input.all();\nvar agents = [];\n\nfor (var i = 0; i < results.length; i++) {\n  var row = results[i].json;\n  \n  // Skip if no mobile phone\n  var phones = row.phones || [];\n  var mobilePhone = null;\n  \n  for (var j = 0; j < phones.length; j++) {\n    var p = phones[j];\n    var pType = (p.type || p.phone_type || '').toLowerCase();\n    if (pType === 'mobile' || pType === 'cell' || pType === 'wireless') {\n      mobilePhone = (p.phone || p.number || '').replace(/[^0-9]/g, '');\n      if (mobilePhone.length === 11 && mobilePhone[0] === '1') {\n        mobilePhone = mobilePhone.substring(1);\n      }\n      break;\n    }\n  }\n  \n  // Skip if no mobile found\n  if (!mobilePhone || mobilePhone.length !== 10) continue;\n  \n  // Build agent record\n  agents.push({\n    json: {\n      name: ((row.first_name || '') + ' ' + (row.last_name || '')).trim(),\n      phone: '+1' + mobilePhone,\n      brokerage: row.brokerage || row.company || '',\n      triggerPropertyAddress: row.address || '',\n      triggerPropertyCity: row.city || '',\n      triggerPropertyZip: row.zip || '',\n      scrapeSource: row.scrape_source || 'manual',\n      source: 'tracerfy_arn'\n    }\n  });\n}\n\nreturn agents.length > 0 ? agents : [{json: {skip: true, reason: 'no_mobile_phones'}}];"
      }
    },
    {
      "id": "arn_has_agents",
      "name": "Has ARN Agents?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1136,
        944
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "check-skip",
              "leftValue": "={{ $json.skip }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "arn_create",
      "name": "Create ARN Agent",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1360,
        880
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/arn-agent-create",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ name: $json.name, phone: $json.phone, brokerage: $json.brokerage, triggerPropertyAddress: $json.triggerPropertyAddress, triggerPropertyCity: $json.triggerPropertyCity, triggerPropertyZip: $json.triggerPropertyZip, scrapeSource: $json.scrapeSource, source: $json.source }) }}",
        "options": {
          "timeout": 10000
        }
      }
    },
    {
      "id": "arn_done",
      "name": "ARN Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1584,
        944
      ],
      "parameters": {}
    }
  ],
  "connections": {
    "Tracerfy Webhook": {
      "main": [
        [
          {
            "node": "Check Ghost Rider Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Ghost Rider Queue": {
      "main": [
        [
          {
            "node": "Is Ghost Rider?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Ghost Rider?": {
      "main": [
        [
          {
            "node": "Download GR Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Job from Convex",
            "type": "main",
            "index": 0
          },
          {
            "node": "Is DishCraft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Job from Convex": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Download Results from URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download for VoiceSDR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Results from URL": {
      "main": [
        [
          {
            "node": "Process Skip Trace Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Skip Trace Results": {
      "main": [
        [
          {
            "node": "Has Phone Leads?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Tracerfy Return Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Phone Leads?": {
      "main": [
        [
          {
            "node": "Stage Leads to Convex",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Has Email-Only Leads?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stage Leads to Convex": {
      "main": [
        [
          {
            "node": "Has Phoneless Leads?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Pipeline: Staged",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Email-Only Leads?": {
      "main": [
        [
          {
            "node": "Stage Email-Only Leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Has Phoneless Leads?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stage Email-Only Leads": {
      "main": [
        [
          {
            "node": "Has Phoneless Leads?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Phoneless Leads?": {
      "main": [
        [
          {
            "node": "Stage Phoneless Leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Complete Job in Convex",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stage Phoneless Leads": {
      "main": [
        [
          {
            "node": "Complete Job in Convex",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Complete Job in Convex": {
      "main": [
        [
          {
            "node": "Log Job Completion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download for VoiceSDR": {
      "main": [
        [
          {
            "node": "Process VoiceSDR Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process VoiceSDR Results": {
      "main": [
        [
          {
            "node": "Update VoiceSDR Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update VoiceSDR Lead": {
      "main": [
        [
          {
            "node": "VoiceSDR Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download GR Results": {
      "main": [
        [
          {
            "node": "Process GR Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process GR Results": {
      "main": [
        [
          {
            "node": "Save GR Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is DishCraft?": {
      "main": [
        [
          {
            "node": "Download CM Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Job from Convex",
            "type": "main",
            "index": 0
          },
          {
            "node": "Is ARN?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download CM Results": {
      "main": [
        [
          {
            "node": "Process CM Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process CM Results": {
      "main": [
        [
          {
            "node": "Update DishCraft Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update DishCraft Lead": {
      "main": [
        [
          {
            "node": "DishCraft Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is ARN?": {
      "main": [
        [
          {
            "node": "Download ARN Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Job from Convex",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download ARN Results": {
      "main": [
        [
          {
            "node": "Process ARN Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process ARN Results": {
      "main": [
        [
          {
            "node": "Has ARN Agents?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has ARN Agents?": {
      "main": [
        [
          {
            "node": "Create ARN Agent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ARN Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create ARN Agent": {
      "main": [
        [
          {
            "node": "ARN Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "binaryMode": "separate"
  },
  "active": false
}
Pro

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

About this workflow

Tracerfy Results Handler. Uses httpRequest. Webhook trigger; 35 nodes.

Source: https://github.com/rafiulislam4246/real-estate-acquisition-workflows/blob/main/workflows/04-skiptrace-results-router.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.

HTTP Request
Web Scraping

This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia

HTTP Request
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request