docs: add README, update SKILL.md and SPEC.md for current state
- Full README with quick start, configuration tables, status reference, project structure, and roadmap - SKILL.md updated with preview mode, retry logic, constants module - SPEC.md updated with pagination, infinite scroll, retry flow, in-memory caching, config validation, and v1 checklist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
209
README.md
Normal file
209
README.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# claw-apply
|
||||||
|
|
||||||
|
Automated job search and application engine for LinkedIn and Wellfound. Searches for matching roles, applies automatically, and learns from every unknown question it encounters.
|
||||||
|
|
||||||
|
Built for [OpenClaw](https://openclaw.dev) but runs standalone with Node.js.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- **Searches** LinkedIn and Wellfound on a schedule with your configured keywords and filters
|
||||||
|
- **Applies** to matching jobs automatically via LinkedIn Easy Apply and Wellfound's native flow
|
||||||
|
- **Learns** — when it hits a question it can't answer, it messages you on Telegram, saves your reply, and never asks again
|
||||||
|
- **Deduplicates** across runs so you never apply to the same job twice
|
||||||
|
- **Retries** failed applications up to a configurable number of times before giving up
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/MattJackson/claw-apply.git
|
||||||
|
cd claw-apply
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Configure
|
||||||
|
|
||||||
|
Copy the example configs and fill in your values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config/settings.example.json config/settings.json
|
||||||
|
cp config/profile.example.json config/profile.json
|
||||||
|
cp config/search_config.example.json config/search_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
| File | What to fill in |
|
||||||
|
|------|----------------|
|
||||||
|
| `profile.json` | Name, email, phone, resume path, work authorization, salary |
|
||||||
|
| `search_config.json` | Job titles, keywords, platforms, filters, exclusions |
|
||||||
|
| `settings.json` | Telegram bot token + user ID, Kernel profiles, proxy ID |
|
||||||
|
|
||||||
|
### 2. Set up Kernel (stealth browsers)
|
||||||
|
|
||||||
|
claw-apply uses [Kernel](https://kernel.sh) for stealth browser sessions that bypass bot detection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @onkernel/cli
|
||||||
|
|
||||||
|
# Create a residential proxy
|
||||||
|
kernel proxies create --type residential --country US --name "claw-apply-proxy"
|
||||||
|
|
||||||
|
# Create authenticated browser profiles (follow prompts to log in)
|
||||||
|
kernel auth create --name "LinkedIn-YourName"
|
||||||
|
kernel auth create --name "WellFound-YourName"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the profile names and proxy ID to `config/settings.json`.
|
||||||
|
|
||||||
|
### 3. Set up Telegram notifications
|
||||||
|
|
||||||
|
1. Message [@BotFather](https://t.me/BotFather) on Telegram to create a bot
|
||||||
|
2. Copy the bot token to `settings.json` -> `notifications.bot_token`
|
||||||
|
3. Message [@userinfobot](https://t.me/userinfobot) to get your user ID
|
||||||
|
4. Add it to `settings.json` -> `notifications.telegram_user_id`
|
||||||
|
|
||||||
|
### 4. Verify setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
KERNEL_API_KEY=your_key node setup.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
This validates your config, tests LinkedIn and Wellfound logins, and sends a test Telegram message.
|
||||||
|
|
||||||
|
### 5. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for jobs
|
||||||
|
KERNEL_API_KEY=your_key node job_searcher.mjs
|
||||||
|
|
||||||
|
# Preview what's in the queue before applying
|
||||||
|
KERNEL_API_KEY=your_key node job_applier.mjs --preview
|
||||||
|
|
||||||
|
# Apply to queued jobs
|
||||||
|
KERNEL_API_KEY=your_key node job_applier.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
For automated runs, set up cron or use OpenClaw's scheduler:
|
||||||
|
|
||||||
|
```
|
||||||
|
Search: 0 * * * * (hourly)
|
||||||
|
Apply: 0 */6 * * * (every 6 hours)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Search flow
|
||||||
|
|
||||||
|
1. Runs your configured keyword searches on LinkedIn and Wellfound
|
||||||
|
2. Paginates through results (LinkedIn) and infinite-scrolls (Wellfound)
|
||||||
|
3. Filters out excluded keywords and companies
|
||||||
|
4. Deduplicates against the existing queue by job ID and URL
|
||||||
|
5. Saves new jobs to `data/jobs_queue.json` with status `new`
|
||||||
|
6. Sends a Telegram summary
|
||||||
|
|
||||||
|
### Apply flow
|
||||||
|
|
||||||
|
1. Picks up all `new` and `needs_answer` jobs from the queue (up to `max_applications_per_run`)
|
||||||
|
2. Opens a stealth browser session per platform
|
||||||
|
3. For each job:
|
||||||
|
- **LinkedIn Easy Apply**: navigates to job, clicks Easy Apply, fills the multi-step modal, submits
|
||||||
|
- **Wellfound**: navigates to job, clicks Apply, fills the form, submits
|
||||||
|
- Detects and skips recruiter-only listings, external ATS jobs, and honeypot questions
|
||||||
|
4. On unknown required fields, messages you on Telegram and moves on
|
||||||
|
5. Failed jobs are retried on the next run (up to `max_retries`, default 2)
|
||||||
|
6. Sends a summary with counts: applied, failed, needs answer, skipped
|
||||||
|
|
||||||
|
### Self-learning answers
|
||||||
|
|
||||||
|
When the applier encounters a form question it doesn't know how to answer:
|
||||||
|
|
||||||
|
1. Marks the job as `needs_answer` with the question text
|
||||||
|
2. Sends you a Telegram message with the question
|
||||||
|
3. You reply with the answer
|
||||||
|
4. The answer is saved to `config/answers.json` as a pattern match
|
||||||
|
5. Next run, it retries the job and fills in the answer automatically
|
||||||
|
|
||||||
|
Patterns support regex:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "pattern": "quota attainment", "answer": "1.12" },
|
||||||
|
{ "pattern": "years.*enterprise", "answer": "5" },
|
||||||
|
{ "pattern": "1.*10.*scale", "answer": "9" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
|-----|---------|-------------|
|
||||||
|
| `max_applications_per_run` | `50` | Cap applications per run to avoid rate limits |
|
||||||
|
| `max_retries` | `2` | Times to retry a failed application before marking it permanently failed |
|
||||||
|
| `browser.provider` | `"kernel"` | `"kernel"` for stealth browsers, `"local"` for local Playwright |
|
||||||
|
|
||||||
|
### Search filters
|
||||||
|
|
||||||
|
| Filter | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `remote` | boolean | Remote jobs only |
|
||||||
|
| `posted_within_days` | number | Only jobs posted within N days |
|
||||||
|
| `easy_apply_only` | boolean | LinkedIn Easy Apply only |
|
||||||
|
| `exclude_keywords` | string[] | Skip jobs with these words in title or company |
|
||||||
|
| `first_run_days` | number | On first run, look back N days (default 90) |
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
claw-apply/
|
||||||
|
├── job_searcher.mjs Search agent
|
||||||
|
├── job_applier.mjs Apply agent
|
||||||
|
├── setup.mjs Setup wizard
|
||||||
|
├── lib/
|
||||||
|
│ ├── constants.mjs Shared constants and defaults
|
||||||
|
│ ├── browser.mjs Kernel/Playwright browser factory
|
||||||
|
│ ├── form_filler.mjs Generic form filling with pattern matching
|
||||||
|
│ ├── linkedin.mjs LinkedIn search + Easy Apply
|
||||||
|
│ ├── wellfound.mjs Wellfound search + apply
|
||||||
|
│ ├── queue.mjs Job queue and config management
|
||||||
|
│ └── notify.mjs Telegram notifications with rate limiting
|
||||||
|
├── config/
|
||||||
|
│ ├── *.example.json Templates (committed)
|
||||||
|
│ ├── profile.json Your info (gitignored)
|
||||||
|
│ ├── search_config.json Your searches (gitignored)
|
||||||
|
│ ├── answers.json Learned answers (gitignored)
|
||||||
|
│ └── settings.json Your settings (gitignored)
|
||||||
|
└── data/
|
||||||
|
├── jobs_queue.json Job queue (auto-managed)
|
||||||
|
└── applications_log.json Application history (auto-managed)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Job statuses
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| `new` | Found, waiting to apply |
|
||||||
|
| `applied` | Successfully submitted |
|
||||||
|
| `needs_answer` | Blocked on unknown question, waiting for your reply |
|
||||||
|
| `failed` | Failed after max retries |
|
||||||
|
| `skipped` | Honeypot detected |
|
||||||
|
| `skipped_recruiter_only` | LinkedIn recruiter-only listing |
|
||||||
|
| `skipped_external_unsupported` | External ATS (Greenhouse, Lever — not yet supported) |
|
||||||
|
| `skipped_easy_apply_unsupported` | No Easy Apply button available |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [x] LinkedIn Easy Apply
|
||||||
|
- [x] Wellfound apply
|
||||||
|
- [x] Kernel stealth browsers + residential proxy
|
||||||
|
- [x] Self-learning answer bank
|
||||||
|
- [x] Retry logic for transient failures
|
||||||
|
- [x] Preview mode (`--preview`)
|
||||||
|
- [x] Configurable application caps and retry limits
|
||||||
|
- [ ] Indeed support
|
||||||
|
- [ ] External ATS support (Greenhouse, Lever)
|
||||||
|
- [ ] Job scoring and ranking
|
||||||
|
- [ ] Per-job cover letter generation via LLM
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
109
SKILL.md
109
SKILL.md
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: claw-apply
|
name: claw-apply
|
||||||
description: Automated job search and application for LinkedIn and Wellfound. Searches for matching roles hourly, applies automatically every 6 hours using Playwright + Kernel stealth browsers. Handles LinkedIn Easy Apply and Wellfound applications. Asks you via Telegram when it hits a question it can't answer, saves your answer, and never asks again. Use when you want to automate your job search and application process.
|
description: Automated job search and application for LinkedIn and Wellfound. Searches for matching roles hourly, applies automatically every 6 hours using Playwright + Kernel stealth browsers. Handles LinkedIn Easy Apply multi-step modals and Wellfound applications. Self-learning — asks you via Telegram when it hits an unknown question, saves your answer, and never asks again. Retries failed applications automatically. Preview mode lets you review the queue before applying.
|
||||||
---
|
---
|
||||||
|
|
||||||
# claw-apply
|
# claw-apply
|
||||||
@@ -9,7 +9,8 @@ Automated job search and application. Finds matching roles on LinkedIn and Wellf
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Kernel.sh](https://kernel.sh) account (for stealth browsers + bot detection bypass)
|
- Node.js 18+
|
||||||
|
- [Kernel](https://kernel.sh) account (stealth browsers + bot detection bypass) — or local Playwright
|
||||||
- Kernel CLI: `npm install -g @onkernel/cli`
|
- Kernel CLI: `npm install -g @onkernel/cli`
|
||||||
- Kernel Managed Auth sessions for LinkedIn and Wellfound
|
- Kernel Managed Auth sessions for LinkedIn and Wellfound
|
||||||
- Kernel residential proxy (US recommended)
|
- Kernel residential proxy (US recommended)
|
||||||
@@ -17,97 +18,95 @@ Automated job search and application. Finds matching roles on LinkedIn and Wellf
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### 1. Install dependencies
|
### 1. Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://github.com/MattJackson/claw-apply.git
|
||||||
cd claw-apply
|
cd claw-apply
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Create Kernel Managed Auth sessions
|
### 2. Create Kernel browser sessions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create residential proxy
|
# Create residential proxy
|
||||||
kernel proxies create --type residential --country US --name "claw-apply-proxy"
|
kernel proxies create --type residential --country US --name "claw-apply-proxy"
|
||||||
|
|
||||||
# Create authenticated browser profiles
|
# Create authenticated browser profiles
|
||||||
kernel auth create --name "LinkedIn-YourName" # Follow prompts to log in
|
kernel auth create --name "LinkedIn-YourName"
|
||||||
kernel auth create --name "WellFound-YourName" # Follow prompts to log in
|
kernel auth create --name "WellFound-YourName"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configure
|
### 3. Configure
|
||||||
Edit these files in `config/`:
|
|
||||||
|
|
||||||
- **`profile.json`** — your personal info, resume path, cover letter
|
Copy example configs and fill in your values:
|
||||||
- **`search_config.json`** — what jobs to search for (titles, keywords, filters)
|
|
||||||
- **`settings.json`** — Telegram bot token, Kernel profile names, proxy ID, mode A/B
|
```bash
|
||||||
|
cp config/settings.example.json config/settings.json
|
||||||
|
cp config/profile.example.json config/profile.json
|
||||||
|
cp config/search_config.example.json config/search_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`profile.json`** — name, email, phone, resume path, work authorization, salary
|
||||||
|
- **`search_config.json`** — keywords, platforms, filters, exclusions
|
||||||
|
- **`settings.json`** — Telegram bot token, Kernel profile names, proxy ID, run caps
|
||||||
|
|
||||||
|
### 4. Verify
|
||||||
|
|
||||||
### 4. Run setup
|
|
||||||
```bash
|
```bash
|
||||||
KERNEL_API_KEY=your_key node setup.mjs
|
KERNEL_API_KEY=your_key node setup.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
Verifies config, tests logins, sends a test Telegram message.
|
### 5. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
KERNEL_API_KEY=your_key node job_searcher.mjs # search now
|
||||||
|
KERNEL_API_KEY=your_key node job_applier.mjs --preview # preview queue
|
||||||
|
KERNEL_API_KEY=your_key node job_applier.mjs # apply now
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Schedule (via OpenClaw or cron)
|
||||||
|
|
||||||
### 5. Register cron jobs (via OpenClaw)
|
|
||||||
```
|
```
|
||||||
Search: 0 * * * * (hourly)
|
Search: 0 * * * * (hourly)
|
||||||
Apply: 0 */6 * * * (every 6 hours)
|
Apply: 0 */6 * * * (every 6 hours)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running manually
|
|
||||||
```bash
|
|
||||||
KERNEL_API_KEY=your_key node job_searcher.mjs # search now
|
|
||||||
KERNEL_API_KEY=your_key node job_applier.mjs # apply now
|
|
||||||
```
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
**JobSearcher** (hourly):
|
**Search** — runs your keyword searches on LinkedIn and Wellfound, paginates/scrolls through results, filters exclusions, deduplicates, and queues new jobs.
|
||||||
1. Searches LinkedIn + Wellfound with your configured keywords
|
|
||||||
2. Filters out excluded roles/companies
|
|
||||||
3. Adds new jobs to `data/jobs_queue.json`
|
|
||||||
4. Sends Telegram: "Found X new jobs"
|
|
||||||
|
|
||||||
**JobApplier** (every 6 hours):
|
**Apply** — picks up queued jobs, opens stealth browser sessions, fills forms using your profile + learned answers, and submits. Detects and skips honeypots, recruiter-only listings, and external ATS. Retries failed jobs automatically (default 2 retries).
|
||||||
1. Reads queue for `new` + `needs_answer` jobs
|
|
||||||
2. LinkedIn: navigates two-panel search view, clicks Easy Apply, fills form, submits
|
|
||||||
3. Wellfound: navigates to job, fills profile, submits
|
|
||||||
4. On unknown question → Telegrams you → saves answer → retries next run
|
|
||||||
5. Sends summary when done
|
|
||||||
|
|
||||||
## Mode A vs Mode B
|
**Learn** — on unknown questions, messages you on Telegram. You reply, the answer is saved to `answers.json` with regex pattern matching, and the job is retried next run.
|
||||||
Set in `config/settings.json` → `"mode": "A"` or `"B"`
|
|
||||||
|
|
||||||
- **A**: Fully automatic. No intervention needed.
|
|
||||||
- **B**: Applier sends you the queue 30 min before running. You can flag jobs to skip before it fires.
|
|
||||||
|
|
||||||
## File structure
|
## File structure
|
||||||
|
|
||||||
```
|
```
|
||||||
claw-apply/
|
claw-apply/
|
||||||
├── job_searcher.mjs search agent
|
├── job_searcher.mjs Search agent
|
||||||
├── job_applier.mjs apply agent
|
├── job_applier.mjs Apply agent
|
||||||
├── setup.mjs setup wizard
|
├── setup.mjs Setup wizard
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── browser.mjs Kernel/Playwright factory
|
│ ├── constants.mjs Shared constants
|
||||||
│ ├── form_filler.mjs generic form filling
|
│ ├── browser.mjs Kernel/Playwright browser factory
|
||||||
│ ├── linkedin.mjs LinkedIn search + apply
|
│ ├── form_filler.mjs Form filling with pattern matching
|
||||||
│ ├── wellfound.mjs Wellfound search + apply
|
│ ├── linkedin.mjs LinkedIn search + Easy Apply
|
||||||
│ ├── queue.mjs queue management
|
│ ├── wellfound.mjs Wellfound search + apply
|
||||||
│ └── notify.mjs Telegram notifications
|
│ ├── queue.mjs Job queue + config management
|
||||||
|
│ └── notify.mjs Telegram notifications
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── profile.json ← fill this in
|
│ ├── *.example.json Templates (committed)
|
||||||
│ ├── search_config.json ← fill this in
|
│ └── *.json Your config (gitignored)
|
||||||
│ ├── answers.json ← auto-grows over time
|
|
||||||
│ └── settings.json ← fill this in
|
|
||||||
└── data/
|
└── data/
|
||||||
├── jobs_queue.json auto-managed
|
├── jobs_queue.json Job queue (auto-managed)
|
||||||
└── applications_log.json auto-managed
|
└── applications_log.json History (auto-managed)
|
||||||
```
|
```
|
||||||
|
|
||||||
## answers.json — self-learning Q&A bank
|
## answers.json — self-learning Q&A
|
||||||
When the applier hits a question it can't answer, it messages you on Telegram.
|
|
||||||
You reply. The answer is saved to `config/answers.json` and used forever after.
|
When the applier can't answer a question, it messages you. Your reply is saved and reused:
|
||||||
|
|
||||||
Pattern matching is regex-friendly:
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{ "pattern": "quota attainment", "answer": "1.12" },
|
{ "pattern": "quota attainment", "answer": "1.12" },
|
||||||
@@ -115,3 +114,5 @@ Pattern matching is regex-friendly:
|
|||||||
{ "pattern": "1.*10.*scale", "answer": "9" }
|
{ "pattern": "1.*10.*scale", "answer": "9" }
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Patterns are matched case-insensitively and support regex.
|
||||||
|
|||||||
246
SPEC.md
246
SPEC.md
@@ -1,36 +1,56 @@
|
|||||||
# claw-apply — Skill Spec v0.1
|
# claw-apply — Technical Spec
|
||||||
|
|
||||||
Automated job search and application skill for OpenClaw.
|
Automated job search and application engine. Searches LinkedIn and Wellfound for matching roles, applies automatically using Playwright + Kernel stealth browsers, and self-learns from unknown questions.
|
||||||
Searches LinkedIn and Wellfound for matching roles, applies automatically using Playwright + Kernel stealth browsers.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Two agents
|
### Two agents, shared queue
|
||||||
|
|
||||||
**JobSearcher** (`job_searcher.mjs`)
|
**JobSearcher** (`job_searcher.mjs`)
|
||||||
- Runs on a schedule (default: hourly)
|
- Runs on schedule (default: hourly)
|
||||||
- Searches configured platforms with configured queries
|
- Searches configured platforms with configured keywords
|
||||||
|
- LinkedIn: paginates through up to 40 pages of results
|
||||||
|
- Wellfound: infinite-scrolls up to 10 times to load all results
|
||||||
- Filters out excluded roles/companies
|
- Filters out excluded roles/companies
|
||||||
- Dedupes against existing queue
|
- Deduplicates by job ID and URL against existing queue
|
||||||
- Writes new jobs to `jobs_queue.json` with status `new`
|
- Writes new jobs to `jobs_queue.json` with status `new`
|
||||||
- Sends Telegram summary: "Found X new jobs"
|
- Sends Telegram summary
|
||||||
|
|
||||||
**JobApplier** (`job_applier.mjs`)
|
**JobApplier** (`job_applier.mjs`)
|
||||||
- Runs on a schedule (default: every 6 hours)
|
- Runs on schedule (default: every 6 hours)
|
||||||
- Reads `jobs_queue.json` for status `new` + `needs_answer`
|
- Reads queue for status `new` + `needs_answer`
|
||||||
- Attempts to apply to each job
|
- Respects `max_applications_per_run` cap
|
||||||
- On success → status: `applied`
|
- LinkedIn: navigates directly to job URL, detects apply type (Easy Apply / external / recruiter-only), fills multi-step modal
|
||||||
- On unknown question → messages user via Telegram, status: `needs_answer`
|
- Wellfound: navigates to job, fills form, submits
|
||||||
- On skip/fail → status: `skipped` or `failed`
|
- Detects honeypot questions and skips
|
||||||
- Sends Telegram summary when done
|
- On unknown required fields: messages user via Telegram, marks `needs_answer`
|
||||||
|
- On error: retries up to `max_retries` (default 2) before marking `failed`
|
||||||
|
- Sends summary with granular skip reasons
|
||||||
|
|
||||||
|
**Preview mode** (`--preview`): shows queued jobs without applying.
|
||||||
|
|
||||||
|
### Shared modules
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|--------|---------------|
|
||||||
|
| `lib/constants.mjs` | All timeouts, selectors, defaults — no magic numbers in code |
|
||||||
|
| `lib/browser.mjs` | Browser factory — Kernel stealth (default) with local Playwright fallback |
|
||||||
|
| `lib/form_filler.mjs` | Generic form filling — custom answers first, then built-in profile matching |
|
||||||
|
| `lib/queue.mjs` | Queue CRUD with in-memory caching, config file validation |
|
||||||
|
| `lib/notify.mjs` | Telegram Bot API with rate limiting (1.5s between sends) |
|
||||||
|
| `lib/linkedin.mjs` | LinkedIn search (paginated) + Easy Apply (multi-step modal) |
|
||||||
|
| `lib/wellfound.mjs` | Wellfound search (infinite scroll) + apply |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Config Files (user sets up once)
|
## Config files
|
||||||
|
|
||||||
|
All user config is gitignored. Example templates are committed.
|
||||||
|
|
||||||
### `profile.json`
|
### `profile.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": { "first": "Jane", "last": "Smith" },
|
"name": { "first": "Jane", "last": "Smith" },
|
||||||
@@ -51,81 +71,41 @@ Searches LinkedIn and Wellfound for matching roles, applies automatically using
|
|||||||
},
|
},
|
||||||
"willing_to_relocate": false,
|
"willing_to_relocate": false,
|
||||||
"desired_salary": 150000,
|
"desired_salary": 150000,
|
||||||
"cover_letter": "Your cover letter text here..."
|
"cover_letter": "Your cover letter text here."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `search_config.json`
|
### `search_config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"first_run_days": 90,
|
||||||
"searches": [
|
"searches": [
|
||||||
{
|
{
|
||||||
"name": "Founding GTM",
|
"name": "Founding GTM",
|
||||||
"track": "gtm",
|
"track": "gtm",
|
||||||
"keywords": [
|
"keywords": ["founding account executive", "first sales hire"],
|
||||||
"founding account executive",
|
|
||||||
"first sales hire",
|
|
||||||
"first GTM hire",
|
|
||||||
"founding AE",
|
|
||||||
"head of sales startup remote"
|
|
||||||
],
|
|
||||||
"platforms": ["linkedin", "wellfound"],
|
"platforms": ["linkedin", "wellfound"],
|
||||||
"filters": {
|
|
||||||
"remote": true,
|
|
||||||
"posted_within_days": 2
|
|
||||||
},
|
|
||||||
"exclude_keywords": ["BDR", "SDR", "staffing", "insurance", "retail", "consumer", "recruiter"],
|
|
||||||
"salary_min": 130000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Enterprise AE",
|
|
||||||
"track": "ae",
|
|
||||||
"keywords": [
|
|
||||||
"enterprise account executive SaaS remote",
|
|
||||||
"senior account executive technical SaaS remote"
|
|
||||||
],
|
|
||||||
"platforms": ["linkedin"],
|
|
||||||
"filters": {
|
"filters": {
|
||||||
"remote": true,
|
"remote": true,
|
||||||
"posted_within_days": 2,
|
"posted_within_days": 2,
|
||||||
"easy_apply_only": true
|
"easy_apply_only": false
|
||||||
},
|
},
|
||||||
"exclude_keywords": ["BDR", "SDR", "SMB", "staffing"],
|
"exclude_keywords": ["BDR", "SDR", "staffing", "insurance"]
|
||||||
"salary_min": 150000
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `answers.json`
|
|
||||||
Flat array of pattern → answer mappings. Pattern is substring match (case-insensitive). First match wins.
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "pattern": "quota attainment", "answer": "1.12", "note": "FY24 $1.2M quota, hit $1.12M" },
|
|
||||||
{ "pattern": "sponsor", "answer": "No" },
|
|
||||||
{ "pattern": "authorized", "answer": "Yes" },
|
|
||||||
{ "pattern": "relocat", "answer": "No" },
|
|
||||||
{ "pattern": "years.*sales", "answer": "7" },
|
|
||||||
{ "pattern": "years.*enterprise", "answer": "5" },
|
|
||||||
{ "pattern": "years.*crm", "answer": "7" },
|
|
||||||
{ "pattern": "1.*10.*scale", "answer": "9" },
|
|
||||||
{ "pattern": "salary", "answer": "150000" },
|
|
||||||
{ "pattern": "start date", "answer": "Immediately" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `settings.json`
|
### `settings.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mode": "A",
|
|
||||||
"review_window_minutes": 30,
|
|
||||||
"schedules": {
|
|
||||||
"search": "0 * * * *",
|
|
||||||
"apply": "0 */6 * * *"
|
|
||||||
},
|
|
||||||
"max_applications_per_run": 50,
|
"max_applications_per_run": 50,
|
||||||
|
"max_retries": 2,
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"telegram_user_id": "YOUR_TELEGRAM_ID"
|
"telegram_user_id": "YOUR_TELEGRAM_USER_ID",
|
||||||
|
"bot_token": "YOUR_TELEGRAM_BOT_TOKEN"
|
||||||
},
|
},
|
||||||
"kernel": {
|
"kernel": {
|
||||||
"proxy_id": "YOUR_KERNEL_PROXY_ID",
|
"proxy_id": "YOUR_KERNEL_PROXY_ID",
|
||||||
@@ -136,16 +116,29 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse
|
|||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"provider": "kernel",
|
"provider": "kernel",
|
||||||
"fallback": "local"
|
"playwright_path": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `answers.json`
|
||||||
|
|
||||||
|
Flat array of pattern-answer pairs. Patterns are matched case-insensitively and support regex. First match wins.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "pattern": "quota attainment", "answer": "1.12" },
|
||||||
|
{ "pattern": "years.*enterprise", "answer": "5" },
|
||||||
|
{ "pattern": "1.*10.*scale", "answer": "9" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Data Files (auto-managed)
|
## Data files (auto-managed)
|
||||||
|
|
||||||
### `jobs_queue.json`
|
### `jobs_queue.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -158,6 +151,7 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse
|
|||||||
"found_at": "2026-03-05T22:00:00Z",
|
"found_at": "2026-03-05T22:00:00Z",
|
||||||
"status": "new",
|
"status": "new",
|
||||||
"status_updated_at": "2026-03-05T22:00:00Z",
|
"status_updated_at": "2026-03-05T22:00:00Z",
|
||||||
|
"retry_count": 0,
|
||||||
"pending_question": null,
|
"pending_question": null,
|
||||||
"applied_at": null,
|
"applied_at": null,
|
||||||
"notes": null
|
"notes": null
|
||||||
@@ -165,83 +159,93 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Statuses:** `new` → `applied` / `skipped` / `failed` / `needs_answer`
|
### Job statuses
|
||||||
|
|
||||||
|
| Status | Meaning | Next action |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `new` | Found, waiting to apply | Applier picks it up |
|
||||||
|
| `applied` | Successfully submitted | Done |
|
||||||
|
| `needs_answer` | Blocked on unknown question | Applier retries after user answers |
|
||||||
|
| `failed` | Failed after max retries | Manual review |
|
||||||
|
| `skipped` | Honeypot detected | Permanent skip |
|
||||||
|
| `skipped_recruiter_only` | LinkedIn recruiter-only | Permanent skip |
|
||||||
|
| `skipped_external_unsupported` | External ATS | Saved for future ATS support |
|
||||||
|
| `skipped_easy_apply_unsupported` | No Easy Apply button | Permanent skip |
|
||||||
|
|
||||||
### `applications_log.json`
|
### `applications_log.json`
|
||||||
Append-only history of every application attempt with outcome.
|
|
||||||
|
Append-only history of every application attempt with outcome, timestamps, and metadata.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unknown Question Flow
|
## Unknown question flow
|
||||||
|
|
||||||
1. Applier hits a required field it can't answer
|
1. Applier encounters a required field with no matching answer
|
||||||
2. Marks job as `needs_answer`, stores the question text in `pending_question`
|
2. Marks job as `needs_answer`, stores question in `pending_question`
|
||||||
3. Sends Telegram: *"Applying to Senior AE @ Acme Corp and hit this question: 'What was your last quota attainment in $M?' — what should I answer?"*
|
3. Sends Telegram: "Applying to Senior AE @ Acme Corp — question: 'What was your quota attainment?' — what should I answer?"
|
||||||
4. Moves on to next job
|
4. Moves on to next job
|
||||||
5. User replies → answer saved to `answers.json`
|
5. User replies with answer
|
||||||
6. Next applier run retries all `needs_answer` jobs
|
6. Answer saved to `answers.json` as pattern match
|
||||||
|
7. Next applier run retries all `needs_answer` jobs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mode A vs Mode B
|
## Retry logic
|
||||||
|
|
||||||
**Mode A (fully automatic):**
|
When an application fails due to a transient error (timeout, network issue, page didn't load):
|
||||||
Search → Queue → Apply. No intervention required.
|
|
||||||
|
|
||||||
**Mode B (soft gate):**
|
1. `retry_count` is incremented on the job
|
||||||
Search → Queue → Telegram summary sent to user → 30 min window to reply with any job IDs to skip → Apply runs.
|
2. Job status is reset to `new` so the next run picks it up
|
||||||
|
3. After `max_retries` (default 2) failures, job is marked `failed` permanently
|
||||||
Configured via `settings.json` → `mode: "A"` or `"B"`
|
4. Failed jobs are logged to `applications_log.json` with error details
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Structure
|
## File structure
|
||||||
|
|
||||||
```
|
```
|
||||||
claw-apply/
|
claw-apply/
|
||||||
├── SKILL.md ← OpenClaw skill entry point
|
├── README.md Documentation
|
||||||
├── SPEC.md ← this file
|
├── SKILL.md OpenClaw skill manifest
|
||||||
├── job_searcher.mjs ← search agent
|
├── SPEC.md This file
|
||||||
├── job_applier.mjs ← apply agent
|
├── job_searcher.mjs Search agent
|
||||||
|
├── job_applier.mjs Apply agent
|
||||||
|
├── setup.mjs Setup wizard
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── browser.mjs ← Kernel/Playwright browser factory
|
│ ├── constants.mjs Shared constants and defaults
|
||||||
│ ├── form_filler.mjs ← form filling logic
|
│ ├── browser.mjs Kernel/Playwright browser factory
|
||||||
│ ├── linkedin.mjs ← LinkedIn search + apply
|
│ ├── form_filler.mjs Form filling with pattern matching
|
||||||
│ ├── wellfound.mjs ← Wellfound search + apply
|
│ ├── linkedin.mjs LinkedIn search + Easy Apply
|
||||||
│ └── notify.mjs ← Telegram notifications
|
│ ├── wellfound.mjs Wellfound search + apply
|
||||||
|
│ ├── queue.mjs Queue management + config validation
|
||||||
|
│ └── notify.mjs Telegram notifications + rate limiting
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── profile.json ← user fills this
|
│ ├── *.example.json Templates (committed)
|
||||||
│ ├── search_config.json← user fills this
|
│ └── *.json User config (gitignored)
|
||||||
│ ├── answers.json ← auto-grows over time
|
|
||||||
│ └── settings.json ← user fills this
|
|
||||||
└── data/
|
└── data/
|
||||||
├── jobs_queue.json ← auto-managed
|
├── jobs_queue.json Job queue (auto-managed)
|
||||||
└── applications_log.json ← auto-managed
|
└── applications_log.json Application history (auto-managed)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup (user steps)
|
## Roadmap
|
||||||
|
|
||||||
1. Install: `openclaw skill install claw-apply`
|
### v1 (current)
|
||||||
2. Configure Kernel Managed Auth for LinkedIn + Wellfound (or provide local Chrome)
|
- [x] LinkedIn Easy Apply (multi-step modal, pagination)
|
||||||
3. Create a residential proxy in Kernel: `kernel proxies create --type residential --country US`
|
- [x] Wellfound apply (infinite scroll)
|
||||||
4. Fill in `config/profile.json`, `config/search_config.json`, `config/settings.json`
|
- [x] Kernel stealth browsers + residential proxy
|
||||||
5. Run: `openclaw skill run claw-apply setup` — registers crons, verifies login, sends test notification
|
- [x] Self-learning answer bank with regex patterns
|
||||||
6. Done. Runs automatically.
|
- [x] Retry logic with configurable max retries
|
||||||
|
- [x] Preview mode (`--preview`)
|
||||||
|
- [x] Configurable application caps
|
||||||
|
- [x] Telegram notifications with rate limiting
|
||||||
|
- [x] Config validation with clear error messages
|
||||||
|
- [x] In-memory queue caching for performance
|
||||||
|
- [x] Constants extracted — no magic numbers in code
|
||||||
|
|
||||||
---
|
### v2 (planned)
|
||||||
|
- [ ] Indeed support
|
||||||
## v1 Scope
|
- [ ] External ATS support (Greenhouse, Lever)
|
||||||
|
- [ ] Job scoring and ranking
|
||||||
- [x] LinkedIn Easy Apply
|
- [ ] Per-job cover letter generation via LLM
|
||||||
- [x] Wellfound apply
|
|
||||||
- [x] Kernel stealth browser + residential proxy
|
|
||||||
- [x] Mode A + Mode B
|
|
||||||
- [x] Unknown question → Telegram → answers.json flow
|
|
||||||
- [x] Deduplication
|
|
||||||
- [x] Hourly search / 6hr apply cron
|
|
||||||
- [ ] Indeed (v2)
|
|
||||||
- [ ] External ATS / Greenhouse / Lever (v2)
|
|
||||||
- [ ] Job scoring/ranking (v2)
|
|
||||||
- [ ] Cover letter generation per-job via LLM (v2)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user