{
  "id": "10dailyjobmatchdiges",
  "name": "Get a daily AI job-match digest scored against your profile from 6+ free job boards",
  "tags": [],
  "nodes": [
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        0
      ],
      "parameters": {
        "width": 632,
        "height": 680,
        "content": "## Daily job-match digest \u2192 email\n\nFor any job seeker in any field. Pull fresh roles from many free job boards, filter them your way, score every match against YOUR profile with AI, and get a ranked shortlist emailed each morning.\n\n### How it works\nA daily schedule fetches openings from RemoteOK, Remotive, Arbeitnow, Jobicy, Himalayas and TheMuse (all free, no API key), plus Adzuna if you add keys. A Code node normalises every board into one shape and applies your filters, then de-duplicates across boards. An AI Agent reads your resume and preferences and scores each role 0 to 100 on real fit, writing a one-line why-it-fits, a risk flag, and a tailored first-person pitch. The workflow ranks the matches, drops anything below your threshold, and emails a clean digest showing which board each came from.\n\n### Setup (all in the config node)\n- About you: profile, headline, your full resume, and preferences (deal-breakers and must-haves). The Agent scores against all of these.\n- Filters: position (the role title to search), tags (skills that must appear), locations, remoteOnly (yes/no), minSalary, and excludeTerms (titles to skip, e.g. junior, intern).\n- Output: minScore, how many to send, and the email address.\n- Credentials in the canvas: add an Anthropic credential on the chat model node and pick a model; add Gmail on the send node. Optional: Adzuna app id and key for extra breadth across countries.\n\n### Customization\nChange position to any role in any field, tune the filters, add or remove boards in the expand node, or swap the email for Slack or a Google Sheet to track applications.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation and AI workflows. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "section-fetch-boards",
      "name": "Section: Fetch boards",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 664,
        "height": 548,
        "content": "## 1. Fetch many boards\nOne request per board (RemoteOK, Remotive, Arbeitnow, Jobicy, Himalayas, TheMuse + optional Adzuna). The normalise node merges them into one shape, keyword-filters and de-duplicates across boards."
      },
      "typeVersion": 1
    },
    {
      "id": "section-score-with-ai",
      "name": "Section: Score with AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 548,
        "content": "## 2. Score with an AI Agent\nThe Agent reads your resume + preferences and scores every role 0-100 with a fit line, risk flag and a tailored pitch. Add your Anthropic credential on the chat model node and pick a model."
      },
      "typeVersion": 1
    },
    {
      "id": "section-rank-send",
      "name": "Section: Rank & send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 1064,
        "height": 724,
        "content": "## 3. Rank & email\nKeep only jobs at or above your minimum score, rank them, and email the digest showing each role's source board."
      },
      "typeVersion": 1
    },
    {
      "id": "every-morning",
      "name": "Every morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -336,
        528
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "set-config-your-profile-search",
      "name": "Set config: your profile & search",
      "type": "n8n-nodes-base.set",
      "position": [
        48,
        528
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "profile",
              "type": "string",
              "value": "A senior individual contributor with ~8 years in the field, strong on the core craft, metrics and cross-functional work. Describe yourself here; the AI scores each role against this."
            },
            {
              "id": "a2",
              "name": "headline",
              "type": "string",
              "value": "Your one-line headline: seniority, domain, what you do best"
            },
            {
              "id": "a3",
              "name": "resume",
              "type": "string",
              "value": "Paste your full resume text here. The AI reads it to judge real fit (skills, seniority, domain, achievements) instead of guessing from the title."
            },
            {
              "id": "a4",
              "name": "preferences",
              "type": "string",
              "value": "What matters to you: company stage, industry, work style, must-haves and deal-breakers (e.g. visa sponsorship, no crypto, hybrid ok)."
            },
            {
              "id": "a5",
              "name": "position",
              "type": "string",
              "value": "product manager"
            },
            {
              "id": "a6",
              "name": "tags",
              "type": "string",
              "value": ""
            },
            {
              "id": "a7",
              "name": "locations",
              "type": "string",
              "value": "Remote, Worldwide, India, United States, United Kingdom, Europe"
            },
            {
              "id": "a8",
              "name": "remoteOnly",
              "type": "string",
              "value": "no"
            },
            {
              "id": "a9",
              "name": "minSalary",
              "type": "string",
              "value": "0"
            },
            {
              "id": "a10",
              "name": "excludeTerms",
              "type": "string",
              "value": "junior, intern, contract"
            },
            {
              "id": "a11",
              "name": "minScore",
              "type": "string",
              "value": "70"
            },
            {
              "id": "a12",
              "name": "maxJobs",
              "type": "string",
              "value": "12"
            },
            {
              "id": "a13",
              "name": "digestTo",
              "type": "string",
              "value": "you@example.com"
            },
            {
              "id": "a14",
              "name": "adzunaAppId",
              "type": "string",
              "value": ""
            },
            {
              "id": "a15",
              "name": "adzunaAppKey",
              "type": "string",
              "value": ""
            },
            {
              "id": "a16",
              "name": "adzunaCountry",
              "type": "string",
              "value": "us,gb,in"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "expand-job-sources",
      "name": "Expand job sources",
      "type": "n8n-nodes-base.code",
      "position": [
        256,
        528
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: your profile & search').first().json;\nconst kw = encodeURIComponent((cfg.position || '').split(',')[0].trim() || 'product manager');\nconst sources = [\n  { source: 'RemoteOK',  url: 'https://remoteok.com/api' },\n  { source: 'Remotive',  url: 'https://remotive.com/api/remote-jobs?search=' + kw },\n  { source: 'Arbeitnow', url: 'https://www.arbeitnow.com/api/job-board-api' },\n  { source: 'Jobicy',    url: 'https://jobicy.com/api/v2/remote-jobs?count=50' },\n  { source: 'Himalayas', url: 'https://himalayas.app/jobs/api?limit=50' },\n  { source: 'TheMuse',   url: 'https://www.themuse.com/api/public/jobs?page=1' },\n];\nif (cfg.adzunaAppId && cfg.adzunaAppKey) {\n  const countries = String(cfg.adzunaCountry || 'us').toLowerCase().split(',').map((s) => s.trim()).filter(Boolean);\n  for (const c of countries) sources.push({ source: 'Adzuna', url: 'https://api.adzuna.com/v1/api/jobs/' + c + '/search/1?app_id=' + cfg.adzunaAppId + '&app_key=' + cfg.adzunaAppKey + '&results_per_page=50&what=' + kw });\n}\nreturn sources.map((s) => ({ json: s }));"
      },
      "typeVersion": 2
    },
    {
      "id": "fetch-each-board",
      "name": "Fetch each board",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        432,
        528
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
            },
            {
              "name": "Accept",
              "value": "application/json, */*"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "normalize-merge-job-boards",
      "name": "Normalize & merge job boards",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        528
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: your profile & search').first().json;\nconst srcIn = $('Expand job sources').all();\nconst strip = (h) => String(h || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/g, ' ').replace(/\\s+/g, ' ').trim();\nconst srcOf = (it, idx) => { let pi = it.pairedItem; let p = (typeof pi === 'number') ? pi : (Array.isArray(pi) ? (pi[0] || {}).item : (pi || {}).item); if (p == null) p = idx; return (srcIn[p] && srcIn[p].json.source) || ''; };\n\n// per-source adapters -> { title, company, city, remote, url, desc, salaryMax }\nfunction adapt(source, j) {\n  const A = [];\n  const push = (o) => { if (o.title && o.url) A.push({ salaryMax: 0, ...o, source }); };\n  if (source === 'RemoteOK' && Array.isArray(j)) for (const r of j) { if (r.legal || !r.position) continue; push({ title: r.position, company: r.company || '', city: r.location || 'Remote', remote: true, url: r.url, desc: strip(r.description), salaryMax: Number(r.salary_max) || 0 }); }\n  else if (source === 'Remotive') for (const r of (j.jobs || [])) push({ title: r.title, company: r.company_name || '', city: r.candidate_required_location || 'Remote', remote: true, url: r.url, desc: strip(r.description) });\n  else if (source === 'Arbeitnow') for (const r of (j.data || [])) push({ title: r.title, company: r.company_name || '', city: r.location || 'Remote', remote: !!r.remote, url: r.url, desc: strip(r.description) });\n  else if (source === 'Jobicy') for (const r of (j.jobs || [])) push({ title: r.jobTitle, company: r.companyName || '', city: r.jobGeo || 'Remote', remote: true, url: r.url, desc: strip(r.jobExcerpt), salaryMax: Number(r.annualSalaryMax) || 0 });\n  else if (source === 'Himalayas') for (const r of (j.jobs || [])) push({ title: r.title, company: r.companyName || '', city: (r.locationRestrictions || []).join(', ') || 'Remote', remote: true, url: r.applicationLink || r.guid, desc: strip(r.excerpt || r.description), salaryMax: (String(r.salaryPeriod || '').toLowerCase().startsWith('year') ? Number(r.maxSalary) || 0 : 0) });\n  else if (source === 'TheMuse') for (const r of (j.results || [])) push({ title: r.name, company: (r.company || {}).name || '', city: (r.locations || []).map((l) => l.name).join(', ') || 'Flexible', remote: /remote|flexible/i.test(((r.locations || []).map((l) => l.name).join(' '))), url: (r.refs || {}).landing_page, desc: strip(r.contents) });\n  else if (source === 'Adzuna') for (const r of (j.results || [])) push({ title: r.title, company: (r.company || {}).display_name || '', city: (r.location || {}).display_name || '', remote: /remote/i.test(r.title + ' ' + ((r.location || {}).display_name || '')), url: r.redirect_url, desc: strip(r.description), salaryMax: Number(r.salary_max) || 0 });\n  return A;\n}\n\nconst lc = (s) => String(s || '').toLowerCase();\nconst list = (s) => lc(s).split(',').map((x) => x.trim()).filter(Boolean);\nconst positions = list(cfg.position);   // title must contain one of these\nconst tags = list(cfg.tags);            // title/desc must contain one of these (if set)\nconst excludes = list(cfg.excludeTerms);// title must NOT contain any of these\nconst locs = list(cfg.locations);\nconst remoteOnly = /^(y|yes|true|1)$/i.test(cfg.remoteOnly || '');\nconst minSalary = Number(cfg.minSalary) || 0;\nconst posOk = (t) => positions.length === 0 || positions.some((p) => lc(t).includes(p));\nconst tagOk = (t, d) => tags.length === 0 || tags.some((g) => (lc(t) + ' ' + lc(d)).includes(g));\nconst exclOk = (t) => !excludes.some((e) => lc(t).includes(e));\nconst locationOk = (job) => job.remote || locs.some((l) => l === 'worldwide' || lc(job.city).includes(l)) || !job.city;\n\nconst seen = new Set(); const jobs = [];\nconst responses = $input.all();\nfor (let idx = 0; idx < responses.length; idx++) {\n  const raw = responses[idx].json;\n  let j; if (typeof raw === 'string') { try { j = JSON.parse(raw); } catch (e) { continue; } }\n  else if (raw && typeof raw.data === 'string') { try { j = JSON.parse(raw.data); } catch (e) { continue; } }\n  else j = raw;\n  const source = srcOf(responses[idx], idx);\n  for (const job of adapt(source, j)) {\n    // apply the config filters\n    if (!posOk(job.title)) continue;                                   // position/title\n    if (!exclOk(job.title)) continue;                                  // exclude terms\n    if (!tagOk(job.title, job.desc)) continue;                         // tags/skills\n    if (remoteOnly && !job.remote) continue;                           // remote yes/no\n    if (minSalary > 0 && job.salaryMax > 0 && job.salaryMax < minSalary) continue; // salary (kept if unknown)\n    const key = (job.company + '|' + job.title).toLowerCase();\n    if (seen.has(key)) continue;\n    seen.add(key);\n    jobs.push({ ...job, desc: job.desc.slice(0, 700), locationOk: locationOk(job) });\n  }\n}\n// prefer location-matching jobs, then cap\njobs.sort((a, b) => (b.locationOk === a.locationOk) ? 0 : (b.locationOk ? 1 : -1));\nconst capped = jobs.slice(0, Number(cfg.maxJobs) || 12).map((j, i) => ({ i, ...j }));\nreturn [{ json: { jobs: capped, count: capped.length, sources: [...new Set(capped.map((j) => j.source))] } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "build-ai-prompt-resume-jobs",
      "name": "Build AI prompt (resume + jobs)",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        528
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: your profile & search').first().json;\nconst jobs = $json.jobs || [];\nconst promptText = [\n  'You are a sharp recruiter screening roles for ONE candidate. Using the candidate context below, score each job 0-100 for genuine fit.',\n  'Weigh the RESUME evidence (real skills, seniority, domain, achievements) and the PREFERENCES (deal-breakers and must-haves) heavily, not just the job title. Penalise seniority/domain mismatch, out-of-region roles, and anything that breaks a stated deal-breaker.',\n  'For each job return: i (its index), score 0-100, fit (one short phrase why it fits), concerns (one short phrase risk, or \"none\"), and pitch (one first-person sentence the candidate could open an application with, grounded in the resume).',\n  'Return ONLY the structured object with a scores array covering every job index.',\n  '',\n  'CANDIDATE PROFILE: ' + cfg.profile,\n  'HEADLINE: ' + cfg.headline,\n  'RESUME:\\n' + cfg.resume,\n  'PREFERENCES: ' + cfg.preferences,\n  'ACCEPTABLE LOCATIONS (remote or any of these): ' + cfg.locations + '. A role tied to a location outside this list is a mismatch: lower the score and flag it.',\n  '',\n  'JOBS (score every one by its index i):',\n  jobs.map((j) => '[' + j.i + '] ' + j.title + ' @ ' + j.company + ' | ' + (j.remote ? 'Remote' : 'Onsite') + ' \u00b7 ' + j.city + (j.salaryMax ? ' \u00b7 ~' + j.salaryMax + ' max' : '') + ' \u00b7 via ' + j.source + ' \u00b7 location_ok=' + j.locationOk + '\\n' + j.desc).join('\\n\\n'),\n].join('\\n');\nreturn [{ json: { promptText } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "score-jobs-with-ai-agent",
      "name": "Score jobs with AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1536,
        528
      ],
      "parameters": {
        "text": "={{ $json.promptText }}",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "anthropic-chat-model",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1520,
        768
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "structured-score-parser",
      "name": "Structured score parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1776,
        768
      ],
      "parameters": {
        "jsonSchemaExample": "{\"scores\":[{\"i\":0,\"score\":82,\"fit\":\"senior B2B SaaS PM match\",\"concerns\":\"none\",\"pitch\":\"I have shipped B2B SaaS products end to end for 8 years.\"}]}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "rank-matches-build-email",
      "name": "Rank matches & build email",
      "type": "n8n-nodes-base.code",
      "position": [
        2032,
        528
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: your profile & search').first().json;\nconst jobs = $('Normalize & merge job boards').first().json.jobs || [];\nconst min = Number(cfg.minScore) || 0;\n// AI Agent (with structured output parser) returns the parsed object under 'output';\n// fall back to a raw JSON string or an Anthropic content block if the shape differs.\nlet scores = [];\nconst j = $json;\nconst cand = (j.output && j.output.scores) ? j.output.scores : (j.scores ? j.scores : null);\nif (cand) scores = cand;\nelse {\n  let text = '';\n  if (typeof j.output === 'string') text = j.output;\n  else if (Array.isArray(j.content)) text = ((j.content.find((b) => b.type === 'text') || {}).text) || '';\n  else if (typeof j.text === 'string') text = j.text;\n  try { scores = (JSON.parse(text).scores) || []; } catch (e) { scores = []; }\n}\nconst esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst merged = scores.map((s) => ({ ...s, ...(jobs[s.i] || {}) })).filter((m) => m.url && m.score >= min).sort((a, b) => b.score - a.score);\nconst rows = merged.map((m) =>\n  '<tr><td style=\"padding:6px 10px;font-weight:700;vertical-align:top\">' + m.score + '</td>' +\n  '<td style=\"padding:6px 10px\"><a href=\"' + esc(m.url) + '\">' + esc(m.title) + '</a><br>' +\n  '<span style=\"color:#555\">' + esc(m.company) + ' \u00b7 ' + (m.remote ? 'Remote' : 'Onsite') + (m.city && m.city !== 'Remote' ? ' \u00b7 ' + esc(m.city) : '') + (m.salaryMax ? ' \u00b7 ~' + Math.round(m.salaryMax / 1000) + 'k' : '') + ' \u00b7 via ' + esc(m.source) + '</span><br>' +\n  '<span style=\"color:#111\">' + esc(m.fit) + '</span>' +\n  (m.concerns && m.concerns.toLowerCase() !== 'none' ? '<br><span style=\"color:#a00\">\u26a0 ' + esc(m.concerns) + '</span>' : '') +\n  '<br><span style=\"color:#036\"><em>Pitch: ' + esc(m.pitch) + '</em></span></td></tr>').join('');\nconst html = merged.length\n  ? '<h2>Your job matches</h2><p style=\"color:#555\">' + merged.length + ' roles at or above score ' + min + ', from ' + (jobs.length ? [...new Set(jobs.map((j) => j.source))].length : 0) + ' boards, ranked by fit.</p>' +\n    '<table style=\"border-collapse:collapse;font-family:system-ui,Arial\"><tr><th style=\"text-align:left;padding:6px 10px\">Fit</th><th style=\"text-align:left;padding:6px 10px\">Role</th></tr>' + rows + '</table>'\n  : '<p>No roles scored at or above ' + min + ' today. Widen your keywords or lower the threshold in the config node.</p>';\nconst subject = merged.length ? 'Job matches: ' + merged.length + ' roles (top ' + merged[0].score + '/100)' : 'Job matches: nothing above your threshold today';\nreturn [{ json: { subject, html, count: merged.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "email-the-digest",
      "name": "Email the digest",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2368,
        528
      ],
      "parameters": {
        "sendTo": "={{ $('Set config: your profile & search').first().json.digestTo }}",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "1585e5d5-5b4c-4257-a446-5277bb20ebbc",
  "nodeGroups": [],
  "connections": {
    "Every morning": {
      "main": [
        [
          {
            "node": "Set config: your profile & search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch each board": {
      "main": [
        [
          {
            "node": "Normalize & merge job boards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand job sources": {
      "main": [
        [
          {
            "node": "Fetch each board",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Score jobs with AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured score parser": {
      "ai_outputParser": [
        [
          {
            "node": "Score jobs with AI Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Score jobs with AI Agent": {
      "main": [
        [
          {
            "node": "Rank matches & build email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank matches & build email": {
      "main": [
        [
          {
            "node": "Email the digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize & merge job boards": {
      "main": [
        [
          {
            "node": "Build AI prompt (resume + jobs)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI prompt (resume + jobs)": {
      "main": [
        [
          {
            "node": "Score jobs with AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: your profile & search": {
      "main": [
        [
          {
            "node": "Expand job sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}