Module 11 · ai · intermediate
Lesson 11 · AI Job Scout
Goal
Combine everything so far: multiple data sources, a Code node for messy normalization, cheap filtering before expensive AI calls, and per-item AI scoring. It's also a tool you'll use yourself to find automation work.
Prerequisites
- Modules 02 (split/sort/limit/aggregate), 06 (Information Extractor)
Concepts
- Normalize early: every source has its own shape (RemoteOK
position, WWRtitle= "Company: Title"). Map them all to one shape right after fetching, and everything downstream stays simple. - Code node: when mapping needs real logic (filtering a legal-notice item, splitting strings, stripping HTML), JavaScript is clearer than five Edit Fields nodes. Return
[{ json: {...} }, …]. - Merge (Append): waits for both inputs and outputs all items from both.
- Filter before AI: keywords → newest → top N, and then AI. Every item that reaches Score Job costs a request, so
limitcaps it (max 25). - Whole-word matching:
includes('llm')matches "enrollment". A small regex with word boundaries fixes it. - Per-item AI: the Information Extractor runs once per job;
$('Top N').item.jsonpairs each result back with its job.
Build it
Step 1: Triggers and settings
- Schedule Trigger
Every 4 Hoursand WebhookScout API(GETjobs/scout, respond via node) → Edit FieldsSettings (edit profile):keywords(Array) ={{ ($json.query?.keywords ?? 'n8n,zapier,make.com,automation engineer,…').split(',').map(k => k.trim().toLowerCase()).filter(Boolean) }}min_score={{ Number($json.query?.min_score ?? 60) }},max_jobs={{ Math.min(Number($json.query?.limit ?? 10), 25) }},dry_run={{ $json.query?.dry_run === 'true' }}profile: your skills, location/timezone and preferences, written as a paragraph
Step 2: Fetch both sources (in parallel)
Connect Settings to both:
- HTTP Request
Fetch RemoteOK: GEThttps://remoteok.com/apiwith headerUser-Agent: Mozilla/5.0 (n8n job scout). The API returns an array, and n8n splits it into items automatically. - RSS Read
Fetch We Work Remotely:https://weworkremotely.com/categories/remote-programming-jobs.rss - On both, node Settings → Retry On Fail (3 tries, 2000 ms) and On Error → Continue. Job boards drop connections now and then, and one flaky source shouldn't kill the run. The Normalize nodes skip the error item.
Step 3: Normalize (Code nodes)
Normalize RemoteOK: drop items without position (the first item is a legal notice), strip HTML from description, map to {source, title, company, url, tags, location, salary, posted_at, description}.
Normalize WWR: split title on ": " into company + title, url = link, posted_at = isoDate, description = contentSnippet.
See workflow/learner.json for the full code.
Step 4: Merge, filter, sort, limit
- Merge
All Jobs(Append), RemoteOK into input 1, WWR into input 2. - Filter
Matches Keywords: Boolean is true on{{ $('Settings (edit profile)').first().json.keywords.some(k => new RegExp('(^|[^a-z0-9])' + k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + 's?($|[^a-z0-9])').test([$json.title, $json.tags.join(' '), $json.description].join(' ').toLowerCase())) }} - Sort
Newest Firstbyposted_atdescending → LimitTop N=max_jobs.
Step 5: Dry run or score
IF Dry Run? (dry_run is true):
- true → Aggregate
Collect Matches→ Respond withcountand slimjobs - false → Information Extractor
Score Jobwith your profile + the job as text, attributesscore(number, 0–100 with a rubric),fit_reasons,red_flags,pitch_angle; system prompt "blunt, practical career advisor… never invent facts". Model: OpenAIgpt-5-mini.
Step 6: Report
Edit Fields Scored Job (job fields from $('Top N').item.json + clamped score) → Filter Good Fit Only (score ≥ min_score, Always Output Data on) → Aggregate → Edit Fields Build Report (jobs sorted by score, count, message text) → IF Called via API? → Respond / Send Alert (Exercise 1).
Test it
./test/test.sh # dry run against live boards
WITH_CREDS=1 ./test/test.sh # + AI scoring of 5 jobs
curl 'http://localhost:5678/webhook/jobs/scout?limit=8' | jq -r '.message'
Common problems
- RemoteOK returns 403 or nothing: the User-Agent header is missing.
- Merge outputs nothing: one source failed or returned zero items. Check each fetch node.
- Irrelevant matches: generic keywords ("automation", "workflow") appear in many descriptions. Use specific phrases.
- Slow / costly runs: lower
limit. Every job reaching Score Job is one AI request. - Same jobs every 4 hours: add "seen jobs" memory (Exercise 2).