{
  "id": "PuYluuZv48Qz4rRX",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Source candidates from job ads posted in Slack with Coresignal and Claude",
  "tags": [],
  "nodes": [
    {
      "id": "5f09b87b-3f1b-42ba-8d12-644f4984a611",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4656,
        2096
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Source candidates from job ads posted in Slack with Coresignal and Claude\n\n### How it works\n\nThis workflow monitors Slack for newly posted job ads, extracts job URLs, and processes each unique ad in a loop. For each job, it retrieves job details from Coresignal, uses Claude to infer candidate search filters, searches Coresignal\u2019s employee database, and posts a formatted candidate shortlist back to Slack. It also acknowledges each job as it enters processing.\n\n### Setup steps\n\n- Connect and authorize the Slack Trigger and Slack nodes with access to the source channel and the destination channel/thread where acknowledgements and shortlists should be posted.\n- Configure Coresignal credentials/API access for the job search, job enrichment, employee search, and employee retrieval nodes.\n- Configure the Anthropic Chat Model credentials for Claude in the AI filter extraction chain.\n- Set the workflow configuration value `candidates_per_job` to control how many candidates are returned per job ad.\n- Verify the Slack message format contains job URLs that the Extract Job URLs code node can parse.\n\n### Customization\n\nAdjust the URL extraction logic, Claude prompt/schema for search filters, Coresignal employee query builder, or Slack shortlist formatting to match different job boards, candidate criteria, and posting style."
      },
      "typeVersion": 1
    },
    {
      "id": "2650c7f0-fb67-4a77-b3ba-124f59786b95",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5152,
        2096
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 336,
        "content": "## Capture job ads\n\nListens for Slack messages, sets the candidate count configuration, extracts job posting URLs, and removes duplicates so only new job ads continue."
      },
      "typeVersion": 1
    },
    {
      "id": "64786ec6-52df-4e37-9711-e9dc13b94947",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6096,
        2128
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 480,
        "content": "## Iterate and acknowledge\n\nLoops through each unique job ad and sends a nearby Slack acknowledgement that processing has started."
      },
      "typeVersion": 1
    },
    {
      "id": "974640fc-5a08-419f-9f88-c2b7206bfcc1",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6576,
        2144
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 304,
        "content": "## Retrieve job details\n\nSearches Coresignal for the job, selects the relevant job ID from the search response, and fetches the full job record."
      },
      "typeVersion": 1
    },
    {
      "id": "14f3441e-2ca7-4743-a7e9-2aef716d9b5e",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7232,
        2112
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 528,
        "content": "## Extract candidate filters\n\nUses Claude with a structured output parser to convert the job details into search filters, then builds a Coresignal employee search query from those filters."
      },
      "typeVersion": 1
    },
    {
      "id": "b76d7521-4b57-4562-a352-dbf1e3d7c5bb",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7872,
        2144
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 304,
        "content": "## Find matching employees\n\nRuns the generated employee search in Coresignal, keeps the top scoring employee IDs, and retrieves the corresponding employee profiles."
      },
      "typeVersion": 1
    },
    {
      "id": "f5f9fc91-c1db-4653-9eb8-2a4448d65c6b",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8528,
        2096
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 336,
        "content": "## Post candidate shortlist\n\nFormats the retrieved profiles into a Slack-friendly shortlist and posts it back to Slack before returning control to the job loop for the next ad."
      },
      "typeVersion": 1
    },
    {
      "id": "ae1d2634-00dd-4d49-b8d7-92b5a118dc35",
      "name": "When Job Ad Posted on Slack",
      "type": "n8n-nodes-base.slackTrigger",
      "position": [
        5216,
        2256
      ],
      "parameters": {
        "options": {},
        "trigger": [
          "message"
        ],
        "authentication": "oAuth2",
        "watchWorkspace": true
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5c720d03-a6f2-47b5-8b44-7662707e9cfa",
      "name": "Set Job Candidates Limit",
      "type": "n8n-nodes-base.set",
      "position": [
        5440,
        2256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-count",
              "name": "candidates_per_job",
              "type": "number",
              "value": 5
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "2e944429-8559-47c9-ad02-3d1430a0e838",
      "name": "Extract Job URL from Message",
      "type": "n8n-nodes-base.code",
      "position": [
        5648,
        2256
      ],
      "parameters": {
        "jsCode": "// Input is a single Slack message event from the Slack Trigger, with\n// candidates_per_job merged in by Workflow Config.\n// Pull every LinkedIn job URL out of the text and emit one item per job.\n// Returning an empty array is fine \u2014 downstream nodes simply don't run,\n// which is how ordinary channel chatter costs nothing.\n\nconst out = [];\nconst seenThisRun = new Set();\n\nfor (const item of $input.all()) {\n  const m = item.json || {};\n\n  // Skip anything the bot posted (our own shortlist contains the job URL and\n  // would otherwise re-trigger the workflow) and skip joins/edits/etc.\n  if (m.bot_id || m.subtype) continue;\n\n  let raw = String(m.text || '');\n  raw = raw.replace(/<@[^>]+>/g, ' ');              // strip @mentions\n  raw = raw.replace(/<([^|>]+)(\\|[^>]*)?>/g, '$1'); // unwrap Slack's <url|label>\n\n  const urls = raw.match(/https?:\\/\\/[^\\s<>]*linkedin\\.com\\/jobs[^\\s<>]*/gi) || [];\n\n  // The trigger gives the channel as a plain ID string (resolveIds off).\n  const channelId = typeof m.channel === 'string'\n    ? m.channel\n    : (m.channel && (m.channel.id || m.channel.value)) || m.channel_id || null;\n\n  for (const url of urls) {\n    // /jobs/view/4237171755  |  /jobs/view/slug-at-company-4237171755  |  ?currentJobId=4237171755\n    const patterns = [\n      /currentJobId=(\\d{6,})/,\n      /jobs\\/view\\/(?:[^\\/?#\\s]*?-)?(\\d{6,})/\n    ];\n\n    let jobId = null;\n    for (const p of patterns) {\n      const mm = url.match(p);\n      if (mm) { jobId = mm[1]; break; }\n    }\n    if (!jobId || seenThisRun.has(jobId)) continue;\n    seenThisRun.add(jobId);\n\n    out.push({\n      json: {\n        linkedin_job_id: jobId,\n        job_url: url,\n        slack_channel_id: channelId,\n        slack_user_id: m.user || null,\n        slack_message_ts: m.ts || null,\n        slack_thread_ts: m.thread_ts || m.ts || null,\n        candidates_per_job: Number(m.candidates_per_job) || 5\n      }\n    });\n  }\n}\n\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "274ff877-361f-43bc-b8fd-6cdacdcc38ae",
      "name": "Deduplicate Job Ads",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        5872,
        2256
      ],
      "parameters": {
        "options": {
          "historySize": 10000
        },
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.linkedin_job_id }}"
      },
      "typeVersion": 2
    },
    {
      "id": "9440906f-6b42-4de7-b4d2-fa31350b3955",
      "name": "Loop Over Job Listings",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        6144,
        2256
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "937eae7d-8876-4c49-ab58-a58ffbfd56a4",
      "name": "Fetch Job Details from Coresignal",
      "type": "n8n-nodes-coresignal-api.coresignal",
      "position": [
        6624,
        2272
      ],
      "parameters": {
        "esQuery": "={{ JSON.stringify({ query: { nested: { path: 'job_sources', query: { term: { 'job_sources.source_id': $json.linkedin_job_id } } } } }) }}",
        "resource": "job",
        "operation": "search_es_dsl"
      },
      "credentials": {
        "coresignalApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ac913546-ee76-43ed-a1c5-c47d8d5267dd",
      "name": "Post Acknowledgement on Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        6416,
        2432
      ],
      "parameters": {
        "text": "=:hourglass_flowing_sand: <@{{ $json.slack_user_id }}> on it \u2014 pulling job `{{ $json.linkedin_job_id }}` from Coresignal and searching for candidates.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.slack_channel_id }}"
        },
        "otherOptions": {
          "mrkdwn": true,
          "unfurl_links": false,
          "includeLinkToWorkflow": false
        },
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4cccff91-0e61-4353-b73b-7e90e892b67e",
      "name": "Extract Job ID",
      "type": "n8n-nodes-base.code",
      "position": [
        6848,
        2272
      ],
      "parameters": {
        "jsCode": "// Coresignal search endpoints return a bare JSON array of IDs. The Coresignal node\n// passes each array element through as its own item, so an ID arrives as a raw\n// number in item.json. Normalise every plausible shape.\nfunction collectIds(items) {\n  const ids = [];\n  for (const item of items) {\n    const j = item.json;\n    if (j === null || j === undefined) continue;\n    if (typeof j === 'number' || typeof j === 'string') { ids.push(j); continue; }\n    if (Array.isArray(j)) { ids.push(...j); continue; }\n    if (Array.isArray(j.data)) { ids.push(...j.data); continue; }\n    if (j.id !== undefined) { ids.push(j.id); continue; }\n    const vals = Object.values(j);\n    if (vals.length === 1 && (typeof vals[0] === 'number' || typeof vals[0] === 'string')) ids.push(vals[0]);\n  }\n  return ids.filter(v => v !== null && v !== undefined && v !== '');\n}\n\nconst ids = collectIds($input.all());\n\n// Batch size is 1, so this is the job currently being processed.\nconst ctx = $('Loop Over Job Listings').first().json;\n\nif (ids.length === 0) {\n  throw new Error('Coresignal has no multi-source job matching LinkedIn job ID ' + ctx.linkedin_job_id + '. It may be too new, or not indexed from that source.');\n}\n\nreturn [{ json: { coresignal_job_id: ids[0], match_count: ids.length, ...ctx } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "45a18997-2a32-4fc3-8f49-b780d21b9005",
      "name": "Retrieve Job Data from Coresignal",
      "type": "n8n-nodes-coresignal-api.coresignal",
      "position": [
        7056,
        2272
      ],
      "parameters": {
        "jobId": "={{ $json.coresignal_job_id }}",
        "resource": "job"
      },
      "credentials": {
        "coresignalApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1782c3e6-b066-4eb2-ae1e-60c49cf53816",
      "name": "Claude Model for Language Processing",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        7392,
        2480
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-5",
          "cachedResultName": "claude-sonnet-4-5"
        },
        "options": {
          "temperature": 0,
          "maxTokensToSample": 2048
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "7481bd4c-9a4e-4d0a-b098-b9aa361200d4",
      "name": "Parse AI Output Filters",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        7568,
        2496
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"job_titles\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"3-6 realistic current job titles a strong candidate would hold today. Plain titles only.\"\n    },\n    \"must_have_skills\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"3-8 hard skills, tools or technologies explicitly required by the ad. Single terms, lowercase.\"\n    },\n    \"nice_to_have_skills\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"0-8 preferred-but-optional skills.\"\n    },\n    \"countries\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"Full English country names where the candidate should be located, e.g. 'United States', 'Germany'. Empty array if the role is fully remote and location-agnostic.\"\n    },\n    \"min_years_experience\": {\n      \"type\": \"number\",\n      \"description\": \"Minimum total years of professional experience. Use 0 if the ad implies entry level.\"\n    },\n    \"seniority\": {\n      \"type\": \"string\",\n      \"description\": \"One of: Internship, Entry level, Associate, Mid-Senior level, Director, Executive\"\n    },\n    \"search_summary\": {\n      \"type\": \"string\",\n      \"description\": \"One sentence describing the ideal candidate.\"\n    }\n  },\n  \"required\": [\"job_titles\", \"must_have_skills\", \"nice_to_have_skills\", \"countries\", \"min_years_experience\", \"seniority\", \"search_summary\"]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "1b200e30-a531-4f22-8428-ad5b445c79b6",
      "name": "AI Extract Search Criteria",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        7296,
        2272
      ],
      "parameters": {
        "text": "=You are a technical recruiter turning a job ad into search filters for a people-search database.\n\nJOB AD\nTitle: {{ $json.title }}\nCompany: {{ $json.company_name }}\nLocation: {{ $json.location }} ({{ $json.country }})\nSeniority (as posted): {{ $json.seniority }}\nEmployment type: {{ $json.employment_type }}\nDepartment: {{ $json.department }}\nRemote allowed: {{ $json.accepts_remote }}\n\nDescription:\n{{ String($json.description || '').slice(0, 12000) }}\n\nRULES\n- Base every filter on what the ad actually says. Do not invent requirements.\n- job_titles must be titles people put on their own profile, not the ad's marketing headline.\n- must_have_skills should be searchable single terms (e.g. \"kubernetes\", \"python\", \"salesforce\"), not sentences.\n- If the ad gives no country and is fully remote, return an empty countries array.\n- If the ad states a years-of-experience minimum, use it. Otherwise infer conservatively from seniority.",
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.5
    },
    {
      "id": "ed55a52a-f455-4b20-a9df-f996a7e7e51e",
      "name": "Build Employee Search Query",
      "type": "n8n-nodes-base.code",
      "position": [
        7696,
        2272
      ],
      "parameters": {
        "jsCode": "// Turn the AI-extracted filters into a Coresignal Multi-source Employee ES DSL query.\nconst raw = $input.first().json;\nconst f = raw.output || raw.data || raw;\n\nconst titles = (f.job_titles || []).filter(Boolean).slice(0, 6);\nconst must = (f.must_have_skills || []).filter(Boolean).slice(0, 8);\nconst nice = (f.nice_to_have_skills || []).filter(Boolean).slice(0, 8);\nconst countries = (f.countries || []).filter(Boolean);\nconst minMonths = Math.max(0, Math.round(Number(f.min_years_experience || 0) * 12));\n\nif (titles.length === 0) {\n  throw new Error('AI returned no job_titles \u2014 cannot build an employee search.');\n}\n\n// Hard filters\nconst filter = [\n  { term: { is_deleted: 0 } },\n  { term: { is_working: 1 } }\n];\nif (minMonths > 0) {\n  filter.push({ range: { total_experience_duration_months: { gte: minMonths } } });\n}\nif (countries.length > 0) {\n  filter.push({ terms: { location_country: countries } });\n}\n\n// At least one title variant must match the person's current role.\nconst titleShould = titles.map(t => ({ match_phrase: { active_experience_title: t } }));\n\n// Skills only boost relevance. Must-haves are weighted higher than nice-to-haves.\nconst skillShould = [];\nfor (const s of must) {\n  skillShould.push({ match: { inferred_skills: { query: s, operator: 'and', boost: 3 } } });\n  skillShould.push({ match: { historical_skills: { query: s, operator: 'and', boost: 2 } } });\n}\nfor (const s of nice) {\n  skillShould.push({ match: { inferred_skills: { query: s, operator: 'and', boost: 1 } } });\n}\n\nconst esQuery = {\n  query: {\n    bool: {\n      filter,\n      must: [{ bool: { should: titleShould, minimum_should_match: 1 } }],\n      should: skillShould\n    }\n  },\n  sort: ['_score']\n};\n\nconst job = $('Retrieve Job Data from Coresignal').first().json;\nconst ctx = $('Extract Job ID').first().json;\n\nreturn [{\n  json: {\n    es_query: esQuery,\n    filters: f,\n    job_title: job.title,\n    job_company: job.company_name,\n    job_location: job.location,\n    job_url: ctx.job_url,\n    slack_channel_id: ctx.slack_channel_id,\n    slack_user_id: ctx.slack_user_id,\n    candidates_per_job: ctx.candidates_per_job || 5\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "47822cfa-6177-4979-a543-a56539e59400",
      "name": "Search Employees in Coresignal",
      "type": "n8n-nodes-coresignal-api.coresignal",
      "position": [
        7920,
        2272
      ],
      "parameters": {
        "esQuery": "={{ JSON.stringify($json.es_query) }}",
        "operation": "search_es_dsl"
      },
      "credentials": {
        "coresignalApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5377fe45-8e3c-47bd-910f-ee63dbef4826",
      "name": "Select Top Employee IDs",
      "type": "n8n-nodes-base.code",
      "position": [
        8128,
        2272
      ],
      "parameters": {
        "jsCode": "// Normalise the ID array and keep the top N (search returns best-scoring first).\nfunction collectIds(items) {\n  const ids = [];\n  for (const item of items) {\n    const j = item.json;\n    if (j === null || j === undefined) continue;\n    if (typeof j === 'number' || typeof j === 'string') { ids.push(j); continue; }\n    if (Array.isArray(j)) { ids.push(...j); continue; }\n    if (Array.isArray(j.data)) { ids.push(...j.data); continue; }\n    if (j.id !== undefined) { ids.push(j.id); continue; }\n    const vals = Object.values(j);\n    if (vals.length === 1 && (typeof vals[0] === 'number' || typeof vals[0] === 'string')) ids.push(vals[0]);\n  }\n  return ids.filter(v => v !== null && v !== undefined && v !== '');\n}\n\nconst ids = collectIds($input.all());\nconst wanted = Number($('Build Employee Search Query').first().json.candidates_per_job || 5);\n\nif (ids.length === 0) {\n  throw new Error('Employee search returned no matches. Loosen the filters (drop the country filter or the experience minimum).');\n}\n\nreturn ids.slice(0, wanted).map((id, i) => ({ json: { employee_id: id, rank: i + 1 } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "bf84c1ce-6b78-4a94-a013-8809d2f72b5d",
      "name": "Fetch Employee Details",
      "type": "n8n-nodes-coresignal-api.coresignal",
      "position": [
        8352,
        2272
      ],
      "parameters": {
        "employeeId": "={{ $json.employee_id }}"
      },
      "credentials": {
        "coresignalApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "473180c3-2c2b-4b2f-b1d1-1b0dedbde3cb",
      "name": "Format Employees Shortlist",
      "type": "n8n-nodes-base.code",
      "position": [
        8560,
        2272
      ],
      "parameters": {
        "jsCode": "// Build one Slack mrkdwn message: profile link, short description, skills,\n// current title and the last 3 roles for each candidate.\nconst meta = $('Build Employee Search Query').first().json;\n\nfunction truncate(s, n) {\n  s = String(s || '').replace(/\\s+/g, ' ').trim();\n  return s.length > n ? s.slice(0, n - 1).trimEnd() + '\u2026' : s;\n}\n\nfunction fmtDate(exp, which) {\n  const y = exp[which + '_year'];\n  const m = exp[which + '_month'];\n  if (!y) return which === 'date_to' ? 'Present' : '?';\n  return m ? String(m).padStart(2, '0') + '/' + y : String(y);\n}\n\nfunction lastThree(experience) {\n  const list = Array.isArray(experience) ? experience.slice() : [];\n  list.sort((a, b) => {\n    const av = (a.date_from_year || 0) * 100 + (a.date_from_month || 0);\n    const bv = (b.date_from_year || 0) * 100 + (b.date_from_month || 0);\n    if (bv !== av) return bv - av;\n    return (a.order_in_profile || 99) - (b.order_in_profile || 99);\n  });\n  return list.slice(0, 3);\n}\n\nconst wanted = [\n  ...(meta.filters?.must_have_skills || []),\n  ...(meta.filters?.nice_to_have_skills || [])\n].map(s => String(s).toLowerCase());\n\nconst blocks = [];\nlet rank = 0;\n\nfor (const item of $input.all()) {\n  const p = item.json || {};\n  rank += 1;\n\n  const name = p.full_name || 'Unknown';\n  const url = p.linkedin_url\n    ? (p.linkedin_url.startsWith('http') ? p.linkedin_url : 'https://' + p.linkedin_url)\n    : null;\n  const title = p.active_experience_title || String(p.headline || '').split(' at ')[0] || '\u2014';\n  const location = p.location_full || p.location_country || '';\n  const about = truncate(p.summary || p.headline || '', 260);\n\n  // inferred_skills is a long, alphabetical, noisy list. Surface the ones that\n  // actually match the job's requirements first, then pad with the rest.\n  const all = (p.inferred_skills || []).map(s => String(s).toLowerCase());\n  const matched = all.filter(s => wanted.some(w => s === w || s.includes(w) || w.includes(s)));\n  const rest = all.filter(s => !matched.includes(s) && s.length > 3);\n  const shown = [...new Set([...matched, ...rest])].slice(0, 12);\n  const skills = shown.length\n    ? shown.map(s => matched.includes(s) ? '*' + s + '*' : s).join(', ')\n    : '\u2014';\n\n  const expLines = lastThree(p.experience).map(e =>\n    '   \u2022 *' + (e.position_title || '\u2014') + '* \u2014 ' + (e.company_name || '\u2014') +\n    ' (' + fmtDate(e, 'date_from') + ' \u2192 ' + (e.active_experience ? 'Present' : fmtDate(e, 'date_to')) + ')'\n  );\n\n  const lines = [];\n  lines.push('*' + rank + '. ' + (url ? '<' + url + '|' + name + '>' : name) + '*');\n  lines.push('   ' + title + (location ? ' \u00b7 ' + location : ''));\n  if (about) lines.push('   _' + about + '_');\n  lines.push('   *Skills:* ' + skills);\n  if (expLines.length) {\n    lines.push('   *Experience:*');\n    lines.push(expLines.join('\\n'));\n  }\n  blocks.push(lines.join('\\n'));\n}\n\nconst header = [\n  '<@' + meta.slack_user_id + '> *' + (meta.filters?.search_summary || 'Candidate shortlist') + '*',\n  ':briefcase: <' + meta.job_url + '|' + (meta.job_title || 'Job ad') + '> \u00b7 ' + (meta.job_company || '') + ' \u00b7 ' + (meta.job_location || ''),\n  ':mag: Titles: ' + (meta.filters?.job_titles || []).join(', '),\n  ':wrench: Must-have skills: ' + (meta.filters?.must_have_skills || []).join(', '),\n  ':round_pushpin: ' + ((meta.filters?.countries || []).join(', ') || 'Any location') + ' \u00b7 ' + (meta.filters?.min_years_experience || 0) + '+ yrs \u00b7 ' + (meta.filters?.seniority || '')\n].join('\\n');\n\nreturn [{\n  json: {\n    slack_channel_id: meta.slack_channel_id,\n    candidate_count: blocks.length,\n    message: header + '\\n\\n' + blocks.join('\\n\\n')\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "0981550c-101b-4d8f-aecc-40688d8277e7",
      "name": "Post Employee Shortlist to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        8768,
        2272
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.slack_channel_id }}"
        },
        "otherOptions": {
          "mrkdwn": true,
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        },
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "c5b25c1f-d5ef-42dd-b76e-0c6ba60c22b3",
  "nodeGroups": [],
  "connections": {
    "Extract Job ID": {
      "main": [
        [
          {
            "node": "Retrieve Job Data from Coresignal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deduplicate Job Ads": {
      "main": [
        [
          {
            "node": "Loop Over Job Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Employee Details": {
      "main": [
        [
          {
            "node": "Format Employees Shortlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Job Listings": {
      "main": [
        [],
        [
          {
            "node": "Fetch Job Details from Coresignal",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post Acknowledgement on Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Output Filters": {
      "ai_outputParser": [
        [
          {
            "node": "AI Extract Search Criteria",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Select Top Employee IDs": {
      "main": [
        [
          {
            "node": "Fetch Employee Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Job Candidates Limit": {
      "main": [
        [
          {
            "node": "Extract Job URL from Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Extract Search Criteria": {
      "main": [
        [
          {
            "node": "Build Employee Search Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Employees Shortlist": {
      "main": [
        [
          {
            "node": "Post Employee Shortlist to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Employee Search Query": {
      "main": [
        [
          {
            "node": "Search Employees in Coresignal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Job Ad Posted on Slack": {
      "main": [
        [
          {
            "node": "Set Job Candidates Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Job URL from Message": {
      "main": [
        [
          {
            "node": "Deduplicate Job Ads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Employees in Coresignal": {
      "main": [
        [
          {
            "node": "Select Top Employee IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Employee Shortlist to Slack": {
      "main": [
        [
          {
            "node": "Loop Over Job Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Job Details from Coresignal": {
      "main": [
        [
          {
            "node": "Extract Job ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Job Data from Coresignal": {
      "main": [
        [
          {
            "node": "AI Extract Search Criteria",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Model for Language Processing": {
      "ai_languageModel": [
        [
          {
            "node": "AI Extract Search Criteria",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}