How I auto-match and rank jobs with n8n and AI
To match and rank jobs with n8n and AI, you run two schedules. An ingest branch crawls free job boards (Greenhouse, Lever, Ashby, Adzuna, RemoteOK) into one shared pool every few hours with no AI, so it stays cheap. A daily branch loops each seeker, pulls a short list, and lets an AI model score every job from 0 to 100 before it emails their best matches.
I built this because I was skimming a dozen job boards for a handful of people and losing the good roles in the noise. This is a real template I run. Below I walk the actual nodes, the parts that took the most fiddling, and how to point it at your own seekers.
The problem
Good jobs are spread across dozens of boards, and each one only shows its own.
If you are looking for a role, or matching roles for a group of people, the openings sit on Greenhouse, Lever, Ashby, LinkedIn, and a dozen niche boards. No single site shows them all. So you check the same boards over and over, skim past roles that do not fit, and still miss the good ones that went up an hour ago. Do this for more than one person and it turns into a part-time job on its own.
It adds up. The average US job search runs close to 20 weeks, and roughly 80 percent of openings are found online, spread across boards that do not talk to each other.
How most teams deal with it today
There are three common ways people find matching jobs today. Each has a catch.
Scroll job boards by hand
Free and simple. You open LinkedIn, Indeed, and a few company pages and read. But it eats an hour a day, the best roles hide in the noise, and you always miss the ones posted while you were not looking.
Job-alert emails
Most boards will email you when a new role matches a saved search. Handy, but the alerts key off simple keywords, so you get near-misses and repeats. Nobody ranks them for real fit, and ten alert feeds are their own inbox mess.
Hand it to a recruiter
A recruiter or a virtual assistant can shortlist for you. That works, but it costs money every month, they only know the roles they see, and a busy week means your list shows up late or not at all.
The fix
One flow crawls the boards for you, then an AI model ranks what fits.
A single n8n flow does the reading for you. Every few hours it crawls free job boards into one shared pool. Once a day it walks each seeker's profile, pulls a short list, and lets an AI model score every job for real fit before it emails only the best ones. Here is what it does, in three parts.
Crawls free boards into one pool
Every 3 hours the ingest branch hits the Greenhouse, Lever and Ashby board APIs plus Adzuna and RemoteOK, shapes each posting the same way, drops anything older than 90 days, and saves it into one shared jobs pool keyed on id. There is no AI here, so it is cheap to run often.
Scores each job against a profile
Once a day the match branch loops every active seeker, pulls a short list from the pool, and sends it to an AI model in batches of 20. The model returns a 0 to 100 fit score, a one-line reason, and any concerns for each job, so the ranking reflects real fit and not keyword stuffing.
Emails only the top matches
It keeps jobs at or above that seeker's min_score, which is the lowest score they want to see, sorts them high to low, saves them so they never repeat, and emails a digest with the score, role, company and an apply link. Each person gets a tight ranked list instead of a firehose.
Want this running for you?
Take the file and set it up yourself, or book a free call and we wire it into your sources, database and inbox for you.
Build it step by step
Every node in the flow, in execution order, and how to set each one up. The ingest branch fills the pool, the match branch scores and emails, and a small error branch warns you if a run fails.
- 1Every 3 hours · Schedule Trigger
Starts the ingest crawl on a timer so the shared pool stays fresh.
Set it up: Add a Schedule Trigger and set the interval to every 3 hours. This is the only trigger the ingest branch needs.
- 2Search terms · Set
Holds the list of roles the crawl searches for.
Set it up: Add a Set node with one array field called queries. Put your job titles in it, like software engineer, product manager, and data analyst.
- 3ATS board list · Code
Lists the company career boards to crawl.
Set it up: Add a Code node that returns one item per company, each with an ats name (greenhouse, lever, or ashby) and a company slug. Add or remove companies to fit your search.
- 4Fetch ATS board · HTTP Request
Pulls the open jobs from each company board.
Set it up: Add an HTTP Request that builds the board URL from the ats and slug. Turn on retries and set neverError so one dead board does not stop the run.
- 5Parse ATS · Code
Cleans each posting into one common shape.
Set it up: Add a Code node that strips the HTML tags out of the text and maps every posting to the same fields: id, title, company, location, url, and description.
- 6Adzuna per query · Code
Turns each search term into its own Adzuna request.
Set it up: Add a Code node that reads the queries array from Search terms and returns one item per term.
- 7Adzuna search · HTTP Request
Searches the Adzuna job API for each term.
Set it up: Add an HTTP Request to the Adzuna search endpoint. Pass your free app id and app key, the search term, and ask for up to 50 results each.
- 8Parse Adzuna · Code
Maps Adzuna results into the same shape as the rest.
Set it up: Add a Code node that reads the results array and writes the same fields the other sources use.
- 9RemoteOK · HTTP Request
Pulls the whole RemoteOK feed in one call.
Set it up: Add an HTTP Request to the RemoteOK API. It is open, so it needs no key.
- 10Parse RemoteOK · Code
Maps each remote posting into the common shape.
Set it up: Add a Code node that skips empty rows and writes the same fields, with location set to Remote.
- 11Merge sources · Merge
Joins the three source branches into one stream.
Set it up: Add a Merge node with 3 inputs and wire Parse ATS, Parse Adzuna, and Parse RemoteOK into it.
- 12Normalize + freshness · Code
Removes repeats and old postings before the pool write.
Set it up: Add a Code node that drops any job it has already seen by id, throws out postings older than 90 days, and flags the rest as fresh.
- 13Upsert to jobs pool · HTTP Request
Saves every job into the shared Supabase pool without making copies.
Set it up: Add an HTTP Request that posts the rows to your Supabase jobs table with on_conflict set to id. That writes new jobs and updates ones already there, so the pool never doubles up.
- 14Daily 08:00 · Schedule Trigger
Starts the matching run once a day.
Set it up: Add a Schedule Trigger set to a daily cron of 8am. This kicks off the per-seeker scoring.
- 15Get active seekers · HTTP Request
Reads the people you want to match jobs for.
Set it up: Add an HTTP Request that reads the active rows from your Supabase job_seekers table, pulling each person's email, profile, function, and min_score (the lowest score they want to see).
- 16Loop Over Users · Split In Batches
Handles one seeker at a time.
Set it up: Add a Split In Batches node with batch size 1. Everything after it runs once per seeker, then loops back for the next one.
- 17Pool shortlist · HTTP Request
Grabs a small set of likely jobs for this seeker.
Set it up: Add an HTTP Request that reads up to 15 fresh jobs from the pool whose title matches the seeker's function, newest first. This cheap filter keeps the AI off the whole pool.
- 18Already-matched ids · HTTP Request
Finds jobs this seeker was already sent.
Set it up: Add an HTTP Request that reads every job_id already saved for this seeker so the next step can skip them.
- 19Make scoring batches · Code
Builds the requests the AI model will score.
Set it up: Add a Code node that drops the already-matched jobs, splits the rest into batches of 20, and packs each batch with the seeker's profile and a strict answer format for the model.
- 20Claude score batch · HTTP Request
Asks the AI model to score each job from 0 to 100.
Set it up: Add an HTTP Request to the Anthropic API. My build calls the claude-haiku-4-5 model. Turn on retries so a slow call does not fail the run.
- 21Attach scores + filter · Code
Matches each score back to its job and keeps the good ones.
Set it up: Add a Code node that puts the model's scores onto the jobs by index, keeps any job at or above the seeker's min_score, and sorts them from high to low.
- 22Save matches · HTTP Request
Records which jobs this seeker was matched to.
Set it up: Add an HTTP Request that writes the kept jobs to the user_job_matches table. Set it to ignore duplicates on user and job, so the same job is only ever saved once per person.
- 23Email digest · HTTP Request
Emails the seeker their ranked matches.
Set it up: Add an HTTP Request to the Resend API that builds an HTML list of the scored jobs and sends it to the seeker. Wire its output back into Loop Over Users so the flow moves to the next person.
- 24On workflow error · Error Trigger
Catches any run that fails.
Set it up: Add an Error Trigger. Set this workflow as its own Error Workflow in Settings so a failed run lands here.
- 25Format alert · Code
Writes a short failure email.
Set it up: Add a Code node that reads the failing workflow name, the node that broke, and the error message, then builds an alert email from them.
- 26Send alert (Resend) · HTTP Request
Emails you the failure alert.
Set it up: Add an HTTP Request to the Resend API that sends the alert to your inbox. Turn on retries in case the send itself hiccups.
What made it tricky?
Three things ate most of the build time. If you rebuild this, they are where it goes wrong first.
Keeping the AI cheap
The easy mistake is to score the whole pool. I keep the AI out of ingest, and the Pool shortlist node pre-filters to 15 fresh jobs per seeker with plain SQL before any scoring. The model only ever sees a small, relevant list, 20 jobs at a time.
Structured scores, not prose
A model that free-writes is impossible to rank. Make scoring batches hands the model a strict answer format, so each job comes back with an index, a score, a fit line, and any concerns. Attach scores and filter reads those numbers, keeps the ones over min_score, and sorts them. That is what turns AI text into a real ranking.
The loop-back wiring
Loop Over Users has two outputs. Output 0 means done, and output 1 is the per-seeker path. The last node, Email digest, has to connect back into the loop, not into Done. Get it backwards and it either scores one seeker or never moves on. Batch size 1 keeps each seeker's scoring and email apart.
How do I adapt it to my own setup?
The three branches are the working template. Adapting it means swapping sources, profiles and keys, not rewiring the logic.
| To change | What you edit |
|---|---|
| The roles it looks for | The queries array in Search terms and the function field on each job_seekers row. |
| The job sources | The slug list in ATS board list, or copy a branch into Merge sources for a new API. |
| Who gets matched | Rows in job_seekers, each with email, profile, function and min_score. |
| How strict the ranking is | Each seeker's min_score, plus the shortlist limit in Pool shortlist. |
| The scoring model | The model in Make scoring batches and the key in Claude score batch. |
| The digest look | The HTML template and sender in Email digest. |
Replace every YOUR_ placeholder (your Supabase URL and service_role key, your Adzuna app id and key, your model key, and your Resend key), create the three Supabase tables, and add one seeker row to test. If you would rather not wire the auth and database yourself, this is exactly the kind of build you can hire an n8n expert to finish. It pairs well with my match candidates to open roles and my screen job applicants automatically.
Wondering what that costs? See what an n8n expert costs in 2026.
Job matching questions, straight answers.
How do I match and rank jobs with n8n and AI?
I split it into two schedules. An ingest branch runs every 3 hours, crawling free job sources into one shared Supabase pool with no AI so it stays cheap. A daily branch loops each seeker, pulls a shortlist, sends batches to an AI model that scores each job 0 to 100, then emails the top matches.
Why keep the AI out of the ingest step?
Ingest runs every few hours across many boards, so running an AI model there would be slow and cost money for no gain. That branch just crawls, cleans up and saves jobs into a shared pool keyed on id. The scoring branch runs once a day and only ever scores a small pre-filtered shortlist, not the whole pool.
How does the AI score and rank each job?
Make scoring batches builds groups of 20 jobs, each with the candidate profile and a strict answer format. The model returns a score from 0 to 100 plus a fit line and concerns for every job. Attach scores and filter matches those back, keeps anything at or above the seeker's min_score, and sorts high to low.
Which job sources does the workflow pull from?
Five free ones, no scraping. Greenhouse, Lever and Ashby expose open JSON board APIs, so I hit a curated list of company slugs. Adzuna gives a keyed search API across my five search terms, and RemoteOK returns its whole feed. Merge sources combines all three branches before dedup, so adding a source is just another branch.
How does it avoid emailing the same job twice?
Two guards. Upsert to jobs pool matches on id so the pool never holds duplicates. Then, per user, Already-matched ids reads every job_id already saved for that seeker, and Make scoring batches drops those before scoring. Save matches also writes with ignore-duplicates on user and job, so a job is only ever matched once per person.
Can this scale to thousands of job seekers?
That was the design goal. The AI never scores the whole pool, only a per-user shortlist pulled by a cheap SQL pre-filter on the jobs table. For real scale I would pre-rank with embeddings, score only the top few with a small model, cache per user and job, and run n8n in queue mode with a users cursor.
What API keys do I need to run it?
Four, and two sources need none. Supabase holds the pool and seekers with a service_role key. Adzuna gives a free app id and key. An AI provider key powers the scoring. Resend sends the digest and error emails. Greenhouse, Lever, Ashby and RemoteOK are open, so they need no credential at all.
Want this running for your job seekers?
30 minutes, free. Bring your sources and the profiles you want to match. You leave with a plan to turn open job boards into a ranked shortlist in every inbox, whether you build it or we do.
Keep reading.
Turn a YouTube video into a short
It pulls the transcript, writes a fresh script with AI, makes b-roll with Gemini Veo, and builds a ready-to-post 9:16 short.
Let your inbox answer the easy emails
It reads new replies, writes safe answers, and takes people off the list the moment they ask.
Reply to ad comments while you sleep
It sorts every comment, answers the simple ones, and sends the tricky ones to a person.
Rather have it built for you?
Skip the build. A free 30-minute call and we set this up in your stack, live in a week or two.