Tools
Job Openclaw
Deterministic job-search pipeline (jobctl) packaged as an installable OpenClaw plugin, with the Job Discovery skill and a local web dashboard.
Install
npm install
npm
Configuration Example
{
"source": "linkedin",
"external_job_id": "4201234567",
"company": "Acme Labs",
"title": "Applied AI Engineer",
"location": "New York, NY",
"url": "https://www.linkedin.com/jobs/view/4201234567/",
"description": "Full posting text."
}
README
# jobctl: a job-search pipeline for OpenClaw
`jobctl` is a small, deterministic command-line tool that keeps the state of a job search: which postings were found, which were filtered out, how each was scored, which resume version belongs to which application. It ships with a **Job Discovery skill** for [OpenClaw](https://docs.openclaw.ai) that browses LinkedIn and public ATS boards, judges fit, and posts a digest of matches.
The split is deliberate. The LLM does what only an LLM can do (read messy pages, judge fit). `jobctl` does everything that must be reliable: state transitions, deduplication, filters, score arithmetic, and enforcement of disqualifiers. The agent never touches the database.
> **Status:** this is the first of five planned pieces (discovery and state; resume tailoring; application filling; scheduling; human-in-the-loop policy). Only discovery, scoring and resume *tracking* exist today. Nothing here applies to jobs on your behalf.
## How it works
```text
OpenClaw agent + job-discovery skill
| reads pages, extracts job JSON, judges fit
v
jobctl -- rules and state -- SQLite (data/jobs.db)
| dedupe - prefilter - score - hard gaps - resume versions - events
v
Discord digest / applications.xlsx
```
Job lifecycle:
```text
DISCOVERED --score--> SHORTLISTED | REJECTED_LOW_MATCH | REJECTED_HARD_GAP
REJECTED_* --promote--> SHORTLISTED
SHORTLISTED --attach-resume--> RESUME_READY (or via RESUME_PENDING)
SHORTLISTED | RESUME_PENDING | RESUME_READY --dismiss--> DISMISSED
```
Jobs rejected by the deterministic prefilter (excluded title words, no keyword match, wrong location) are stored as `REJECTED_FILTERED` so re-fetching a board never re-evaluates them.
## Requirements
- Node.js **22.18 or newer** — the CLI runs TypeScript directly and uses the built-in `node:sqlite`, so it needs no build step; the plugin ships prebuilt JavaScript and loads on whatever Node version OpenClaw itself requires
- [OpenClaw](https://docs.openclaw.ai) with a browser profile signed in to LinkedIn (only needed for the LinkedIn pass) and, optionally, a Discord channel configured
The plugin needs no extra runtime dependencies beyond the ones in `package.json`; OpenClaw installs them for you.
## Install
### As an OpenClaw plugin (recommended)
Install the packaged plugin, then point it at a directory and give it your profile:
```bash
openclaw plugins install git:github.com/SiddarthaKoppaka/job-openclaw --force --accept-capabilities
openclaw plugins enable jobctl
```
Whatever the source, `--force` confirms a non-ClawHub package and `--accept-capabilities` accepts its declared capabilities. A released tarball installs the same way (`openclaw plugins install npm-pack:./jobctl-0.1.0.tgz`), as does a local clone (`openclaw plugins install ./job-openclaw`).
Then create the home directory the plugin reads, and fill in the templates:
```bash
mkdir -p ~/.jobctl/profile
cp profile/preferences.example.yaml ~/.jobctl/profile/preferences.yaml
cp profile/boards.example.yaml ~/.jobctl/profile/boards.yaml
cp profile/profile.example.yaml ~/.jobctl/profile/profile.yaml
# ...edit all three...
```
`~/.jobctl` is the default. To keep the home somewhere else, set it in the Gateway config:
```json5
{
plugins: {
entries: {
jobctl: { config: { home: "/path/to/job-home" } },
},
},
}
```
The plugin registers 13 agent tools (`jobctl_where`, `jobctl_list`, `jobctl_get`, `jobctl_add_job`, `jobctl_fetch_ats`, `jobctl_score_job`, `jobctl_promote`, `jobctl_dismiss`, `jobctl_set_status`, `jobctl_attach_resume`, `jobctl_mark_notified`, `jobctl_log_event`, `jobctl_export_xlsx`) and ships the **Job Discovery** skill, so the agent can run the whole pipeline without a shell. Check it loaded with:
```bash
openclaw plugins inspect jobctl --runtime
openclaw skills list # job-discovery should be listed
```
Verify it works, then ask your agent to "run job discovery" (or call `jobctl_where` directly):
```bash
openclaw plugins uninstall jobctl # removes the plugin and its tools
```
### From a checkout (for development)
`jobctl` is also a standalone CLI. It runs TypeScript directly, so there is no build step for the CLI itself:
```bash
npm install
npm link # puts `jobctl` on your PATH
cp profile/preferences.example.yaml profile/preferences.yaml
cp profile/boards.example.yaml profile/boards.yaml
cp profile/profile.example.yaml profile/profile.yaml
export JOBCTL_HOME="$PWD" # add this to your shell profile
jobctl where # confirm the resolved paths
jobctl fetch-ats # pull public ATS boards
jobctl list --status discovered
openclaw skills install ./skills/job-discovery
openclaw skills list # job-discovery should appear
```
Run `npm run build` to regenerate the plugin's `dist/` (see [docs/plugin.md](docs/plugin.md)); `dist/` is committed so plugin installs never need a build step.
## Quick start
With either install, the everyday commands are the same. `JOBCTL_HOME` must be visible to the environment the agent runs `jobctl` in; if the agent cannot find your config, ask it to run `jobctl where`.
## Commands
| Command | What it does |
|---|---|
| `add-job <file\|->` | Add a job from JSON; dedupes, prefilters, stores |
| `fetch-ats [--board X]` | Fetch the Greenhouse, Lever and Ashby boards in `boards.yaml` |
| `score <job> --factors <file\|->` | Score a `DISCOVERED` job from a factor breakdown |
| `list` | Filter by `--status --company --source --external-id --min-score --unnotified --since --limit` |
| `get <job>` | Full record including resume versions |
| `set-status <job> <status>` | Legal transitions that have no dedicated command |
| `promote <job>` / `dismiss <job>` | Override a rejection / drop a job |
| `attach-resume <job> <pdf> [--docx path]` | Record a resume version for a job |
| `mark-notified <job...>` | Record that jobs were reported |
| `log-event --type T` | Add a run-level event (for example `SOURCE_BLOCKED`) |
| `export-xlsx [--all]` | Write `data/exports/applications.xlsx` (prefilter rejections left out unless `--all`) |
| `where` | Print resolved paths |
`export-xlsx` skips jobs the prefilter rejected (`REJECTED_FILTERED`) by default, since on a big board they are most of the rows; pass `--all` to export every job.
Output is always JSON on stdout. Errors are JSON on stderr; exit code 1 for domain errors, 2 for usage errors. See [docs/architecture.md](docs/architecture.md) for the internals, [docs/plugin.md](docs/plugin.md) for the OpenClaw plugin, [docs/skill-guide.md](docs/skill-guide.md) for how the skill works and how to change it, [docs/troubleshooting.md](docs/troubleshooting.md) when something goes wrong, and [docs/dashboard.md](docs/dashboard.md) for the web dashboard.
### Input formats
`add-job -` reads one job as JSON on stdin (`source` is `linkedin`, `greenhouse`, `lever` or `ashby`; `url` must be an http(s) URL):
```json
{
"source": "linkedin",
"external_job_id": "4201234567",
"company": "Acme Labs",
"title": "Applied AI Engineer",
"location": "New York, NY",
"url": "https://www.linkedin.com/jobs/view/4201234567/",
"description": "Full posting text."
}
```
`score <job> --factors -` reads the whole scoring payload, not just the factors. `required_skills` and `preferred_skills` are optional:
```json
{"factors": {"required": 0.9, "preferred": 0.7, "experience": 0.8, "education": 1, "domain": 0.7}, "hard_gaps": []}
```
Passing only the inner `factors` object (without the `{"factors": ...}` wrapper) fails with `INVALID_FACTORS`.
## Scoring
The agent reports five factors (required skills, preferred skills, experience, education, domain), each from 0 to 1, plus a list of hard gaps. `jobctl` computes the overall score with the weights in `preferences.yaml` and decides the outcome:
- any hard gap: `REJECTED_HARD_GAP`, whatever the score;
- otherwise a score at or above `minimum_match_score` (default 0.72): `SHORTLISTED`;
- otherwise `REJECTED_LOW_MATCH`.
Hard gaps are detected by the agent, and `jobctl` adds a deterministic backstop that scans the description for security-clearance requirements, citizenship requirements, and years of experience above `max_years_ceiling`. Only an explicit `promote` overrides a rejection. Details are in [docs/configuration.md](docs/configuration.md).
## Dashboard
A lightweight local web dashboard shows the pipeline in real time and lets you promote, dismiss or queue jobs for a resume, all through `jobctl`'s own rules:
```bash
npm run web:install # once
npm run web:build
npm run web:start # http://127.0.0.1:7420
```
It reads the same `data/jobs.db`, updates by itself when anything (a `jobctl` command, an agent run, a click) changes the database, and listens on `127.0.0.1` only. See [docs/dashboard.md](docs/dashboard.md) for options, the security model and the API.
## Data and privacy
`jobctl`'s own state (the database, the exports and your profile files) stays on your machine; see [Safety notes](#safety-notes) for what does leave it: job text and `profile.yaml` go to your model provider, and the digest goes to Discord. `data/` (the SQLite file and exports) and `profile/*.yaml` (your preferences, boards and background) are gitignored. The repository ships only `*.example.yaml` templates. No credentials are stored anywhere; LinkedIn access uses the OpenClaw browser profile you already signed in with.
## Safety notes
- **LinkedIn.** Automated browsing may conflict with LinkedIn's terms of service and can put your account at risk of restriction. You are responsible for how you use it. The skill caps new jobs per run (`linkedin.max_new_per_run`), paces itself, and stops at any login wall or CAPTCHA, but that cap is guidance to the agent, not a technical limit.
- **Your data.** `profile.yaml` and the text of the postings the agent reads are sent to your model provider as part of scoring, and the digest is pos
... (truncated)
tools
Comments
Sign in to leave a comment