How to use n8n to Search Job Boards

After several months of frustration searching for new job postings – I started using n8n to search the job boards. This post focuses on how it’s built, and links to other posts for more information.

My job search agent uses the following:

  • An n8n workflow drives the logic – I host this via Docker on my Synology NAS, but this works just as well hosted on n8n.io
  • PostgreSQL stores the data, but I’ve also used Airtable successfully
  • Google Custom Search and Brave Search returns the latest jobs
  • Claude is my career coach, evaluating whether job postings fit in with my career aspirations and flagging the best fits

My n8n Workflows

I use six (6) n8n workflows to provide a solid level of logical abstraction. I originally did this in one workflow, but quickly realized that it yielded duplicate code and functionality.

The workflows are as follows:

  1. The Job Search Workflow is the primary driver and triggers every morning.
  2. I have two workflows that Retrieve [Search] Results, one configured for Google Search and one for Brave Search. This stores the search engine specific search query and branches out the calls to the job boards of interest (currently four).
  3. I have two workflows that Run [Search Engine] Search, one per search engine. This one handles the search-engine specific process to retrieve all pages of relevant results.
  4. Lastly, Process JD Search Result takes the job posting URL and analyzes it – normalizing up the URL, retrieving the job posting, and calling the LLM to analyze it.

Each workflow is available on GitHub, but the JSON is also embedded below. The embedded JSON is interactive to enable exploration without installing n8n – you can zoom in/out, view the properties for standard n8n nodes, and you can Copy/Paste the workflow JSON by selecting the nodes you want to copy (or select all) and copy them to your clipboard.

Primary Workflow: ‘Job Search Workflow’

The primary workflow is straightforward:

  1. It retrieves a list of previously processed JDs from the data store
  2. It queries the job boards using Google Search and Brave Search by calling the relevant sub-workflows
  3. It merges the search results together and uses the Combine node to remove previously processed results (see my How to bulk validate data to save API checks post)
  4. It then iterates over the remaining search results using the ‘Process JD Search Result’ sub-workflow
  5. Once done, it uses Gmail to send an email summary of the run

Sub-workflow: ‘Retrieve [Google | Brave] Results ‘

The first sub-workflows that we call retrieve the search results from Google and Brave. And I have one sub-workflow for each search engine, primarily because the search query strings are different between them, but also because I originally used different search engines with different job boards.

The workflows abstract the calls to n job boards: (a) they simplify the primary workflow, (b) enable me to set a search string in one place, and (c) make it easy to add additional job boards later.

Retrieve Google Results
Retrieve Brave Results

As you can see, both workflows behave the same way – the only material difference is the search string.

Sub-workflows: ‘Run [Google | Brave] Search Query’

The second sub-workflows run the query and site strings passed to them and runs through the Search APIs.

Although structurally the same, the code in the code nodes is tailored to the search API.

Run Google Search Query

The Google-focused workflow uses the APIs to use page numbers to iterate through the search results.

For additional detail on the approach, check out my Adding Google Search to my job search agent and Retrieving all Google search results in n8n blog posts.

Run Brave Search Query

The Brave Search API takes a different approach, using a hasMoreResults value to let us know when we are done processing the relevant results.

Sub-workflow: 'Process JD Search Result'

This is the most interesting workflow of the bunch - here is where we evaluate if we've seen the job description ('JD') before, get our LLM to evaluate it, and then add it to our data store.

A few design notes on this sub-workflow and the learnings that drove them:

  • I normalize the job board URLs to help spot variations and to prevent processing '/apply/*' URLs and URLs with a variety of query parameters that do tend to show up in search results.
  • I test if I've seen the URL before again here after normalizing the URL to help conserve AI tokens and to save the data store from adding a lot of duplicate data.
  • I do the HTTP Request and JD extraction outside of the LLM for a few reasons:
    • It significantly reduces the number of input tokens
    • It allows for use of hosted LLMs like the Nebius AI Studio that may not have web access
    • Some job boards blocked LLMs from reading/scraping the post for analysis
  • The LLM call (in my case, to Claude) should be detailed in what you want it to do and the data that you want back from it. Although I've simplified the prompt it should be sufficient to help you define your workflow.
  • I have two dataset insert calls because I use different strings for (a) JDs that have expired or have no information, and (b) JDs that have LLM feedback in them.

