# job_radar.md

> Source: <https://gist.github.com/ReaganS94/eaee436898ea0c3a5e1a15398f4a1718>
> Published: 2026-10-21 15:34:59+00:00

In this guide you build an automation that actually helps you: a **job radar**.

Every morning, it checks the Arbeitsagentur job board for new postings. AI reads each one and rates how well it fits *you*. You only get an email with the good matches.

``` php
[Manual trigger]──┐
                  ├─> Search settings -> Fetch jobs -> Split jobs -> Clean up jobs
[Every morning]───┘        -> Remember seen jobs -> Limit -> Score with AI (Gemini)
                           -> Job + score -> Good matches only -> Build email -> Send email
```

You build it with **n8n**, an automation tool like Make, with two big differences:

- It runs on your own laptop, so your data stays with you.
- It's free when you host it yourself. No credits, no monthly limit.

- **Goal:** what you're building in this part.
- **Checkpoint:** what you should see before you move on.
- **Stuck?** Quick fixes for the usual problems.

`[drag X]` means: drag the field X from the left panel into this spot. Replace the whole `[drag X]`, brackets included.

**If you know Make**, most of this will feel familiar. Only the words change:

| Make | n8n | What it means | 
|---|---|---|
| Scenario | Workflow | The whole automation | 
| Module | Node | One step | 
| Bundle | Item | One piece of data, like one job posting | 
| Operation | Execution | One run of the workflow | 
| Connection | Credential | Your saved login or API key for a service | 
| Mapping | Expression | Pulling data from earlier steps, written as `{{ }}` | 

You won't write code yourself. At one point, AI writes a small piece of code for you, and this guide shows you how to ask for it.

Please have these ready:

- **Docker Desktop** installed and running.
- **n8n downloaded.** This saves the classroom Wi-Fi. In a terminal, run:

```
docker pull n8nio/n8n
```

- **A Gemini API key:** go to**[https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)** and click**Create API key** . Copy it somewhere safe.
- **A Gmail app password:** see the step-by-step section below.
- **Your profile in 3–4 sentences:** your background, what kind of job you want, languages, preferences. There's an example in Part 2.

n8n sends the email through your Gmail account. Gmail won't accept your normal password from an app like n8n, so you create a special 16-character "app password" just for n8n.

**Use a personal Gmail account.** Work and school accounts usually don't allow app passwords.

App passwords only exist once 2-Step Verification is on. Skip to part B if yours already is.

