AutomationFlowsSocial Media › Tool - Lookup_linkedin_profile

Tool - Lookup_linkedin_profile

61 - TOOL - lookup_linkedin_profile. Uses executeWorkflowTrigger, httpRequest, dataTable. Event-driven trigger; 10 nodes.

Event trigger★★★★☆ complexity10 nodesExecute Workflow TriggerHTTP RequestData Table
Social Media Trigger: Event Nodes: 10 Complexity: ★★★★☆ Added:
Tool - Lookup_linkedin_profile — n8n workflow card showing Execute Workflow Trigger, HTTP Request, Data Table integration

This workflow follows the Datatable → 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
{
  "name": "61 - TOOL - lookup_linkedin_profile",
  "nodes": [
    {
      "parameters": {
        "content": "## LinkedIn profile lookup\n\nThis paid, read-only tool searches Crustdata's indexed professional-person dataset for one named person. It requires explicit approval for a maximum 0.30-credit search, ranks the returned candidates, and exposes only public professional fields. It never returns email addresses, phone numbers, contact records, or the raw provider payload.",
        "height": 260,
        "width": 620,
        "color": 5
      },
      "id": "b6100000-0000-4000-8000-000000000000",
      "name": "Lookup explanation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1120,
        -300
      ]
    },
    {
      "parameters": {
        "inputSource": "workflowInputs",
        "workflowInputs": {
          "values": [
            {
              "name": "session_id",
              "type": "string"
            },
            {
              "name": "request_id",
              "type": "string"
            },
            {
              "name": "email_address",
              "type": "string"
            },
            {
              "name": "full_name",
              "type": "string"
            },
            {
              "name": "country_region",
              "type": "string"
            },
            {
              "name": "state_province",
              "type": "string"
            },
            {
              "name": "city_location",
              "type": "string"
            },
            {
              "name": "industry",
              "type": "string"
            },
            {
              "name": "paid_lookup_confirmed",
              "type": "boolean"
            }
          ]
        }
      },
      "id": "b6100000-0000-4000-8000-000000000001",
      "name": "Tool Input",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.2,
      "position": [
        -1120,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const clean = (value, limit = 200) => typeof value === 'string' ? value.trim().slice(0, limit) : '';\nconst sessionId = clean($json.session_id, 100);\nconst requestId = clean($json.request_id, 100) || 'linkedin-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);\nconst emailAddress = clean($json.email_address, 254);\nconst fullName = clean($json.full_name, 160);\nconst countryRegion = clean($json.country_region, 100);\nconst stateProvince = clean($json.state_province, 100);\nconst cityLocation = clean($json.city_location, 100);\nconst industry = clean($json.industry, 180);\nconst normalise = (value) => String(value ?? '').normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\nconst industryStop = new Set(['and','business','services','service','company','industry','sector','professional']);\nconst industryTerms = industry\n  .toLowerCase()\n  .replace(/\\band\\b/g, ',')\n  .replaceAll(';', ',')\n  .replaceAll('/', ',')\n  .replaceAll('|', ',')\n  .split(',')\n  .map(normalise)\n  .filter((term) => term.length >= 3 && !industryStop.has(term))\n  .slice(0, 4);\nconst paidLookupConfirmed = $json.paid_lookup_confirmed === true;\nconst emailOk = !emailAddress || /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(emailAddress);\nconst nameTokens = normalise(fullName).split(' ').filter(Boolean);\nconst honorificAliases = { dr: 'dr', doctor: 'dr', prof: 'prof', professor: 'prof' };\nconst requestedHonorific = honorificAliases[nameTokens[0]] ?? '';\nconst searchName = fullName\n  .replace(/^(dr|doctor|mr|mrs|ms|miss|prof|professor)\\.?\\s+/i, '')\n  .replace(/\\s+(phd|md|mba|jd|esq)\\.?$/i, '')\n  .trim();\nconst australianCapitalRegions = {\n  adelaide: 'South Australia', brisbane: 'Queensland', canberra: 'Australian Capital Territory',\n  darwin: 'Northern Territory', hobart: 'Tasmania', melbourne: 'Victoria', perth: 'Western Australia',\n  sydney: 'New South Wales'\n};\nconst inferredRegion = normalise(countryRegion).includes('australia')\n  ? (australianCapitalRegions[normalise(cityLocation)] ?? '')\n  : '';\nlet errorCode = '';\nlet errorMessage = '';\nif (!searchName || searchName.split(/\\s+/).length < 2) {\n  errorCode = 'FULL_NAME_REQUIRED';\n  errorMessage = 'Provide the person\\'s first and last name before searching.';\n} else if (!emailOk) {\n  errorCode = 'INVALID_EMAIL';\n  errorMessage = 'The optional work email is not valid.';\n} else if (!paidLookupConfirmed) {\n  errorCode = 'PAID_LOOKUP_APPROVAL_REQUIRED';\n  errorMessage = 'Before searching, explicitly approve one Crustdata person search costing up to 0.30 credits.';\n}\nconst conditions = [{ field: 'basic_profile.name', type: '(.)', value: searchName }];\n// Location is never a provider filter. Crustdata returns HTTP 200 with\n// total_count 0 for basic_profile.location.full_location and for\n// basic_profile.location.country with either '(.)' or '=', so any location\n// condition silently empties the result set. City, state, country and\n// industry are scored locally in Rank Safe Candidates instead.\nconst searchScope = 'name only';\nconst searchBody = {\n  filters: conditions.length === 1 ? conditions[0] : { op: 'and', conditions },\n  fields: [\n    'metadata.updated_at',\n    'basic_profile',\n    'experience.employment_details.current',\n    'social_handles.professional_network_identifier.profile_url'\n  ],\n  limit: 10\n};\nconst maskedEmail = emailAddress && emailAddress.includes('@')\n  ? emailAddress[0] + '***@' + emailAddress.split('@').pop()\n  : '';\nreturn { json: {\n  valid: errorCode === '', errorCode, errorMessage, sessionId, requestId,\n  emailAddress, maskedEmail, fullName, searchName, requestedHonorific,\n  countryRegion, stateProvince, cityLocation, inferredRegion,\n  industry, industryTerms, paidLookupConfirmed, searchBody, searchScope, maxCredits: 0.30\n} };"
      },
      "id": "b6100000-0000-4000-8000-000000000002",
      "name": "Validate Lookup Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -880,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "b6100000-0000-4000-8000-000000000003c",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b6100000-0000-4000-8000-000000000003",
      "name": "Input Is Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -640,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.crustdata.com/person/search",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-version",
              "value": "2025-11-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.searchBody) }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "b6100000-0000-4000-8000-000000000004",
      "name": "Search Crustdata People",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -400,
        -100
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const input = $('Validate Lookup Input').first().json;\nconst statusCode = Number($json.statusCode ?? 200);\nconst body = $json.body && typeof $json.body === 'object' ? $json.body : $json;\nconst headers = $json.headers && typeof $json.headers === 'object' ? $json.headers : {};\nconst creditsUsed = Number(headers['x-credits-used'] ?? headers['X-Credits-Used'] ?? 0) || 0;\nif (statusCode < 200 || statusCode >= 300) {\n  return { json: { response: { ok: false, match_status: 'unavailable', confidence: 'none', score: 0, evidence: [], profile: null, candidates: [], profile_enriched: false, credits_used: creditsUsed, message: 'The professional-data provider was unavailable (HTTP ' + statusCode + '). No automatic retry was made.' } } };\n}\nconst profiles = Array.isArray(body.profiles) ? body.profiles : [];\nconst normalise = (value) => String(value ?? '').normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\nconst ignoredNameTokens = new Set(['dr','doctor','mr','mrs','ms','miss','prof','professor','phd','md','mba','jd','esq','fgia','cem']);\nconst normaliseName = (value) => normalise(value).split(' ').filter((token) => !ignoredNameTokens.has(token)).join(' ');\nconst wantedName = normaliseName(input.fullName);\nconst wantedLocation = {\n  country: normalise(input.countryRegion),\n  state: normalise(input.stateProvince || input.inferredRegion),\n  city: normalise(input.cityLocation)\n};\nconst stop = new Set(['and','business','services','service','company','industry','sector','professional']);\nconst industryTerms = Array.isArray(input.industryTerms)\n  ? input.industryTerms.map(normalise).filter((term) => term.length >= 3 && !stop.has(term)).slice(0, 4)\n  : String(input.industry ?? '')\n      .toLowerCase()\n      .replace(/\\band\\b/g, ',')\n      .replaceAll(';', ',')\n      .replaceAll('/', ',')\n      .replaceAll('|', ',')\n      .split(',')\n      .map(normalise)\n      .filter((term) => term.length >= 3 && !stop.has(term))\n      .slice(0, 4);\nconst linkedinSlug = (value) => {\n  const url = String(value ?? '').trim();\n  const lower = url.toLowerCase();\n  const prefix = 'https://';\n  if (!lower.startsWith(prefix)) return '';\n  const afterScheme = lower.slice(prefix.length);\n  const firstSlash = afterScheme.indexOf('/');\n  if (firstSlash <= 0) return '';\n  const host = afterScheme.slice(0, firstSlash);\n  if (host !== 'linkedin.com' && !host.endsWith('.linkedin.com')) return '';\n  const pathAndSuffix = afterScheme.slice(firstSlash);\n  if (!pathAndSuffix.startsWith('/in/')) return '';\n  const slugAndSuffix = pathAndSuffix.slice(4);\n  let stopAt = slugAndSuffix.length;\n  for (const separator of ['/', '?', '#']) {\n    const position = slugAndSuffix.indexOf(separator);\n    if (position >= 0 && position < stopAt) stopAt = position;\n  }\n  const slug = slugAndSuffix.slice(0, stopAt);\n  const allowed = 'abcdefghijklmnopqrstuvwxyz0123456789%_.-';\n  return slug && [...slug].every((character) => allowed.includes(character)) ? slug : '';\n};\nconst safeUrl = (value) => linkedinSlug(value) ? String(value ?? '').trim() : '';\nconst requestedHonorific = String(input.requestedHonorific ?? '');\nconst honorificMatches = (name) => {\n  const tokens = normalise(name).split(' ');\n  if (requestedHonorific === 'dr') return tokens.includes('dr') || tokens.includes('doctor');\n  if (requestedHonorific === 'prof') return tokens.includes('prof') || tokens.includes('professor');\n  return false;\n};\nconst rows = profiles.map((raw) => {\n  const basic = raw.basic_profile && typeof raw.basic_profile === 'object' ? raw.basic_profile : {};\n  const location = basic.location && typeof basic.location === 'object' ? basic.location : {};\n  const current = raw.experience?.employment_details?.current;\n  const jobs = Array.isArray(current) ? current : [];\n  const primary = jobs.find((job) => job?.is_default === true) ?? jobs[0] ?? {};\n  const name = String(basic.name ?? '').trim();\n  const professionalNetworkName = String(basic.professional_network_name ?? '').trim();\n  const profileUrl = safeUrl(raw.social_handles?.professional_network_identifier?.profile_url);\n  const currentCompany = String(primary.name ?? primary.company_name ?? '').trim();\n  const currentTitle = String(basic.current_title ?? primary.title ?? '').trim();\n  const fullLocation = String(location.full_location ?? location.raw ?? '').trim();\n  const structuredLocation = [location.city, location.state, location.country].map((value) => String(value ?? '').trim()).filter(Boolean).join(', ');\n  const displayLocation = fullLocation || structuredLocation;\n  const industries = [...new Set(jobs.flatMap((job) => [\n    ...(Array.isArray(job?.company_industries) ? job.company_industries : []),\n    job?.company_professional_network_industry\n  ]).map((value) => String(value ?? '').trim()).filter(Boolean))];\n  let score = 0;\n  const evidence = [];\n  const actualName = normaliseName(professionalNetworkName || name);\n  if (wantedName && actualName === wantedName) { score += 54; evidence.push('exact core name'); }\n  else if (wantedName && actualName && wantedName.split(' ').every((token) => actualName.split(' ').includes(token))) { score += 40; evidence.push('partial name'); }\n  if (requestedHonorific && honorificMatches([professionalNetworkName, name].filter(Boolean).join(' '))) { score += 36; evidence.push('requested professional title'); }\n  const normalLocation = normalise([fullLocation, structuredLocation].filter(Boolean).join(' '));\n  for (const [field, points, label] of [['city',10,'city'],['state',8,input.stateProvince ? 'state or province' : 'metropolitan region'],['country',6,'country or region']]) {\n    if (wantedLocation[field] && normalLocation.includes(wantedLocation[field])) { score += points; evidence.push(label); }\n  }\n  const professionalText = normalise([\n    industries.join(' '),\n    basic.headline,\n    basic.summary,\n    currentTitle,\n    currentCompany,\n    ...jobs.flatMap((job) => [job?.title, job?.name, job?.company_name, job?.function_category, job?.description])\n  ].join(' '));\n  if (industryTerms.length && industryTerms.some((term) => professionalText.includes(term))) { score += 10; evidence.push('industry or professional context'); }\n  const profile = { name: professionalNetworkName || name || null, linkedin_url: profileUrl || null, headline: String(basic.headline ?? '').trim() || null, current_company: currentCompany || null, current_title: currentTitle || null, location: displayLocation || null, industry: industries.join(', ') || null, public_identifier: profileUrl ? linkedinSlug(profileUrl) : null };\n  return { score: Math.max(0, Math.min(100, score)), evidence, profile };\n}).filter((row) => row.profile.linkedin_url).sort((a, b) => b.score - a.score);\nif (rows.length === 0) {\n  return { json: { response: { ok: true, match_status: 'not_found', confidence: 'none', score: 0, evidence: [], profile: null, candidates: [], profile_enriched: false, credits_used: creditsUsed, total_matches: Number(body.total_count ?? 0), message: 'The provider returned no profile for this ' + String(input.searchScope || 'name') + ' search. Widen or correct a detail before approving another paid search.' } } };\n}\nconst top = rows[0];\nconst gap = top.score - (rows[1]?.score ?? 0);\nconst matched = top.score >= 76 && gap >= 10;\nconst confidence = matched ? (top.score >= 84 ? 'high' : 'medium') : (top.score >= 60 ? 'low' : 'none');\nreturn { json: { response: { ok: true, match_status: matched ? 'matched' : 'ambiguous', confidence, score: top.score, evidence: top.evidence, profile: matched ? top.profile : null, candidates: matched ? [] : rows.slice(0, 3), profile_enriched: false, credits_used: creditsUsed, total_matches: Number(body.total_count ?? rows.length), returned_matches: profiles.length, message: matched ? 'One likely public professional profile was identified. Verify it before relying on the match.' : 'Several people may match. No profile was selected; add a current employer, role, or more specific location before another paid search.' } } };"
      },
      "id": "b6100000-0000-4000-8000-000000000005",
      "name": "Rank Safe Candidates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -160,
        -100
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const input = $('Validate Lookup Input').first().json;\nreturn { json: { response: { ok: false, match_status: 'unavailable', confidence: 'none', score: 0, evidence: [], profile: null, candidates: [], profile_enriched: false, credits_used: 0, error: { code: input.errorCode || 'INVALID_INPUT', message: input.errorMessage || 'The lookup input could not be read.' }, message: input.errorMessage || 'The lookup input could not be read.' } } };"
      },
      "id": "b6100000-0000-4000-8000-000000000006",
      "name": "Shape Invalid Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -400,
        120
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const response = $json.response;\nconst input = $('Validate Lookup Input').first().json;\nconst error = response?.ok === false ? String(response.error?.message ?? response.message ?? 'Tool failed') : '';\nreturn { json: { occurredAt: new Date().toISOString(), sessionId: input.sessionId, requestId: input.requestId, toolName: 'lookup_linkedin_profile', proposedInput: JSON.stringify({ full_name: input.fullName, email_masked: input.maskedEmail, country_region: input.countryRegion, state_province: input.stateProvince, city_location: input.cityLocation, industry: input.industry, maximum_credits: input.maxCredits }), result: error ? 'error' : String(response.match_status ?? 'ok'), error, response } };"
      },
      "id": "b6100000-0000-4000-8000-000000000007",
      "name": "Prepare Audit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        80,
        0
      ]
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": {
          "__rl": true,
          "value": "tool_audit",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "occurredAt": "={{ $json.occurredAt }}",
            "sessionId": "={{ $json.sessionId }}",
            "requestId": "={{ $json.requestId }}",
            "toolName": "={{ $json.toolName }}",
            "proposedInput": "={{ $json.proposedInput }}",
            "result": "={{ $json.result }}",
            "error": "={{ $json.error }}"
          }
        },
        "options": {}
      },
      "id": "b6100000-0000-4000-8000-000000000008",
      "name": "Write Tool Audit",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        320,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const prepared = $('Prepare Audit').item.json;\nconst response = { ...prepared.response };\nif (!Number.isInteger($json.id)) response.auditWarning = 'The lookup result could not be written to the local tool audit.';\nreturn { json: response };"
      },
      "id": "b6100000-0000-4000-8000-000000000009",
      "name": "Return Tool Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        0
      ]
    }
  ],
  "connections": {
    "Tool Input": {
      "main": [
        [
          {
            "node": "Validate Lookup Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Lookup Input": {
      "main": [
        [
          {
            "node": "Input Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Is Valid?": {
      "main": [
        [
          {
            "node": "Search Crustdata People",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Shape Invalid Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Crustdata People": {
      "main": [
        [
          {
            "node": "Rank Safe Candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank Safe Candidates": {
      "main": [
        [
          {
            "node": "Prepare Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape Invalid Input": {
      "main": [
        [
          {
            "node": "Prepare Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Audit": {
      "main": [
        [
          {
            "node": "Write Tool Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Tool Audit": {
      "main": [
        [
          {
            "node": "Return Tool Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 45,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveExecutionProgress": true,
    "saveManualExecutions": true
  },
  "versionId": "b6100000-0000-4000-8000-000000000100",
  "meta": {
    "templateCredsSetupCompleted": false,
    "phase": 12,
    "testedWithN8n": "2.30.5",
    "toolRisk": "paid_external_read",
    "authorization": "explicit-current-user-credit-approval",
    "maximumCreditsPerRun": 0.3,
    "externalWrite": "none",
    "personalContactData": "not-requested-not-returned"
  },
  "tags": [
    {
      "id": "tagAgentCanDo",
      "name": "What your agent can do"
    }
  ],
  "id": "phase12LookupLinkedInProfile"
}

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

61 - TOOL - lookup_linkedin_profile. Uses executeWorkflowTrigger, httpRequest, dataTable. Event-driven trigger; 10 nodes.

Source: https://github.com/drsamdonegan/ai-solopreneur/blob/main/n8n/workflows/61-tool-lookup-linkedin-profile.json — original creator credit. Request a take-down →

More Social Media workflows → · Browse all categories →

Related workflows

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

Social Media

Disclaimer: this workflow only works on self-hosted instances due to the file system usage.

Execute Workflow Trigger, HTTP Request, Form Trigger +3
Social Media

This workflow polls a YouTube Shorts playlist RSS feed, deduplicates videos using an n8n Data Table, downloads each new Short via the YouTube Media Downloader API (RapidAPI), and uploads it to TikTok

Rss Feed Read Trigger, Data Table, HTTP Request
Social Media

Thread Detection: Automatically detects whether the provided Twitter link is a single tweet or a thread. Tweet Extraction: Fetches and returns the content of a single tweet, or gathers all tweets in a

Execute Workflow Trigger, HTTP Request
Social Media

This workflow retrieves a LinkedIn contact’s full name from their profile URL using the LinkUp AI Search API. It’s a lightweight alternative to web scraping or complex APIs — integrating neatly into y

HTTP Request, Data Table
Social Media

This subworkflow is ideal for developers and automation builders working with UniPile and n8n to automate message enrichment and LinkedIn lead routing.

Execute Workflow Trigger, HTTP Request