# Scan Job Portals

Source: https://career-ops.org/docs/introduction/guides/scan-job-portals (canonical HTML, identical content)

> Set up and run the portal scanner to find new job openings automatically.



This guide walks through setting up `portals.yml` and running the portal scanner to find new job openings automatically.

> **Before you start**
Make sure you have completed the [Quick Start](https://career-ops.org/docs.md) guide. You need `cv.md` and `modes/_profile.md` in place before scanning is useful.

## Set up portals.yml [#set-up-portalsyml]

`portals.yml` is the file that controls what the scanner looks for and where it looks. You create it once from the included template, then edit it to match your target roles.

Run this command from the project folder:

```bash title="Terminal"
cp templates/portals.example.yml portals.yml
```

Open `portals.yml` in any text editor. The file has three sections. The steps below walk through each one.

### Configure your title filter [#configure-your-title-filter]

The title filter is how the scanner decides if a job is worth showing you. It checks the job title against two lists of keywords.

Find the `title_filter` section near the top of the file:

```yaml title="portals.yml"
title_filter:
  positive:
    - "AI"
    - "ML"
    - "Product Manager"
  negative:
    - "Junior"
    - "Intern"
  seniority_boost:
    - "Senior"
    - "Staff"
    - "Lead"
```

**Replace the `positive` list** with keywords that match your target roles. A job must match at least one keyword from this list to pass through. The check is not case-sensitive, so `"AI"` also catches `"ai engineer"`.

**Replace the `negative` list** with keywords that rule a job out. If any of these appear in the title, the job is skipped — even if it matched a positive keyword.

**Leave `seniority_boost` as is** unless you want to add or remove seniority levels. These keywords do not filter anything out — they just move matching jobs higher in the results.

**Example:** If you are looking for Rails engineering roles, your filter might look like this:

```yaml title="portals.yml"
title_filter:
  positive:
    - "Rails"
    - "Ruby"
    - "Full Stack"
    - "Backend"
  negative:
    - "Junior"
    - "Intern"
    - "PHP"
    - "Java"
  seniority_boost:
    - "Senior"
    - "Staff"
    - "Lead"
```

> **Tip**
Start with a small `positive` list of three to five keywords. You can always add more later. A long list makes it harder to see why a job passed or failed.

### Add companies to track [#add-companies-to-track]

The `tracked_companies` section is a list of specific companies you want the scanner to check every time. The scanner goes directly to each company's job page and reads the open roles.

Find the `tracked_companies` section near the bottom of the file. Each entry looks like this:

```yaml title="portals.yml"
tracked_companies:
  - name: Anthropic
    careers_url: https://job-boards.greenhouse.io/anthropic
    api: https://boards-api.greenhouse.io/v1/boards/anthropic/jobs
    enabled: true

  - name: OpenAI
    careers_url: https://openai.com/careers
    enabled: true
```

**To add a company**, copy an existing entry and change the values:

| Field         | What to put                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------------- |
| `name`        | The company name as you want it to appear in results                                               |
| `careers_url` | The direct URL to the company's job listings page                                                  |
| `api`         | Optional. The Greenhouse API URL if the company uses Greenhouse. Leave it out if you are not sure. |
| `enabled`     | Set to `true` to include the company or `false` to skip it without deleting the entry.             |

**To find a company's `careers_url`**, go to the company's website, click Careers or Jobs, and copy the URL of the page that lists open roles. That is the URL to use.

**To disable a company without deleting it**, set `enabled: false`:

```yaml title="portals.yml"
  - name: Stripe
    careers_url: https://stripe.com/jobs/search
    enabled: false
```

> **Note**
Every company in `tracked_companies` must have a `careers_url`. Without it, the scanner has no page to visit and will skip that entry.

### Review the search queries [#review-the-search-queries]

The `search_queries` section runs broader web searches to find roles at companies not in your tracked list. The template includes many pre-built queries.

You do not need to change this section to get started. The pre-built queries cover major job boards including Greenhouse, Ashby, among others.

When you are ready to customize, each query looks like this:

```yaml title="portals.yml"
search_queries:
  - name: Greenhouse — Rails Engineer
    query: 'site:job-boards.greenhouse.io "Rails Engineer" OR "Ruby on Rails" remote'
    enabled: true
```

To turn off a query you do not need, set `enabled: false`. To add a new one, copy an existing entry, give it a new `name`, and update the `query` text.

## Run a scan [#run-a-scan]

There are two ways to run a scan. Use the one that fits what you need.

### Option A — Run the script directly [#option-a--run-the-script-directly]

```bash title="Terminal"
npm run scan
```

Run this from the project folder. The script reads `portals.yml`, hits each company's API directly, and writes any new jobs to `data/pipeline.md`. It uses no AI tokens and runs in about 30 seconds.

This option only works for companies that use Greenhouse, Ashby, or Lever. Companies without one of those platforms are skipped.

**Preview results before saving them:**

```bash title="Terminal"
npm run scan -- --dry-run
```

This runs the full scan but does not write anything to disk. Use it to check your filter settings before committing results.

**Scan a single company:**

```bash title="Terminal"
npm run scan -- --company Anthropic
```

Replace `Anthropic` with any company name from your `tracked_companies` list. The match is not case-sensitive.

### Option B — Run the AI-powered scan inside Claude Code [#option-b--run-the-ai-powered-scan-inside-claude-code]

Open Claude Code in the project folder and type:

```bash title="Claude Code"
/career-ops scan
```

This version visits each company's job page directly using a browser, so it works even for companies that do not have a public API. It also runs the `search_queries` from your `portals.yml` to find roles at companies not in your tracked list.

Use this option when:

* A company in your list does not use Greenhouse, Ashby, or Lever
* You want to discover new companies, not just check known ones
* The script scan missed something you expected to see

> **Tip**
Option B uses Claude API tokens and takes longer than the script. For a quick daily check, Option A is faster.

## Read the scan output [#read-the-scan-output]

When the scan finishes, you will see a summary like this:

```text title="Terminal"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Portal Scan — 2026-04-13
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Companies scanned:     42
Total jobs found:      318
Filtered by title:     291 removed
Duplicates:            4 skipped
New offers added:      23

New offers:
  + Anthropic | AI Engineer | San Francisco, CA
  + ElevenLabs | Solutions Architect | Remote
  ...

→ Run /career-ops pipeline to evaluate new offers.
```

Here is what each line means:

| Line                   | What it tells you                                                |
| ---------------------- | ---------------------------------------------------------------- |
| **Companies scanned**  | How many companies from `tracked_companies` were checked         |
| **Total jobs found**   | All open roles seen across those companies, before any filtering |
| **Filtered by title**  | Jobs removed because the title did not match your `title_filter` |
| **Duplicates skipped** | Jobs already in your pipeline or tracker — not added again       |
| **New offers added**   | Jobs that passed all filters and are new to you                  |

If **New offers added** is 0, your title filter may be too strict. Try adding a keyword to your `positive` list and run the scan again.

**Where new jobs go:** Each new offer is added as a line in `data/pipeline.md`. That is the list of jobs waiting to be evaluated.

**What `data/scan-history.tsv` does:** Every URL the scanner sees — whether it was added, filtered, or skipped — gets logged here. This is how the scanner avoids showing you the same job twice across future scans. You do not need to edit this file.

## Evaluate new offers from a scan [#evaluate-new-offers-from-a-scan]

After a scan adds new offers to `data/pipeline.md` — your list of pending job offers — run the pipeline command to evaluate each one against your profile. This step uses Claude to read the job description, compare it against your `cv.md` and `modes/_profile.md`, and produce a scored summary for each role.

Open Claude Code in the project folder and type:

```bash title="Terminal"
/career-ops pipeline
```

Claude reads every pending URL in `data/pipeline.md`, visits each job posting, and evaluates it. Results come back in batches. Each batch shows a table like this:

Batch 1 done (Anthropic, ElevenLabs, Mistral):

| #   | Company    | Role                | Score | Legitimacy                       |
| --- | ---------- | ------------------- | ----- | -------------------------------- |
| 024 | Anthropic  | AI Engineer         | 4.8/5 | High Confidence                  |
| 025 | ElevenLabs | Solutions Architect | 4.1/5 | High Confidence — fast-growing   |
| 026 | Mistral AI | Product Manager     | 3.2/5 | Proceed w/ Caution — role closed |

Here is what each column means:

| Column         | What it tells you                                                                                                                                                                            |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **#**          | The report number. Use it to find the full report in `reports/` — for example, report 024 is `reports/024-anthropic-2026-04-13.md`                                                           |
| **Score**      | How well the role fits your profile, from 1.0 to 5.0                                                                                                                                         |
| **Legitimacy** | Whether the posting looks real and active. "High Confidence" means the job is live and the details check out. "Proceed w/ Caution" means something raised a flag — read the note next to it. |

**To read the full evaluation for a role**, you have two options:

* **Open the report file directly** — find it in the `reports/` folder using the number from the table.
* **Use the dashboard** — run `./dashboard/career-dashboard` from the project folder to browse all your reports in a terminal interface. You can filter by score and status, and open any report without leaving the terminal.

## What to do next [#what-to-do-next]

When `/career-ops pipeline` finishes, each job has a score from 1.0 to 5.0. You can find the full report for any role in the `reports/` folder. The score shows how well the role matches your profile.

Use the score as your starting point:

| Score             | Recommended action                                                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **4.5 or higher** | Strong match. Run `/career-ops apply` right away.                                                                                     |
| **4.0 – 4.4**     | Good fit. Run `/career-ops apply`. You can also run `/career-ops contacto` first to reach out to someone on the team before applying. |
| **3.5 – 3.9**     | Could go either way. Run `/career-ops deep` to learn more before you decide.                                                          |
| **Below 3.5**     | Skip it unless you have a specific reason to apply.                                                                                   |

Here is what each command does:

* **`/career-ops apply`** — Opens the application form and writes answers tailored to your profile. It stops before submitting so you can review everything first.
* **`/career-ops contacto`** — Finds a hiring manager or team member on LinkedIn and drafts a short outreach message for you to send.
* **`/career-ops deep`** — Researches the company and the role in depth. Use this when you are on the fence and want more information before deciding.
* **`/career-ops tracker`** — Shows the status of every role in your pipeline. Run this any time you want to see where things stand.

> **Tip**
You do not have to go in order. If a role scores 4.8 but you have never heard of the company, run `/career-ops deep` first. The commands work on their own — use which ever one fits your situation.