1. Go to **[https://myaccount.google.com/security](https://myaccount.google.com/security)** .
2. Check the profile picture at the top right: is it your personal Gmail? If not, click it and switch accounts.
3. Scroll to **How you sign in to Google** and click**2-Step Verification** .
4. Click **Turn on 2-Step Verification** and follow the steps. Usually you confirm your phone number and enter a code sent by SMS.
5. Back on the Security page, **2-Step Verification** should now say**On** .

1. Open **[https://myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords)** . Type the full address, including`https://` .
  - Google doesn't show a button for this on the Security page. If the link doesn't work, go to **[https://myaccount.google.com](https://myaccount.google.com)** , click the search bar at the top, type`App passwords` , and click the result.
2. Google doesn't show a button for this on the Security page. If the link doesn't work, go to 
3. Sign in again if Google asks.
4. In **App name** , type`n8n` and click**Create** .
5. A box shows your app password: 16 letters in four groups, like `abcd efgh ijkl mnop` .
6. **Copy it now and save it somewhere safe.** Google shows it only once. If you lose it, delete it and create a new one.
7. Click **Done** .

That's all for now. You'll paste the app password into n8n in **Part 8: Send the email**, which walks you through every click. Until then, keep it somewhere easy to copy from, like a note on your laptop.

Work through these in order:

1. **2-Step Verification is off.** Do part A, then reload the page.
2. **Wrong account.** If you're signed in to several Google accounts, the link opens your default one. Check the profile picture at the top right and switch accounts, or open the link in a private/incognito window and sign in with your personal Gmail only.
3. **Work or school account.** Your organization has blocked app passwords. Use a personal Gmail.
4. **2-Step Verification uses only a security key, or Advanced Protection is on.** Add your phone number as a second step in part A, or use a different Gmail account.

**Good to know:** if you ever change your Google password, all app passwords stop working. Then just create a new one and update it in n8n.

**Goal:** n8n running on your laptop.

n8n runs inside Docker. Think of Docker as a box that has everything n8n needs, so you don't have to install anything else.

1. Make sure Docker Desktop is running.
2. Open a terminal (Mac: Terminal, Windows: PowerShell) and run this command. It creates a storage space, so your workflows survive when n8n stops:

```
docker volume create n8n_data
```

3. Then run this command to start n8n. The timezone part makes sure "8 a.m." means 8 a.m. in Germany:

```
docker run -it --rm --name n8n -p 5678:5678 -e GENERIC_TIMEZONE="Europe/Berlin" -e TZ="Europe/Berlin" -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true -v n8n_data:/home/node/.n8n n8nio/n8n
```

4. Wait until the terminal shows the editor URL (`http://localhost:5678` ). The first start can take a minute.
5. Open **[http://localhost:5678](http://localhost:5678)** in your browser.
6. Create your owner account (email, name, password). It's only stored on your laptop.
7. Skip the survey and the license offer.

**Checkpoint:** you see the n8n start screen.

**Keep this terminal open.** If you close it, n8n stops. To start n8n again later, run the command from step 3 again.

**Stuck?**

- **"Cannot connect to the Docker daemon":** Docker Desktop isn't running. Start it and wait until it says it's running.
- **"Port is already allocated":** n8n is already running somewhere. Check Docker Desktop >**Containers** .
- **"Container name already in use":** run`docker rm n8n` , then the command from step 3 again.

**Goal:** all your settings in one place: job title, city, and your profile. That way, you can change the search later without touching the rest of the workflow.

A habit for this whole guide: **name every node.** It makes the workflow readable, and it helps later when you reference earlier steps.

1. Click **Create workflow** . Name it`Job radar` (top left).
2. Click **Add first step** >**Trigger manually** .
3. Click the **+** on the right of the trigger, search**Edit Fields (Set)** , and add it.
4. Rename it `Search settings` : click the title at the top of the panel.
5. Click **Add Field** five times. For each one, type the name, choose the type, and enter the value:

| Name | Type | Value | 
|---|---|---|
| `jobTitle` | String | e.g. `Marketing` | 
| `city` | String | e.g. `Hannover` | 
| `radiusKm` | Number | `25` | 
| `daysBack` | Number | `7` | 
| `profile` | String | Your 3–4 sentence profile (example below) | 

Example profile:

```
Former retail team lead with 5 years of experience in customer service and managing small teams. Currently finishing a course in AI tools and automation. Looking for junior roles in marketing, customer success or operations. Fluent in German and English, prefers hybrid or remote work.
```

1. Click **Execute step** .

`daysBack` is 7 for now, so you have enough jobs to test with. In Part 10, you switch it to 1, because the workflow will run daily.

**Checkpoint:** the output on the right shows one item with your five fields.

**Goal:** a list of real job postings from the Arbeitsagentur.

The Arbeitsagentur has a free **API**: a door for programs instead of humans. The **HTTP Request** node is how n8n talks to any API, even ones that don't have their own node.

1. Add an **HTTP Request** node after`Search settings` . Rename it`Fetch jobs` .
2. **Method:** GET
3. **URL:**

```
https://rest.arbeitsagentur.de/jobboerse/jobsuche-service/pc/v6/jobs
```

4. Turn on **Send Query Parameters** and click**Add Parameter** six times:

| Name | Value | 
|---|---|
| `was` | drag `jobTitle` from the left panel | 
| `wo` | drag `city` | 
| `umkreis` | drag `radiusKm` | 
| `veroeffentlichtseit` | drag `daysBack` | 
| `angebotsart` | `1` | 
| `size` | `25` | 

1. Turn on **Send Headers** and add one header:
  - **Name:**`X-API-Key`
  - **Value:**`jobboerse-jobsuche`
2. Click **Execute step** .

**What's going on here:**

- The parameter names are German because it's a German API. `was` is "what",`wo` is "where",`umkreis` is the radius.
- `angebotsart` 1 means regular jobs, not apprenticeships.
- When you drag a field in, it turns into an expression, the part with curly braces. That's n8n saying: take this value from an earlier step.
- The header works like a visitor badge. This one is public, it's the same for everyone.

**Checkpoint:** one item that contains `ergebnisliste`, a list of job postings.

**Stuck?**

- **No `ergebnisliste` in the output:** no jobs were found. Try a bigger radius, more days, or a broader job title.
- **Error 401 or 403:** check the URL, and the header name and value for typos.

**Goal:** one item per job, with only the fields you need.

Right now, all 25 jobs are packed into one item. But the AI should look at each job separately. So you split the list: one job becomes one item, and every node after this runs once per job.

1. Add a **Split Out** node. Rename it`Split jobs` .
2. **Fields To Split Out:**`ergebnisliste` (type it or drag it in)
3. Click **Execute step** .

**Checkpoint:** the output now shows many items, one per job.

Each job has a lot of fields you don't need. Now keep only the useful ones and give them simple names.

1. Add an **Edit Fields (Set)** node. Rename it`Clean up jobs` .
2. For each row: click **Add Field** , type the name, choose the type from the table, then drag the value from the left panel.

| Name | Type | Value | 
|---|---|---|
| `title` | String | drag `stellenangebotsTitel` | 
| `occupation` | String | drag `hauptberuf` | 
| `employer` | String | drag `firma` | 
| `city` | String | open `stellenlokationen` >`adresse` , drag`ort` | 
| `jobId` | String | drag `referenznummer` | 
| `link` | String | paste the link expression (see below) | 
| `careerChangerFriendly` | Boolean | drag `quereinstiegGeeignet` | 
| `remotePossible` | Boolean | drag `homeofficemoeglich` | 

Link expression to paste into the `link` value:

```
{{ $json.externeURL || 'https://www.arbeitsagentur.de/jobsuche/jobdetail/' + $json.referenznummer }}
```

1. Click **Execute step** .

**What's going on here:**

- `referenznummer` is each job's unique ID. You call it`jobId` . You'll need it later, so the workflow remembers which jobs it already sent.
- Many jobs on the Arbeitsagentur come from partner job boards. Those only exist on the partner's site, so the API gives you an `externeURL` for them. The link expression says: if there's an external link, use it. If not, use the Arbeitsagentur page.
- A job can have several locations. For the city, you simply take the first one.
- Two fields are especially useful for you: `quereinstiegGeeignet` means the job is suitable for career changers, and`homeofficemoeglich` means remote work is possible. The AI will use both for the score.

**Checkpoint:** every item has exactly these eight fields. Open one link in the browser: it should show the job posting.

**Stuck?**

- **No `stellenangebotsTitel` field:** use`hauptberuf` for the title instead.
- **Link shows "Dieses Stellenangebot gibt es nicht oder nicht mehr":** check that the`link` value is exactly the expression above. If it is, the posting was taken down in the meantime. Try another one.

**Goal:** AI reads each job, compares it to your profile, and gives a score from 1 to 10 with a short reason.

The AI makes a judgment call here, which a normal rule couldn't do. That's what makes this an AI automation.

**First, a limit**, so testing doesn't use up your free Gemini quota:

1. Add a **Limit** node. Rename it`Limit` .
2. **Max Items:**`5`
3. Click **Execute step** .

**Now the AI node:**

1. Add an **Information Extractor** node. Rename it`Score with AI` .
2. Click into **Text** . If there's already something in it, delete it.
3. Type the labels below and drag in the matching fields. For `profile` , scroll down in the left panel to`Search settings` , where every earlier node is listed.

```
Candidate profile:
[drag profile from Search settings]

Job posting:
Title: [drag title]
Occupation: [drag occupation]
Employer: [drag employer]
Location: [drag city]
Suitable for career changers: [drag careerChangerFriendly]
Remote work possible: [drag remotePossible]
```

4. **Schema Type:** From Attribute Descriptions
5. Add two attributes:

| Name | Type | Required | Description | 
|---|---|---|---|
| `score` | Number | Yes | see below | 
| `reason` | String | Yes | `One short sentence explaining the score.` | 

Description for `score`:

```
How well this job fits the candidate profile, from 1 (no fit) to 10 (perfect fit). Be strict: give 7 or more only if the job clearly matches the candidate's goals and background. If the candidate is changing careers, rate jobs suitable for career changers higher. If the candidate prefers remote or hybrid work, take into account whether remote work is possible.
```

1. Close the node panel. On the canvas, below `Score with AI` , click the**+** labeled**Model** (or**Chat Model** ) and choose**Google Gemini Chat Model** .
2. **Credential:****Create new credential** . Paste your Gemini API key and leave**Host** as it is. Save.
3. **Model:** pick a Flash model.
4. Close the panel, open `Score with AI` again, and click**Execute step** .

**What's going on here:**

- The Information Extractor lets you describe what you want back, and gives you clean fields instead of a wall of text.
- The descriptions are your prompt. The clearer they are, the better the scores.
- The API only gives you the title, occupation, employer and the two flags, not the full job ad. So the AI is pre-screening. You still decide.
- Your profile goes to Google here, so don't put anything private in it.

**Checkpoint:** five items, each with an `output` containing `score` and `reason`. Read a few: do they make sense?

**Stuck?**

- **Error about `Search settings` that mentions "paired item":** in the Text field, change`.item` to`.first()` in the profile expression. That's safe here, because there's only one`Search settings` item.
- **Error 429 / quota:** wait a minute and run again.
- **Scores feel random:** sharpen the score description or your profile.

**Goal:** each job together with its score, and only the good matches.

The AI node only outputs the score and the reason. The job title and link are gone. That's no problem: n8n remembers which item came from where, so you can reach back to earlier steps and put everything together.

1. Add an **Edit Fields (Set)** node. Rename it`Job + score` .
2. Add six fields with **Add Field** , each with the same name as the field you drag in:
  - From `Clean up jobs` (scroll down in the left panel):`title` ,`employer` ,`city` ,`link` , all type String.
  - From `Score with AI` (inside`output` ):`score` with type**Number** , and`reason` with type String.
3. From 
4. Click **Execute step** .

**Checkpoint:** five items with all six fields, and each job has its *own* title and score.

**Stuck?**

- **"Paired item" error here:** run the whole workflow once with**Execute workflow** (bottom of the canvas), then try again. Don't use`.first()` here, or every job would get the first job's title.

Now the filter. This one is a plain rule, no AI: only jobs with a score of 7 or higher get through.

1. Add a **Filter** node. Rename it`Good matches only` .
2. **Condition:** drag`score` > choose**Number** >**is greater than or equal to** >`7`
3. Click **Execute step** .

**Checkpoint:** the output only has jobs with a score of 7 or more.

If nothing gets through, the workflow is working, there just wasn't a good match. For testing, lower the number to 5 so you have something to email.

**Goal:** one nicely formatted email with all matches, not one email per job.

n8n doesn't have a ready-made node for exactly this layout, so you use a **Code** node. You don't need to know how to code. You need to know how to *ask*.

A good code prompt has four parts: where the code runs, what goes in, what should come out, and how it should look.

Open ChatGPT, Claude or Gemini and paste this prompt:

```
I'm using n8n, the automation tool. Write JavaScript for a Code node
set to "Run Once for All Items".

Input: several items. Each item has these fields: title, employer,
city, link, score (a number from 1 to 10), reason.

Output: exactly ONE item with two fields:
- html: an HTML email listing all jobs, highest score first. For each
  job show the score, the title as a clickable link, the employer and
  city, and the reason in italics.
- count: how many jobs there are.

Keep the code short and simple, with comments explaining each part.
```

Notice that the prompt names the exact fields. The AI can't see your workflow, so you have to describe it.

1. Add a **Code** node after`Good matches only` . Rename it`Build email` .
2. **Mode:** Run Once for All Items.**Language:** JavaScript.
3. Delete the example code and paste in the AI's code.
4. Click **Execute step** .

**Checkpoint:** one single item with `html` and `count`.

If no job passes the filter, n8n simply stops before this node, so you never get an empty email.

**Stuck?**

- **The code throws an error:** copy the error message back into the AI chat and ask it to fix the code. That's normal, even for developers.
- **It still doesn't work:** use this reference version:

``` js
// Get all jobs that passed the filter
const jobs = $input.all().map(item => item.json);

// Best matches first
jobs.sort((a, b) => b.score - a.score);

// One list entry per job
const listItems = jobs.map(job => `
  <li style="margin-bottom:16px">
    <strong>${job.score}/10: <a href="${job.link}">${job.title}</a></strong><br>
    ${job.employer}, ${job.city}<br>
    <em>${job.reason}</em>
  </li>`).join('');

// Put the whole email together
const html = `
  <h2>Job radar: ${jobs.length} new ${jobs.length === 1 ? 'match' : 'matches'}</h2>
  <ul style="padding-left:18px">${listItems}</ul>
  <p style="color:#888">Sent by your n8n job radar</p>`;

// Return ONE item for the email node
return [{ json: { html, count: jobs.length } }];
```

**Goal:** the job radar email in your inbox.

Have your Gmail app password ready (see "Gmail app password, step by step").

**Add the node**

1. On the canvas, click the **+** on the right side of`Build email` .
2. In the search box, type `Send Email` and click**Send Email** .
3. If n8n asks which action you want, choose **Send email** . The node's settings panel opens.
4. Click the node title at the top of the panel and rename it `Send email` .

**Connect your Gmail (this is where the app password goes)**

1. At the top of the panel, find **Credential to connect with** . Click the dropdown and choose**+ Create new credential** .
2. A new window opens, titled something like **SMTP account** . Fill it in:

| Field | What to enter | 
|---|---|
| **User** | your full Gmail address, e.g. `alex.example@gmail.com` | 
| **Password** | **paste the app password here** , exactly as Google shows it, including the spaces | 
| **Host** | `smtp.gmail.com` | 
| **Port** | `465` | 
| **SSL/TLS** | switch on | 

Leave everything else (Disable STARTTLS, Client Host Name) as it is.
7. Click **Save** at the top right of the window. n8n tests the connection right away.
8. Look for a green **Connection tested successfully**. Red error? Check the "Stuck?" tips below.
9. Close the credential window with the **X**. Back in the node, the dropdown now shows your new credential.

**Fill in the email**

1. **From Email:** your Gmail address
2. **To Email:** your Gmail address (or any inbox you check)
3. **Subject:** type`Job radar:` , then drag`count` from the left panel after it, then type `new matches`
4. **Email Format:** HTML
5. **HTML:** drag`html` from the left panel
6. Click **Add option** >**Append n8n Attribution** , and switch it off.
7. Click **Execute step** .

**Checkpoint:** the email arrives within a minute. Check your spam folder if it doesn't.

**Stuck?**

- **"Invalid login" / "Username and Password not accepted":** it has to be the app password, not your normal Gmail password. Paste it exactly as Google shows it, including the spaces, and check that**User** is the same Gmail account you created the app password in.
- **Can't create an app password:** see "If it says 'The setting you are looking for is not available for your account'" in the Gmail section.

**Goal:** the workflow only lets through jobs it hasn't seen before.

Right now, if this runs every day, you'd get the same jobs again and again. So you give the workflow a memory: it notes every job ID it has seen and only lets new ones through.

1. Hover over the line between `Clean up jobs` and`Limit` , click**+** , and search**Remove Duplicates** . n8n shows three options. Choose**Remove items processed in previous executions** . Rename the node`Remember seen jobs` .
2. Check that **Operation** shows**Remove Items Processed in Previous Executions** .
3. **Keep Items Where:** Value Is New
4. **Value to Dedupe On:** drag`jobId`
5. Click **Execute workflow** (bottom of the canvas) twice. Wait for the first run to finish before you start the second.

**Checkpoint:** on the second run, `Remember seen jobs` outputs 0 items, nothing after it runs, and no email is sent. That's exactly what you want: from now on, only new jobs get through.

**Good to know:** `Remember seen jobs` comes before `Limit`, so jobs cut off by the limit are also marked as seen and never scored. If you regularly get more new jobs per day than your limit, make your search more specific or raise the limit.

**Stuck?**

- **You want to test again with the same jobs:** select`Remember seen jobs` and press**D** to deactivate it. Press**D** again before publishing.

**Goal:** the workflow runs by itself every morning.

You add a second trigger, a schedule. The manual trigger stays, so you can still test by hand.

1. Click the **+** at the top right of the canvas, search**Schedule Trigger** , and add it. Rename it`Every morning` .
2. **Trigger Interval:** Days.**Days Between Triggers:**`1` .** Trigger at Hour:** 8am.**Trigger at Minute:**`0` .
3. Drag a line from the output dot of `Every morning` to the input of`Search settings` .
4. Switch to daily mode:
  - In `Search settings` , change`daysBack` to`1` .
  - In `Limit` , change**Max Items** to`10` .
  - In `Good matches only` , set the number back to`7` if you lowered it.
  - Pressed **D** on`Remember seen jobs` earlier? Press it again to switch it back on.
5. In 
6. Click **Publish** (top right).

**Checkpoint:** the workflow shows as published.

**Good to know:**

- Published means the workflow now runs on its own.
- If you change anything later, click **Publish** again. Otherwise the schedule keeps using the old version.
- Running locally has one catch: your laptop, Docker and the n8n terminal have to be on at 8 a.m. That's the price of free and private.
- To check if it ran, open the **Executions** tab of your workflow.
- To pause it, open the three-dot menu at the top right and unpublish.

| Problem | Fix | 
|---|---|
| localhost:5678 doesn't load | Is the terminal with n8n still open? Is Docker Desktop running? | 
| Workflow gone after restart | The start command needs the `-v n8n_data:...` part. Run exactly the command from Part 1. | 
| HTTP Request: no `ergebnisliste` | No results. Broaden the search: bigger `radiusKm` , more`daysBack` , simpler`jobTitle` | 
| Field missing (e.g. `stellenangebotsTitel` ) | Check the actual field names in the `Fetch jobs` output and drag those instead | 
| Can't find a field from an earlier node | Scroll down in the left panel. Earlier nodes are listed further down. Run them first if they show no data. | 
| "Paired item" error with `Search settings` | Change `.item` to`.first()` in that expression | 
| "Paired item" error in `Job + score` | Run the whole workflow with Execute workflow. Don't use `.first()` there. | 
| Gemini error 429 | Free quota reached. Wait a minute, and keep `Limit` at 5–10 | 
| Nothing after the Filter | No job scored 7 or more. Lower the number to test, or broaden the search | 
| Email not arriving | Check spam. A login error means the app password is wrong | 
| Second test run sends nothing | That's `Remember seen jobs` working. Press D on it to test again | 
| Schedule ignores recent changes | Changes only count after you publish again | 
| Schedule didn't run | Laptop asleep, Docker closed, or the n8n terminal was closed at 8 a.m. Check the Executions tab. | 

1. Where does AI make a decision in this workflow, and where does it just follow rules?
2. What would happen without the `Remember seen jobs` node?
3. What are the limits of the AI pre-screening?
4. Why run n8n locally instead of in the cloud? What are the pros and cons?

- **Several searches:** duplicate the workflow with a different job title or city.
- **Better scoring:** fetch each job's full description from the Arbeitsagentur details endpoint and give it to the AI. Hint: ask AI how to base64-encode`jobId` in an n8n expression. Test the details endpoint first, since the API changes without notice.
- **Weekly summary:** add a second schedule that sends a Friday overview instead of daily emails.