And I continue do weekly tweaks the nodes in this sub-workflow - the URL normalization, the HTML retrieval, and the LLM instructions. I do this by watching the performance and adjusting the assumptions in the code. And over time, my tweaking workflow uses Claude as my coding assistant/copilot.

PostgreSQL Database

I store all of my data in PostgreSQL. I originally stored it in Airtable, but my projects have exceeded the number of API calls available in the free tier plan.

To use Airtable, use the workflows in the GH repository and replace the Postgres nodes with their Airtable equivalents. The GH workflows still have the Airtable nodes within the workflows, they just aren't wired into the workflow currently.

Data Schema

I'm storing everything from n8n in a single database table - 'applications' - and I have a number of views constructed on top of to filter data down for jobs to be done.

CREATE TABLE IF NOT EXISTS public.applications
(
    id integer NOT NULL DEFAULT nextval('applications_id_seq'::regclass),
    company character varying(255) COLLATE pg_catalog."default",
    "position" text COLLATE pg_catalog."default",
    date_identified date,
    date_applied date,
    decision_date date,
    status character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'New'::character varying,
    job_description_link text COLLATE pg_catalog."default",
    notes text COLLATE pg_catalog."default",
    source character varying(50) COLLATE pg_catalog."default",
    job_description_text text COLLATE pg_catalog."default",
    green_flags text COLLATE pg_catalog."default",
    red_flags text COLLATE pg_catalog."default",
    normalized_url text COLLATE pg_catalog."default",
    platform character varying(50) COLLATE pg_catalog."default",
    created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT applications_pkey PRIMARY KEY (id)
)
Database Configuration

A few quick notes about setting up your database:

  • Create a user that is dedicated to the AI Agent and only has access to the one table, and use a complicated, one-time-use password (duh ^_^ ).
  • If you're running this at home and exposing this externally, configure your router to limit who can get to what on your home network.
  • Create views to work with the data (e.g., triaging new JDs, tracking applications in-flight, etc.), and do this from a different user to enable better logging of who is doing what in your database.

Claude LLM: My Career Coach

The Anthropic AI node makes it easy to out to Claude and is fairly straightforward.

A few notes worth calling out:

  • Use your LLM of choice - you'll need to setup an API account as you can't use your Pro subscription
  • Constantly review and refine your prompt - I revise mine every couple of weeks
  • After each prompt revision, pass it JDs that you think should be in that grey zone of 'maybe a fit' and see how it responds.
Below is a generalized version of my current prompt that I use, which is also available on GitHub.
You are a career coach evaluating job opportunities for a <role>(n years experience, <level>-level) seeking <seniority> roles in <job roles>. The candidate has strong <type> experience but gets rejected for <reasons>.

Parse the passed job title and job description and analyze it against the STRICT criteria and the guidelines below, and document your evaluation using JSON.


REQUIRED CRITERIA (ALL must be met):
1. Experience: n+ years minimum (prefer m+). Exception: <title> roles at smaller companies (<500 employees) requiring k+ years for exceptional <discipline> skill-building opportunities 
2. Seniority: <titles> level. Exception: <title> roles at smaller companies (<500 employees) for exceptional skill-building opportunities in <discipline>-focused roles.
3. Function: <job functions>
4. Audience: <important to me; you can replace with tech stack if developer>
5. Location: United States, Canada, or explicit remote-first for US timezones
6. Background Fit: Role should leverage <skillsets>, but roles requiring <skillsets or environments> are poor fits.

AUTOMATIC DISQUALIFIERS:
- <level or seniority>
- Missing/expired job descriptions
- <audience; tech stack>
- Europe/APAC only positions
- Contract/temp roles
- Agencies, consulting firms, non-product companies
- <vertials> companies
- Roles explicitly requiring <flags you typically don't do well with> or specialized domain expertise that I don't have
- >8 "must have" requirements (unicorn roles)
- Inclusion of unprofessional language - "ninja", "rockstar", or similar
- Companies with recent layoffs/hiring freezes


COMPANY STAGE GUIDANCE:
- Series C+/Enterprise: Strong fit (enterprise background advantage)
- Series B: Acceptable but flag startup experience requirements
- Series A/Earlier: Generally poor fit unless exceptional
- Smaller Companies (<500 employees): IC or small team roles acceptable for <role> skill development if strong domain fit and growth potential


