AutomationFlowsAI & RAG › Score Linkedin Job Matches with Openai, Google Sheets, Slack, and Apify

Score Linkedin Job Matches with Openai, Google Sheets, Slack, and Apify

ByHanna Nosova @fetch-cat on n8n.io

Runs fetch_cat/linkedin-jobs-scraper daily to collect recent LinkedIn jobs, scores each listing against your candidate profile with OpenAI, saves qualified matches to Google Sheets, and sends the five strongest results to Slack without redelivering previously processed job IDs.…

Event trigger★★★★☆ complexityAI-powered28 nodesData TableHTTP RequestOpenAIGoogle SheetsSlack
AI & RAG Trigger: Event Nodes: 28 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #17019 — we link there as the canonical source.

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": "LinkedIn Job Match Digest",
  "nodes": [
    {
      "id": "10000000-0000-0000-4000-8000-000000000001",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1520,
        80
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000002",
      "name": "Daily Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1520,
        -100
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 12,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000020",
      "name": "Ensure Delivery Ledger",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -1280,
        0
      ],
      "parameters": {
        "columns": {
          "column": [
            {
              "name": "workflowSlug",
              "type": "string"
            },
            {
              "name": "itemKey",
              "type": "string"
            },
            {
              "name": "destination",
              "type": "string"
            },
            {
              "name": "deliveredAt",
              "type": "date"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        },
        "resource": "table",
        "operation": "create",
        "tableName": "FetchCat Delivery Ledger"
      },
      "typeVersion": 1.1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000021",
      "name": "1. Set Your Job Search",
      "type": "n8n-nodes-base.set",
      "position": [
        -1040,
        0
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "linkedin-keywords",
              "name": "keywords",
              "type": "string",
              "value": "automation engineer, workflow automation"
            },
            {
              "id": "linkedin-location",
              "name": "location",
              "type": "string",
              "value": "Remote"
            },
            {
              "id": "linkedin-profile",
              "name": "candidateProfile",
              "type": "string",
              "value": "Senior automation engineer with n8n, JavaScript, APIs, and data pipeline experience."
            },
            {
              "id": "linkedin-score",
              "name": "minimumScore",
              "type": "number",
              "value": 70
            },
            {
              "id": "linkedin-limit",
              "name": "maxItems",
              "type": "number",
              "value": 10
            }
          ]
        },
        "duplicateItem": false
      },
      "typeVersion": 3.4
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000004",
      "name": "Build Actor Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -80,
        0
      ],
      "parameters": {
        "jsCode": "const config = $input.first()?.json;\nif (!config) throw new Error('Configure the 1. Set Your Job Search node.');\nconst keywords = String(config.keywords || '').split(',').map((value) => value.trim()).filter(Boolean);\nif (keywords.length === 0) throw new Error('Configure at least one comma-separated job keyword.');\nconst candidateProfile = String(config.candidateProfile || '').trim();\nif (candidateProfile.length < 20) throw new Error('Candidate profile must be at least 20 characters.');\nconst minimumScore = Math.max(0, Math.min(Number(config.minimumScore) || 70, 100));\nconst maxItems = Math.max(1, Math.min(Number(config.maxItems) || 10, 10));\nreturn [{ json: { config: { candidateProfile, minimumScore }, actorInput: {\n  keywords,\n  location: String(config.location || 'Remote'),\n  maxItems,\n  includeDetails: true,\n  datePosted: 'past24h',\n  sortBy: 'recent'\n} } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000005",
      "name": "2. Find Recent LinkedIn Jobs",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        160,
        0
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/0XhGPLTjZjicBXYV5/runs",
        "method": "POST",
        "options": {
          "timeout": 310000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ $json.actorInput }}",
        "sendBody": true,
        "sendQuery": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "300"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept-Encoding",
              "value": "identity"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000033",
      "name": "Get LinkedIn Job Results",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        400,
        0
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.data.defaultDatasetId }}/items",
        "method": "GET",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "clean",
              "value": "true"
            },
            {
              "name": "limit",
              "value": "10"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000006",
      "name": "Normalize and Cap Jobs",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        0
      ],
      "parameters": {
        "jsCode": "const rawJobs = $input.all().flatMap((item) => {\n  let payload = item.json?.data ?? item.json;\n  if (typeof payload === 'string') {\n    try { payload = JSON.parse(payload); } catch { throw new Error('Apify returned invalid JSON.'); }\n  }\n  return Array.isArray(payload) ? payload : [payload];\n}).slice(0, 10);\nconst normalized = [];\nfor (const job of rawJobs) {\n  const title = String(job.title || '').trim();\n  const invalidTitles = new Set(['delete account', 'sign in', 'join now', 'linkedin']);\n  if (!job.jobId || !title || !job.jobUrl || invalidTitles.has(title.toLowerCase())) continue;\n  normalized.push({ json: {\n    jobId: String(job.jobId),\n    title,\n    companyName: String(job.companyName || 'Unknown company'),\n    location: String(job.location || 'Not specified'),\n    postedAtText: String(job.postedAtText || ''),\n    scrapedAt: String(job.scrapedAt || ''),\n    jobUrl: String(job.jobUrl),\n    description: String(job.description || '').slice(0, 12000),\n    employmentType: String(job.employmentType || ''),\n    seniorityLevel: String(job.seniorityLevel || ''),\n    applicantsText: String(job.applicantsText || '')\n  } });\n}\nreturn normalized;"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000007",
      "name": "Keep Undelivered Jobs",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        880,
        0
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "workflowSlug",
              "keyValue": "linkedin-job-match-digest",
              "condition": "eq"
            },
            {
              "keyName": "itemKey",
              "keyValue": "={{ $json.jobId }}",
              "condition": "eq"
            }
          ]
        },
        "resource": "row",
        "matchType": "allConditions",
        "operation": "rowNotExists",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "FetchCat Delivery Ledger"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000008",
      "name": "Build Job Batch",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        0
      ],
      "parameters": {
        "jsCode": "const jobs = $input.all().map((item) => item.json);\nif (jobs.length === 0) return [];\nreturn [{ json: { jobs, jobIds: jobs.map((job) => job.jobId) } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000009",
      "name": "3. Score Jobs Against Your Profile",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1360,
        0
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "gpt-5.4-mini"
        },
        "options": {
          "store": false,
          "maxTokens": 4000,
          "reasoning": {
            "reasoningOptions": {
              "effort": "low",
              "summary": "none"
            }
          },
          "textFormat": {
            "textOptions": {
              "name": "linkedin_job_fit_batch",
              "type": "json_schema",
              "schema": "{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"results\"],\"properties\":{\"results\":{\"type\":\"array\",\"minItems\":1,\"maxItems\":10,\"items\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"jobId\",\"score\",\"reason\"],\"properties\":{\"jobId\":{\"type\":\"string\",\"minLength\":1},\"score\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100},\"reason\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":500}}}}}}",
              "strict": true,
              "verbosity": "low",
              "description": "Score every job from 0 to 100 against the candidate profile. Return exactly one result for every supplied jobId and no others. Explain the fit in concise, natural English. Return the strict schema."
            }
          },
          "instructions": "Score every job from 0 to 100 against the candidate profile. Return exactly one result for every supplied jobId and no others. Explain the fit in concise, natural English. Return the strict schema."
        },
        "resource": "text",
        "simplify": true,
        "operation": "response",
        "responses": {
          "values": [
            {
              "role": "user",
              "type": "text",
              "content": "=Candidate profile:\n{{ $(\"Build Actor Input\").first().json.config.candidateProfile }}\n\nMinimum score: {{ $(\"Build Actor Input\").first().json.config.minimumScore }}\n\nEvaluate every job exactly once and preserve each jobId:\n{{ JSON.stringify($json.jobs) }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000010",
      "name": "Validate Job Batch",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        0
      ],
      "parameters": {
        "jsCode": "\nfunction parseStructured(root, requiredKeys) {\n  const seen = new Set();\n  function visit(value) {\n    if (value === null || value === undefined) return null;\n    if (typeof value === 'string') {\n      const text = value.trim().replace(/^\\x60\\x60\\x60(?:json)?\\s*/i, '').replace(/\\s*\\x60\\x60\\x60$/, '');\n      if (!text.startsWith('{')) return null;\n      try { return visit(JSON.parse(text)); } catch { return null; }\n    }\n    if (typeof value !== 'object' || seen.has(value)) return null;\n    seen.add(value);\n    if (requiredKeys.every((key) => Object.prototype.hasOwnProperty.call(value, key))) return value;\n    const preferred = ['output_text', 'outputText', 'text', 'content', 'output', 'message', 'response', 'data'];\n    for (const key of preferred) {\n      if (Object.prototype.hasOwnProperty.call(value, key)) {\n        const found = visit(value[key]);\n        if (found) return found;\n      }\n    }\n    for (const nested of Array.isArray(value) ? value : Object.values(value)) {\n      const found = visit(nested);\n      if (found) return found;\n    }\n    return null;\n  }\n  return visit(root);\n}\n\nconst batch = $(\"Build Job Batch\").first().json;\nconst parsed = parseStructured($input.first().json, ['results']);\nif (!parsed || !Array.isArray(parsed.results)) throw new Error('OpenAI returned an invalid LinkedIn batch.');\nconst expectedIds = new Set(batch.jobIds);\nconst actualIds = parsed.results.map((result) => String(result.jobId));\nif (actualIds.length !== expectedIds.size || new Set(actualIds).size !== actualIds.length || actualIds.some((value) => !expectedIds.has(value))) throw new Error('OpenAI result IDs do not exactly match the LinkedIn input batch.');\nconst minimumScore = Number($(\"Build Actor Input\").first().json.config.minimumScore);\nconst byId = new Map(batch.jobs.map((job) => [job.jobId, job]));\nconst sheetsEpochOffset = 25569;\nconst toSheetsSerial = (date) => date.getTime() / 86400000 + sheetsEpochOffset;\nconst parsePostedAt = (text, reference) => {\n  const value = String(text || '').trim().toLowerCase();\n  const date = new Date(reference);\n  if (!Number.isFinite(date.getTime())) return null;\n  const match = value.match(/(\\d+)\\s+(minute|hour|day|week|month)s?\\s+ago/);\n  if (match) {\n    const unitDays = { minute: 1 / 1440, hour: 1 / 24, day: 1, week: 7, month: 30 };\n    date.setTime(date.getTime() - Number(match[1]) * unitDays[match[2]] * 86400000);\n  } else if (!value.includes('today') && !value.includes('just now')) {\n    const absolute = new Date(text);\n    if (!Number.isFinite(absolute.getTime())) return null;\n    date.setTime(absolute.getTime());\n  }\n  return toSheetsSerial(date);\n};\nconst qualifiedJobs = [];\nfor (const result of parsed.results) {\n  if (!Number.isInteger(result.score) || result.score < 0 || result.score > 100 || typeof result.reason !== 'string' || !result.reason.trim()) throw new Error('OpenAI returned a malformed LinkedIn result.');\n  if (result.score < minimumScore) continue;\n  const job = byId.get(String(result.jobId));\n  const collected = new Date(job.scrapedAt || Date.now());\n  const url = String(job.jobUrl);\n  qualifiedJobs.push({ title: job.title, company: job.companyName, location: job.location, postedAt: parsePostedAt(job.postedAtText, collected), postedRelative: job.postedAtText, jobLink: '=HYPERLINK(\"' + url.replace(/\"/g, '\"\"') + '\",\"Open job\")', url, score: result.score, reason: result.reason.trim(), collectedAt: toSheetsSerial(collected), linkedInJobId: job.jobId });\n}\nreturn [{ json: { allKeys: batch.jobIds, qualifiedJobs } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000011",
      "name": "Has Qualified Jobs",
      "type": "n8n-nodes-base.if",
      "position": [
        1840,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890+1234567890",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.qualifiedJobs.length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000012",
      "name": "Build Delivery Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        -100
      ],
      "parameters": {
        "jsCode": "const qualifiedJobs = $('Validate Job Batch').first().json.qualifiedJobs;\nconst jobs = [...qualifiedJobs].sort((a, b) => b.score - a.score).slice(0, 5);\nif (jobs.length === 0) return [];\nconst slackLines = jobs.map((job, index) => (index + 1) + '. *' + job.title + '* - ' + job.company + '\\nScore: ' + job.score + '/100 | Location: ' + job.location + ' | Posted: ' + job.postedRelative + '\\nWhy it matches: ' + job.reason + '\\n<' + job.url + '|View job on LinkedIn>');\nreturn [{ json: {\n  qualifiedJobs,\n  slackMessage: '*LinkedIn Job Match Digest*\\n\\n' + slackLines.join('\\n\\n')\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000026",
      "name": "Expand Jobs for Sheets",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        -100
      ],
      "parameters": {
        "jsCode": "return $json.qualifiedJobs.map((job) => ({ json: job }));"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000013",
      "name": "4. Save Matches to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2560,
        -100
      ],
      "parameters": {
        "columns": {
          "value": {
            "Job": "={{ $json.jobLink }}",
            "Company": "={{ $json.company }}",
            "Added at": "={{ $json.collectedAt }}",
            "Location": "={{ $json.location }}",
            "Job title": "={{ $json.title }}",
            "Posted at": "={{ $json.postedAt }}",
            "Match score": "={{ $json.score }}",
            "Why it matches": "={{ $json.reason }}",
            "LinkedIn job ID": "={{ $json.linkedInJobId }}"
          },
          "schema": [
            {
              "id": "Job title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Job title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Company",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Posted at",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Posted at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Job",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Match score",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Match score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Why it matches",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Why it matches",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Added at",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Added at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "LinkedIn job ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "LinkedIn job ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "LinkedIn job ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "useAppend": true
        },
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "0",
          "cachedResultName": "Jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "+1234567890+1234567890+1234567890"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000032",
      "name": "Continue After Sheets",
      "type": "n8n-nodes-base.code",
      "position": [
        2800,
        -100
      ],
      "parameters": {
        "jsCode": "return [{ json: $('Build Delivery Payload').first().json }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000015",
      "name": "5. Send Top Matches to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        3040,
        -100
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C+1234567890"
        },
        "operation": "post",
        "messageType": "text",
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000016",
      "name": "Prepare Delivery Ledger",
      "type": "n8n-nodes-base.code",
      "position": [
        3280,
        -100
      ],
      "parameters": {
        "jsCode": "return $('Validate Job Batch').first().json.allKeys.map((itemKey) => ({ json: { workflowSlug: 'linkedin-job-match-digest', itemKey } }));"
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000017",
      "name": "Commit Delivered Jobs",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        3520,
        -100
      ],
      "parameters": {
        "columns": {
          "value": {
            "itemKey": "={{ $json.itemKey }}",
            "deliveredAt": "={{ $now.toISO() }}",
            "destination": "google-sheets-and-slack",
            "workflowSlug": "={{ $json.workflowSlug }}"
          },
          "schema": [
            {
              "id": "workflowSlug",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "workflowSlug",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "itemKey",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "itemKey",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "destination",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "destination",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "deliveredAt",
              "type": "dateTime",
              "display": true,
              "required": false,
              "displayName": "deliveredAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "resource": "row",
        "operation": "insert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "FetchCat Delivery Ledger"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000018",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2128,
        -256
      ],
      "parameters": {
        "color": 1,
        "width": 480,
        "height": 896,
        "content": "## LinkedIn Job Match Digest\n\n### How it works\n\n1. Starts manually or every day at noon and creates the delivery ledger if needed.\n2. Runs FetchCat LinkedIn Jobs Scraper for up to 10 jobs posted in the past 24 hours.\n3. Skips previously delivered LinkedIn job IDs and scores the remaining jobs in one OpenAI request.\n4. Saves qualified matches to Google Sheets and sends the five strongest matches to Slack.\n5. Records IDs only after both destinations succeed, keeping failed deliveries retryable.\n\n### Setup steps\n\n- [ ] Add `fetch_cat/linkedin-jobs-scraper` to your Apify account if required.\n- [ ] Create HTTP Header Auth with `Authorization: Bearer YOUR_APIFY_TOKEN` and select it in both FetchCat request nodes.\n- [ ] Connect OpenAI in 3. Score Jobs Against Your Profile.\n- [ ] Create a Jobs sheet with the documented headers, then select it in 4. Save Matches to Google Sheets.\n- [ ] Connect Slack and choose the digest channel in 5. Send Top Matches to Slack.\n- [ ] Edit keywords, location, candidate profile, threshold, and item limit in 1. Set Your Job Search.\n\n### Customization\n\nAdjust the daily schedule, search settings, score threshold, Slack message, or Google Sheets fields. Keep the item limit at 10 for the included cost controls."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000019",
      "name": "Start and ledger setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 496,
        "content": "## Start and ledger setup\n\nStarts manually or at noon and creates the delivery ledger used to prevent repeated alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000030",
      "name": "Configure job search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 1632,
        "height": 272,
        "content": "## Configure job search\n\nReads your search settings, builds the FetchCat Actor input, runs `fetch_cat/linkedin-jobs-scraper`, and downloads its dataset."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000031",
      "name": "Filter and batch jobs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 304,
        "content": "## Filter and batch jobs\n\nCleans returned jobs, skips LinkedIn job IDs already in the delivery ledger, and prepares one AI batch."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000034",
      "name": "Score qualified matches",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 304,
        "content": "## Score qualified matches\n\nScores every job against the candidate profile, validates the structured response, and applies the configured threshold."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000035",
      "name": "Prepare sheet output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 304,
        "content": "## Prepare sheet output\n\nBuilds the ranked digest and writes qualified jobs to Google Sheets using LinkedIn job ID as the unique key."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000036",
      "name": "Send Slack digest",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2752,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 304,
        "content": "## Send Slack digest\n\nContinues only after the Sheet write succeeds and posts the five strongest matches in one Slack message."
      },
      "typeVersion": 1
    },
    {
      "id": "10000000-0000-0000-4000-8000-000000000037",
      "name": "Commit delivery status",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3232,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 320,
        "content": "## Commit delivery status\n\nRecords evaluated job IDs only after Sheets and Slack succeed, so interrupted deliveries remain retryable."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "Europe/Lisbon",
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "connections": {
    "Daily Schedule": {
      "main": [
        [
          {
            "node": "Ensure Delivery Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Ensure Delivery Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Job Batch": {
      "main": [
        [
          {
            "node": "3. Score Jobs Against Your Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Actor Input": {
      "main": [
        [
          {
            "node": "2. Find Recent LinkedIn Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Qualified Jobs": {
      "main": [
        [
          {
            "node": "Build Delivery Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Job Batch": {
      "main": [
        [
          {
            "node": "Has Qualified Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Continue After Sheets": {
      "main": [
        [
          {
            "node": "5. Send Top Matches to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Undelivered Jobs": {
      "main": [
        [
          {
            "node": "Build Job Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Set Your Job Search": {
      "main": [
        [
          {
            "node": "Build Actor Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Delivery Payload": {
      "main": [
        [
          {
            "node": "Expand Jobs for Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ensure Delivery Ledger": {
      "main": [
        [
          {
            "node": "1. Set Your Job Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Jobs for Sheets": {
      "main": [
        [
          {
            "node": "4. Save Matches to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize and Cap Jobs": {
      "main": [
        [
          {
            "node": "Keep Undelivered Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Delivery Ledger": {
      "main": [
        [
          {
            "node": "Commit Delivered Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get LinkedIn Job Results": {
      "main": [
        [
          {
            "node": "Normalize and Cap Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Find Recent LinkedIn Jobs": {
      "main": [
        [
          {
            "node": "Get LinkedIn Job Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Send Top Matches to Slack": {
      "main": [
        [
          {
            "node": "Prepare Delivery Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Save Matches to Google Sheets": {
      "main": [
        [
          {
            "node": "Continue After Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Score Jobs Against Your Profile": {
      "main": [
        [
          {
            "node": "Validate Job Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

Runs fetch_cat/linkedin-jobs-scraper daily to collect recent LinkedIn jobs, scores each listing against your candidate profile with OpenAI, saves qualified matches to Google Sheets, and sends the five strongest results to Slack without redelivering previously processed job IDs.…

Source: https://n8n.io/workflows/17019/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Overview

Gmail Trigger, Google Drive, OpenAI +4
AI & RAG

This workflow triggers on HubSpot dealstage changes, pulls full deal, contact, and owner details, uses OpenAI to generate a concise Slack-ready update with next steps, notifies the right Slack channel

HubSpot Trigger, HubSpot, OpenAI +4
AI & RAG

How This Works This automation automatically scrapes leads from Apollo using the Apify scraper, filters out those who do not have an Email or URL included, scrapes the leads' website content and write

OpenAI, Google Sheets, HTTP Request +1
AI & RAG

Automate your entire invoice processing pipeline with AI-powered OCR, validation, and approval workflows 📄🤖. This n8n automation monitors incoming Gmail invoices, extracts structured data using OCR an

Gmail Trigger, Gmail, OpenAI +3
AI & RAG

Consultants, agencies, freelancers, and professional service firms who need to create customized proposals and contracts quickly and efficiently.

Google Sheets Trigger, OpenAI, Google Docs +5