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:
2026-03-05 16:10:12 -08:00
parent a244a5fddf
commit 282e2681aa
3 changed files with 389 additions and 175 deletions

209
README.md Normal file
View 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
View File

@@ -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
View File

@@ -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)