REASONING MUST ADDRESS:
- Which criteria are met/failed specifically
- Background alignment with role requirements
- Whether this represents career advancement
- Likelihood of rejection for common patterns (<list>)
- Company stage fit assessment
- Any significant red flags or green flags
- Whether role requirements seem realistic vs unicorn-seeking
- Recognize that team leadership and cross-functional skills transfer across marketing disciplines


MARKET REALITY CHECKS:
- Flag if role seems designed for internal promotion vs external hire
- Note if requirements suggest they want someone 5-10 years younger
- Identify if role combines multiple specializations unrealistically


Poor reasoning: "Good fit for marketing role"
Good reasoning: "Strong match. VP-level requiring 12+ years PMM experience at Series C developer platform company. Clear advancement from current VP level with domain expertise advantage."



OUTPUT FORMAT:
Valid JSON only, no markdown/code blocks:

{
  "relevant": boolean,
  "company": "string",
  "job_title": "string", 
  "reasoning": "string - 2-3 sentences with specific criterion references",
  "experience_requirement": "string - exact years or 'not specified'",
  "green_flags": ["array of positive indicators"],
  "red_flags": ["array of concerning elements"]
}



EXAMPLES:

{
  "relevant": true,
  "company": "BigCoForDevs",
  "job_title": "VP, Developer Marketing & Ecosystem",
  "reasoning": "Strong match. VP-level role requiring 10+ years experience at established platform company targeting developers. Leverages DevRel background without requiring pure PMM specialization.",
  "experience_requirement": "10+ years",
  "green_flags": ["VP level", "Developer-focused", "Platform company", "Enterprise stage"],
  "red_flags": []
}

{
  "relevant": false,
  "company": "StartupXYZ", 
  "job_title": "Head of Product Marketing",
  "reasoning": "Rejected. Explicitly requires Series B scaling experience and pure PMM background. High likelihood of 'insufficient startup leadership' rejection based on candidate's enterprise background.",
  "experience_requirement": "8+ years",
  "green_flags": ["Head level", "8+ years requirement"],
  "red_flags": ["Requires Series B experience", "Pure PMM background required"]
}

{
  "relevant": false,
  "company": "TechCorp",
  "job_title": "Director of Product Marketing",
  "reasoning": "Rejected. Role requires 10+ must-have qualifications including fintech domain expertise, Series A leadership, and pure PMM background. Unicorn requirements suggest unrealistic expectations.",
  "experience_requirement": "8+ years",
  "green_flags": ["Director level", "Developer audience"],
  "red_flags": ["Unicorn requirements (10+ must-haves)", "Domain expertise gaps", "Unrealistic combination"]
}

{
  "relevant": true,
  "company": "DevTools Startup",
  "job_title": "Senior Product Marketing Manager",
  "reasoning": "Acceptable step-back opportunity. Senior Manager role at 150-person developer tools company offers strong PMM skill development in target domain. Small team leadership component with clear growth path as company scales.",
  "experience_requirement": "5+ years",
  "green_flags": ["Developer-focused", "PMM skill building", "Growth stage company", "Domain expertise match"],
  "red_flags": ["Step back in seniority"]
}

=== Job Title ===
{{ $json.jobTitle }}

=== Job Description ===
{{ $json.jobDescription }}

Triage, Triage, Triage

Once created, configured, and activated, the workflow should check the job boards for you every day, and highlight potential role fits. After working with my Job Search Agent for a few months, here are a few notes:

  • Review and triage what the agent processed - I recommend still opening up each new job posting:
    • Not all 'Ignore - not found' are really unavailable and may simply have some more interesting/complicated HTML or scripts on the page.
    • Sometimes the LLM will raise a false positive or false negative - treat the LLM summary as data points and not a final decision
  • Review and revise the prompt as you interview - give it flags to look for and update what you want as you learn more about yourself on your journey

And, as I mentioned at the beginning of the post, this is just the start of the job hunt. Once you have some qualified roles in hand, you can use your trusty LLM to dig in deeper for career fit and to help you tailor your resume and cover letters.

Good luck!

Good luck on your journey and I hope that this workflow helps you. For me, this agent has been a labor of love and a great learning experience - I hope that it can help others.

If you have feedback, questions, or concerns - feel free to send me a DM over LinkedIn or post in the comments below. I plan to keep this page up-to-date as the agent evolves, and I'd love to incorporate learnings from others.

2 Comments

Comments are closed