{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Find mobile numbers from LinkedIn profiles and alert sales team in Slack",
  "nodes": [
    {
      "id": "d0b47422-da33-412e-b7cb-cf49b6516d4d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        96
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Find mobile numbers from LinkedIn profiles and alert sales team in Slack\n\n### How it works\n\n1. The workflow is triggered manually and configured with a list of LinkedIn profile URLs, work emails, and a target Slack channel.\n2. Inputs are combined into a structured array and submitted as a mobile-finder job to the ScraperCity API.\n3. The workflow waits 30 seconds, then enters a polling loop that checks job status every 60 seconds until the job is complete.\n4. Once complete, the results CSV is downloaded, parsed into individual contact records, and deduplicated.\n5. Records without phone numbers are filtered out, and each remaining contact is formatted into a Slack message block.\n6. The sales team is alerted in the configured Slack channel with the discovered mobile numbers.\n\n### Setup steps\n\n- [ ] Obtain a ScraperCity API key and add it as an n8n credential for the HTTP Request nodes\n- [ ] Configure a Slack bot/app with `chat:write` permissions and add the credential to n8n\n- [ ] Open the 'Configure Inputs' node and populate `linkedinProfiles`, `workEmails`, and `slackChannel` with your target data\n- [ ] Verify the ScraperCity endpoint URLs in 'Submit Mobile Finder Job', 'Check Job Status', and 'Download Mobile Finder Results' match your account's API version\n- [ ] Run the workflow manually using 'Execute workflow' and monitor execution logs to confirm polling and results\n\n### Customization\n\nAdjust the wait durations in 'Wait 30 Seconds Before First Poll' and 'Wait 60 Seconds Before Retry' based on typical ScraperCity job completion times. The 'Format Slack Message' code node can be edited to change message layout, add contact fields, or include links back to LinkedIn profiles."
      },
      "typeVersion": 1
    },
    {
      "id": "070d636d-fd93-4b72-97d4-3e196580fcf3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Manual trigger and config\n\nEntry point that fires the workflow manually and sets the core input values: LinkedIn profile URLs, work emails, and the destination Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "383735df-37fa-4045-9a6b-82e87dfb2527",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 304,
        "content": "## Build and submit API job\n\nCombines LinkedIn URLs and emails into a single inputs array, then POSTs the job to the ScraperCity mobile-finder API and stores the returned run ID."
      },
      "typeVersion": 1
    },
    {
      "id": "4368b5b1-7109-4886-815b-0e2c6133418c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Initial wait and poll loop\n\nWaits 30 seconds before the first status check, then enters a loop controller that repeatedly drives polling until the job reports completion."
      },
      "typeVersion": 1
    },
    {
      "id": "8580e06c-820e-42b5-a9b3-7387b7f4f105",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 784,
        "content": "## Job status check and retry\n\nGETs the current job status from ScraperCity and branches: if complete, proceeds to download; if not, waits 60 seconds before looping back to the poll controller."
      },
      "typeVersion": 1
    },
    {
      "id": "ddcfd939-023f-4560-9698-c0198471b826",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 304,
        "content": "## Download and parse results\n\nDownloads the completed results file from ScraperCity, parses the CSV into one item per contact, and removes any duplicate entries."
      },
      "typeVersion": 1
    },
    {
      "id": "fec734c2-1efb-4dc9-ba02-ea16a0bc013a",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3456,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Filter and format contacts\n\nKeeps only records that contain a phone number, then formats each contact into a structured Slack message block ready for delivery."
      },
      "typeVersion": 1
    },
    {
      "id": "d0b3b42d-ccf8-4744-be16-e1d2236be1cc",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3936,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Slack sales team alert\n\nSends the formatted contact message to the configured Slack channel, notifying the sales team of newly discovered mobile numbers."
      },
      "typeVersion": 1
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000001",
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        208,
        304
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000002",
      "name": "Configure Inputs",
      "type": "n8n-nodes-base.set",
      "position": [
        464,
        304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-001",
              "name": "linkedinProfiles",
              "type": "string",
              "value": "linkedin.com/in/johndoe,linkedin.com/in/janedoe"
            },
            {
              "id": "cfg-002",
              "name": "workEmails",
              "type": "string",
              "value": "user@example.com,user@example.com"
            },
            {
              "id": "cfg-003",
              "name": "slackChannel",
              "type": "string",
              "value": "#sales-alerts"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000003",
      "name": "Build Inputs Array",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        304
      ],
      "parameters": {
        "jsCode": "// Combine LinkedIn URLs and work emails into a single inputs array\nconst linkedinRaw = $input.first().json.linkedinProfiles || '';\nconst emailsRaw = $input.first().json.workEmails || '';\n\nconst linkedin = linkedinRaw\n  .split(',')\n  .map(s => s.trim())\n  .filter(s => s.length > 0);\n\nconst emails = emailsRaw\n  .split(',')\n  .map(s => s.trim())\n  .filter(s => s.length > 0);\n\nconst inputs = [...linkedin, ...emails];\n\nreturn [{\n  json: {\n    inputs,\n    slackChannel: $input.first().json.slackChannel\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000004",
      "name": "Submit Mobile Finder Job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        304
      ],
      "parameters": {
        "url": "https://app.scrapercity.com/api/v1/scrape/mobile-finder",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ inputs: $json.inputs }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000005",
      "name": "Store Run ID",
      "type": "n8n-nodes-base.set",
      "position": [
        1248,
        304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "run-001",
              "name": "runId",
              "type": "string",
              "value": "={{ $json.runId }}"
            },
            {
              "id": "run-002",
              "name": "slackChannel",
              "type": "string",
              "value": "={{ $('Build Inputs Array').item.json.slackChannel }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000006",
      "name": "Wait 30 Seconds Before First Poll",
      "type": "n8n-nodes-base.wait",
      "position": [
        1504,
        304
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000007",
      "name": "Poll Loop Controller",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1760,
        304
      ],
      "parameters": {
        "options": {
          "reset": false
        },
        "batchSize": 1
      },
      "typeVersion": 3,
      "alwaysOutputData": false
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000008",
      "name": "Check Job Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2016,
        304
      ],
      "parameters": {
        "url": "=https://app.scrapercity.com/api/v1/scrape/status/{{ $json.runId }}",
        "method": "GET",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000009",
      "name": "Is Job Complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        2288,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-status-001",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "SUCCEEDED"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000010",
      "name": "Wait 60 Seconds Before Retry",
      "type": "n8n-nodes-base.wait",
      "position": [
        2544,
        768
      ],
      "parameters": {
        "amount": 60
      },
      "typeVersion": 1.1
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000011",
      "name": "Download Mobile Finder Results",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2768,
        304
      ],
      "parameters": {
        "url": "=https://app.scrapercity.com/api/downloads/{{ $('Store Run ID').item.json.runId }}",
        "method": "GET",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000012",
      "name": "Parse CSV Results",
      "type": "n8n-nodes-base.code",
      "position": [
        3024,
        304
      ],
      "parameters": {
        "jsCode": "// Parse CSV response from ScraperCity and return one item per contact\nconst raw = $input.first().json.data || $input.first().json.body || '';\n\nif (!raw || typeof raw !== 'string') {\n  // Attempt to handle if result is already an array/object\n  const direct = $input.first().json;\n  if (Array.isArray(direct)) {\n    return direct.map(row => ({ json: row }));\n  }\n  return [{ json: { error: 'No parsable data returned', raw: JSON.stringify(direct) } }];\n}\n\n// Split into lines and parse header + rows\nconst lines = raw.split('\\n').filter(l => l.trim().length > 0);\nif (lines.length < 2) {\n  return [{ json: { error: 'CSV has no data rows', raw } }];\n}\n\nconst headers = lines[0].split(',').map(h => h.trim().replace(/^\"|\"$/g, ''));\nconst results = [];\n\nfor (let i = 1; i < lines.length; i++) {\n  const cols = lines[i].split(',').map(c => c.trim().replace(/^\"|\"$/g, ''));\n  const record = {};\n  headers.forEach((h, idx) => {\n    record[h] = cols[idx] || '';\n  });\n  // Only include records that have some phone data\n  const phoneKey = headers.find(h => /phone|mobile/i.test(h));\n  if (phoneKey && record[phoneKey]) {\n    results.push({ json: record });\n  } else if (!phoneKey) {\n    // No phone column detected -- pass through anyway\n    results.push({ json: record });\n  }\n}\n\nreturn results.length > 0 ? results : [{ json: { info: 'No phone numbers found in results' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000013",
      "name": "Remove Duplicate Contacts",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        3280,
        304
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000014",
      "name": "Filter Records With Phone Numbers",
      "type": "n8n-nodes-base.filter",
      "position": [
        3504,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "filter-phone-001",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.mobile_phone }}",
              "rightValue": ""
            },
            {
              "id": "filter-phone-002",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.phone }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2,
      "alwaysOutputData": false
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000015",
      "name": "Format Slack Message",
      "type": "n8n-nodes-base.code",
      "position": [
        3760,
        304
      ],
      "parameters": {
        "jsCode": "// Build a Slack-friendly message block for each contact with a mobile number\nconst items = $input.all();\n\nconst lines = items.map((item, idx) => {\n  const d = item.json;\n  const name = d.full_name || d.name || d.first_name && d.last_name\n    ? `${d.first_name || ''} ${d.last_name || ''}`.trim()\n    : 'Unknown';\n  const phone = d.mobile_phone || d.phone || d.mobile || 'N/A';\n  const linkedin = d.linkedin_url || d.linkedin || d.input || 'N/A';\n  const email = d.email || 'N/A';\n  return `${idx + 1}. *${name}* -- Phone: ${phone} -- Email: ${email} -- LinkedIn: ${linkedin}`;\n});\n\nconst summary = `*Mobile Finder Results* -- ${items.length} contact(s) found with phone numbers:\\n\\n${lines.join('\\n')}`;\n\nreturn [{\n  json: {\n    slackMessage: summary,\n    contactCount: items.length,\n    slackChannel: $('Store Run ID').item.json.slackChannel\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a1b2c3d4-0001-0001-0001-000000000016",
      "name": "Alert Sales Team in Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        3984,
        304
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.slackChannel }}"
        },
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Store Run ID": {
      "main": [
        [
          {
            "node": "Wait 30 Seconds Before First Poll",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Job Status": {
      "main": [
        [
          {
            "node": "Is Job Complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Inputs": {
      "main": [
        [
          {
            "node": "Build Inputs Array",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Job Complete?": {
      "main": [
        [
          {
            "node": "Download Mobile Finder Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 60 Seconds Before Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse CSV Results": {
      "main": [
        [
          {
            "node": "Remove Duplicate Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Inputs Array": {
      "main": [
        [
          {
            "node": "Submit Mobile Finder Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Slack Message": {
      "main": [
        [
          {
            "node": "Alert Sales Team in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Loop Controller": {
      "main": [
        [
          {
            "node": "Check Job Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit Mobile Finder Job": {
      "main": [
        [
          {
            "node": "Store Run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicate Contacts": {
      "main": [
        [
          {
            "node": "Filter Records With Phone Numbers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 60 Seconds Before Retry": {
      "main": [
        [
          {
            "node": "Poll Loop Controller",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Mobile Finder Results": {
      "main": [
        [
          {
            "node": "Parse CSV Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Configure Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Records With Phone Numbers": {
      "main": [
        [
          {
            "node": "Format Slack Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30 Seconds Before First Poll": {
      "main": [
        [
          {
            "node": "Poll Loop Controller",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}