# Resume-Aware Job Matcher (`congruent_import/resume-job-matcher`) Actor

Rank raw job postings against your resume keywords. Takes a batch of job rows plus your core/secondary/bonus skill buckets and returns a scored, filtered and ranked shortlist with match reasons.

- **URL**: https://apify.com/congruent\_import/resume-job-matcher.md
- **Developed by:** [Sanjay Pv](https://apify.com/congruent_import) (community)
- **Categories:** Jobs
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / actor start

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Resume-Aware Job Matcher

Rank a batch of raw job postings against your resume and get back a scored,
filtered, ranked shortlist — with the reason every job was kept or dropped.

This actor is a thin, data-driven wrapper around the **job-hunt** matching
engine (the same classifier that built a daily 9-month React/Software
Engineer/MERN shortlist). It takes the *matched + dropped* decision logic in
`engine/` and exposes it over Apify's pay-per-result API, so you can plug it
into any job pipeline (a scraper's dataset, a CSV export, a webhook, or a
manual paste) without writing a single filter rule.

### What it does

Given an array of raw job rows plus your skill buckets, the actor:

1. **Scores** every job against your resume — weighted core / secondary /
   bonus buckets (60 / 30 / 10 by default).
2. **Rejects** classes of junk using the production filters:
   - mis-extracted / garbage titles
   - excluded title keywords (QA, Android, iOS, DevOps, Java, Angular, …)
   - senior / lead / principal titles
   - JDs that declare a senior role
   - **required foreign stack in the JD** (with the title-trust escape: a
     title that names your target stack survives a python/angular/java/etc.
     JD requirement; a generic title does not)
   - wrong-stack titles
   - experience-band mismatch (internships, fresher-only, and above your band)
   - scam / engagement-farming / vendor-staffing patterns
   - posting-age window (optional)
3. **Labels** each keeper: region, discipline, experience band.
4. **Ranks** the survivors by match score with region / band / discipline
   tie-breaks, collapses duplicates, and caps the shortlist.
5. **Charges pay-per-result** — you are billed per ranked job delivered, not
   per run.

### Input

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `jobs` | array<object> | yes | Raw postings. Aliases for common fields (`title`/`role`/`position`, `description`/`body`, `applyUrl`/`url`, `company`, `location`, `postedDate`, `experience`, `workMode`, `salary`, `email`, `phone`, `source`). Field matching is case-insensitive. |
| `resumeCore` | array<string> | yes | Must-have skills (e.g. `React, JavaScript, TypeScript, HTML, CSS`) |
| `resumeSecondary` | array<string> | no | Strong signals (e.g. `Redux, Node.js, Express, MongoDB`) |
| `resumeBonus` | array<string> | no | Nice-to-have (e.g. `Next.js, Jest, Vite`) |
| `targetYears` | number | no | Your experience, used for band ranking (default 3) |
| `minScore` | integer | no | Drop below this score (default 25) |
| `maxResults` | integer | no | Shortlist cap, 1–500 (default 50) |
| `maxAgeHours` | integer | no | Drop jobs older than this when a date is present (0 = off) |
| `allowedRegions` | array<string> | no | Keep only locations containing any of these |
| `excludeTitleKeywords` | array<string> | no | Overrides the tuned title exclusions |
| `seniorTitleWords` | array<string> | no | Overrides the seniority markers |
| `jdStackRejects` | array<string> | no | Overrides JD-required foreign stacks |
| `wrongStack` | array<string> | no | Overrides wrong-stack words |
| `excludeInternships` | boolean | no | Drop internship/trainee/apprentice (default true) |
| `acceptFreshers` | boolean | no | Keep 0-experience roles (default false) |
| `includeDropped` | boolean | no | Emit a per-job dropped report with reasons |

All list fields ship with sensible tuned defaults, so sending only `jobs` +
`resumeCore` gets you a working run.

### Output

The dataset is populated with:

- one **`ranked_job`** record per keeper — rank, `matchScore`, title, company,
  location, region, discipline, band, matched/missing skills, apply URL,
  salary, email, phone, notes, and a trimmed description;
- one **`summary`** record — counts, `minScore`, `maxResults`, top result;
- when `includeDropped` is true, one **`dropped_report`** record — counts by
  reason plus up to 200 per-job entries with their drop reason (great for
  tuning your input).

### Pricing model

This actor charges **per result**: `Actor.charge("ranked-jobs", count=kept)`.
You pay only for jobs that make the shortlist, so a huge input with few
matches is cheap and a dense input is priced per useful row. The Apify
platform revenue share pays the developer (you, on the published actor's
owner payouts). To tune price: pick a per-result event price in **Pay per
event & result** under the actor's monetization settings, sized against the
value of a matched opening (typical tier: $0.05–$0.20 per ranked result).

> Note on the pay-per-event model: with rental-based revenue being retired
> (no new rentals after April 1, 2026), price via **pay per event / pay per
> result** — it is the long-lived revenue surface.

### Local development

```bash
## 1. Create a venv and install deps
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt   # Windows
## or: python -m pip install -r requirements.txt (after venv activate)

## 2. Run against a sample input WITHOUT Apify (no push, no charge)
python -m src.main --input sample_input.json

## 3. Run under the Apify CLI (pulls your input, pushes to local storage)
apify login
apify run

## 4. Deploy
apify push
```

The engine side is self-contained in `engine/` (filters, matcher, ranking,
fingerprint, models). At runtime the actor re-synthesizes `filters.yaml`
from the Apify input before importing the engine, so the exact production
regex tables are rebuilt for your run — the vendored `filters.yaml` is the
tuned baseline.

### Publish checklist

1. `apify login` and `apify push` from this directory.
2. In **Monetization → Pay per event & result**, add event `ranked-jobs`
   with your per-result price and a reasonable run budget.
3. Write a store listing describing the input shape and the pricing above.
4. Add a sample dataset (e.g. from any public job scraper) so trial runs
   work out of the box.
5. Monitor the store page's **Cost of results** and adjust the per-result
   price (≈14-day notice, ~once/month) against margin.

### Layout

```
.actor/actor.json       actor metadata + output schema
.actor/input_schema.json  input form
engine/                 vendored job-hunt engine (filters, matcher, ranking)
src/main.py             actor entry: synthesize config -> score -> rank -> push+charge
sample_input.json       local smoke-test input
Dockerfile              apify/actor-python base
requirements.txt        apify + yaml + pydantic-settings + rich
```

# Actor input Schema

## `jobs` (type: `array`):

Raw job rows to score and rank. Each row uses the common Apify fields: title, description/body, location, company, url/applyUrl, source, postedDate (ISO or '… days ago'), experience, employmentType/workMode. Fields are matched case-insensitively by common aliases.

## `resumeCore` (type: `array`):

The stack you're actually hired for. A job mentioning most of these scores ~60/100. Example: React, JavaScript, TypeScript, HTML, CSS.

## `resumeSecondary` (type: `array`):

Supporting stack — Node.js, MongoDB, Redux, Git, Tailwind, REST, MERN, etc. Worth ~30 weighted points.

## `resumeBonus` (type: `array`):

Differentiators worth ~10 weighted points: Next.js, Jest, Vite, Figma, Agile, Storybook, etc.

## `targetYears` (type: `number`):

Used to rank the experience band each job expects.

## `minScore` (type: `integer`):

Jobs scoring below this are dropped (0 disables the gate).

## `maxResults` (type: `integer`):

Cap on how many ranked jobs to return.

## `maxAgeHours` (type: `integer`):

Drop jobs posted longer ago than this when a date is provided. 0 = no age filter.

## `allowedRegions` (type: `array`):

Keep only jobs whose location contains at least one of these (case-insensitive substring). Empty = keep all locations.

## `excludeTitleKeywords` (type: `array`):

Titles containing any of these are dropped (QA, Android, iOS, DevOps, Java, Angular, …).

## `seniorTitleWords` (type: `array`):

Titles containing any of these are treated as too senior.

## `jdStackRejects` (type: `array`):

If a JD REQUIRES any of these stacks (and the title doesn't name your target stack), the job is dropped.

## `wrongStack` (type: `array`):

Titles/JDs dominated by these stacks are dropped.

## `excludeInternships` (type: `boolean`):

Drop internship / trainee / apprentice roles.

## `acceptFreshers` (type: `boolean`):

Keep roles aimed at 0-experience candidates.

## `includeDropped` (type: `boolean`):

Emit a second record per dropped job with the drop reason (useful for tuning SCHEMA\_CONFIG).

## Actor input object example

```json
{
  "resumeCore": [
    "React",
    "JavaScript",
    "TypeScript",
    "HTML",
    "CSS"
  ],
  "resumeSecondary": [
    "Redux",
    "Node.js",
    "Express",
    "MongoDB",
    "REST",
    "Git",
    "Tailwind",
    "MERN"
  ],
  "resumeBonus": [
    "Next.js",
    "Jest",
    "Vite",
    "Figma",
    "Agile"
  ],
  "targetYears": 3,
  "minScore": 25,
  "maxResults": 50,
  "maxAgeHours": 0,
  "allowedRegions": [],
  "excludeTitleKeywords": [
    "React Native",
    "QA",
    "Android",
    "iOS",
    "DevOps",
    "Java",
    "Angular"
  ],
  "seniorTitleWords": [
    "senior",
    "sr",
    "lead",
    "principal",
    "staff",
    "architect",
    "director",
    "manager",
    "vp",
    "head of"
  ],
  "jdStackRejects": [
    "php",
    "angular",
    "angularjs",
    "java",
    "c#",
    "c++",
    ".net",
    "dotnet",
    "asp.net",
    "python"
  ],
  "wrongStack": [
    "java",
    "angular",
    "vue",
    ".net",
    "c#",
    "django",
    "golang"
  ],
  "excludeInternships": true,
  "acceptFreshers": false,
  "includeDropped": false
}
```

# Actor output Schema

## `results` (type: `string`):

The ranked shortlist: every job that passed filtering, scored against the resume keywords. One ranked\_job item per job, followed by a summary item and optional dropped\_report.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "resumeCore": [
        "React",
        "JavaScript",
        "TypeScript",
        "HTML",
        "CSS"
    ],
    "resumeSecondary": [
        "Redux",
        "Node.js",
        "Express",
        "MongoDB",
        "REST",
        "Git",
        "Tailwind",
        "MERN"
    ],
    "resumeBonus": [
        "Next.js",
        "Jest",
        "Vite",
        "Figma",
        "Agile"
    ],
    "allowedRegions": [],
    "excludeTitleKeywords": [
        "React Native",
        "QA",
        "Android",
        "iOS",
        "DevOps",
        "Java",
        "Angular"
    ],
    "seniorTitleWords": [
        "senior",
        "sr",
        "lead",
        "principal",
        "staff",
        "architect",
        "director",
        "manager",
        "vp",
        "head of"
    ],
    "jdStackRejects": [
        "php",
        "angular",
        "angularjs",
        "java",
        "c#",
        "c++",
        ".net",
        "dotnet",
        "asp.net",
        "python"
    ],
    "wrongStack": [
        "java",
        "angular",
        "vue",
        ".net",
        "c#",
        "django",
        "golang"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("congruent_import/resume-job-matcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "resumeCore": [
        "React",
        "JavaScript",
        "TypeScript",
        "HTML",
        "CSS",
    ],
    "resumeSecondary": [
        "Redux",
        "Node.js",
        "Express",
        "MongoDB",
        "REST",
        "Git",
        "Tailwind",
        "MERN",
    ],
    "resumeBonus": [
        "Next.js",
        "Jest",
        "Vite",
        "Figma",
        "Agile",
    ],
    "allowedRegions": [],
    "excludeTitleKeywords": [
        "React Native",
        "QA",
        "Android",
        "iOS",
        "DevOps",
        "Java",
        "Angular",
    ],
    "seniorTitleWords": [
        "senior",
        "sr",
        "lead",
        "principal",
        "staff",
        "architect",
        "director",
        "manager",
        "vp",
        "head of",
    ],
    "jdStackRejects": [
        "php",
        "angular",
        "angularjs",
        "java",
        "c#",
        "c++",
        ".net",
        "dotnet",
        "asp.net",
        "python",
    ],
    "wrongStack": [
        "java",
        "angular",
        "vue",
        ".net",
        "c#",
        "django",
        "golang",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("congruent_import/resume-job-matcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "resumeCore": [
    "React",
    "JavaScript",
    "TypeScript",
    "HTML",
    "CSS"
  ],
  "resumeSecondary": [
    "Redux",
    "Node.js",
    "Express",
    "MongoDB",
    "REST",
    "Git",
    "Tailwind",
    "MERN"
  ],
  "resumeBonus": [
    "Next.js",
    "Jest",
    "Vite",
    "Figma",
    "Agile"
  ],
  "allowedRegions": [],
  "excludeTitleKeywords": [
    "React Native",
    "QA",
    "Android",
    "iOS",
    "DevOps",
    "Java",
    "Angular"
  ],
  "seniorTitleWords": [
    "senior",
    "sr",
    "lead",
    "principal",
    "staff",
    "architect",
    "director",
    "manager",
    "vp",
    "head of"
  ],
  "jdStackRejects": [
    "php",
    "angular",
    "angularjs",
    "java",
    "c#",
    "c++",
    ".net",
    "dotnet",
    "asp.net",
    "python"
  ],
  "wrongStack": [
    "java",
    "angular",
    "vue",
    ".net",
    "c#",
    "django",
    "golang"
  ]
}' |
apify call congruent_import/resume-job-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,congruent_import/resume-job-matcher"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/AgDL50JCNluEH3DIN/builds/hhCzQyC2nFhxjPr6R/openapi.json
