- 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>
7.7 KiB
7.7 KiB
claw-apply — Technical Spec
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.
Architecture
Two agents, shared queue
JobSearcher (job_searcher.mjs)
- Runs on schedule (default: hourly)
- 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
- Deduplicates by job ID and URL against existing queue
- Writes new jobs to
jobs_queue.jsonwith statusnew - Sends Telegram summary
JobApplier (job_applier.mjs)
- Runs on schedule (default: every 6 hours)
- Reads queue for status
new+needs_answer - Respects
max_applications_per_runcap - LinkedIn: navigates directly to job URL, detects apply type (Easy Apply / external / recruiter-only), fills multi-step modal
- Wellfound: navigates to job, fills form, submits
- Detects honeypot questions and skips
- On unknown required fields: messages user via Telegram, marks
needs_answer - On error: retries up to
max_retries(default 2) before markingfailed - 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
All user config is gitignored. Example templates are committed.
profile.json
{
"name": { "first": "Jane", "last": "Smith" },
"email": "jane@example.com",
"phone": "555-123-4567",
"location": {
"city": "San Francisco",
"state": "California",
"zip": "94102",
"country": "United States"
},
"linkedin_url": "https://linkedin.com/in/janesmith",
"resume_path": "/home/user/resume.pdf",
"years_experience": 7,
"work_authorization": {
"authorized": true,
"requires_sponsorship": false
},
"willing_to_relocate": false,
"desired_salary": 150000,
"cover_letter": "Your cover letter text here."
}
search_config.json
{
"first_run_days": 90,
"searches": [
{
"name": "Founding GTM",
"track": "gtm",
"keywords": ["founding account executive", "first sales hire"],
"platforms": ["linkedin", "wellfound"],
"filters": {
"remote": true,
"posted_within_days": 2,
"easy_apply_only": false
},
"exclude_keywords": ["BDR", "SDR", "staffing", "insurance"]
}
]
}
settings.json
{
"max_applications_per_run": 50,
"max_retries": 2,
"notifications": {
"telegram_user_id": "YOUR_TELEGRAM_USER_ID",
"bot_token": "YOUR_TELEGRAM_BOT_TOKEN"
},
"kernel": {
"proxy_id": "YOUR_KERNEL_PROXY_ID",
"profiles": {
"linkedin": "LinkedIn-YourName",
"wellfound": "WellFound-YourName"
}
},
"browser": {
"provider": "kernel",
"playwright_path": null
}
}
answers.json
Flat array of pattern-answer pairs. Patterns are matched case-insensitively and support regex. First match wins.
[
{ "pattern": "quota attainment", "answer": "1.12" },
{ "pattern": "years.*enterprise", "answer": "5" },
{ "pattern": "1.*10.*scale", "answer": "9" }
]
Data files (auto-managed)
jobs_queue.json
[
{
"id": "li_4381658809",
"platform": "linkedin",
"track": "ae",
"title": "Senior Account Executive",
"company": "Acme Corp",
"url": "https://linkedin.com/jobs/view/4381658809/",
"found_at": "2026-03-05T22:00:00Z",
"status": "new",
"status_updated_at": "2026-03-05T22:00:00Z",
"retry_count": 0,
"pending_question": null,
"applied_at": null,
"notes": null
}
]
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
Append-only history of every application attempt with outcome, timestamps, and metadata.
Unknown question flow
- Applier encounters a required field with no matching answer
- Marks job as
needs_answer, stores question inpending_question - Sends Telegram: "Applying to Senior AE @ Acme Corp — question: 'What was your quota attainment?' — what should I answer?"
- Moves on to next job
- User replies with answer
- Answer saved to
answers.jsonas pattern match - Next applier run retries all
needs_answerjobs
Retry logic
When an application fails due to a transient error (timeout, network issue, page didn't load):
retry_countis incremented on the job- Job status is reset to
newso the next run picks it up - After
max_retries(default 2) failures, job is markedfailedpermanently - Failed jobs are logged to
applications_log.jsonwith error details
File structure
claw-apply/
├── README.md Documentation
├── SKILL.md OpenClaw skill manifest
├── SPEC.md This file
├── 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 Form filling with pattern matching
│ ├── linkedin.mjs LinkedIn search + Easy Apply
│ ├── wellfound.mjs Wellfound search + apply
│ ├── queue.mjs Queue management + config validation
│ └── notify.mjs Telegram notifications + rate limiting
├── config/
│ ├── *.example.json Templates (committed)
│ └── *.json User config (gitignored)
└── data/
├── jobs_queue.json Job queue (auto-managed)
└── applications_log.json Application history (auto-managed)
Roadmap
v1 (current)
- LinkedIn Easy Apply (multi-step modal, pagination)
- Wellfound apply (infinite scroll)
- Kernel stealth browsers + residential proxy
- Self-learning answer bank with regex patterns
- Retry logic with configurable max retries
- Preview mode (
--preview) - Configurable application caps
- Telegram notifications with rate limiting
- Config validation with clear error messages
- In-memory queue caching for performance
- Constants extracted — no magic numbers in code
v2 (planned)
- Indeed support
- External ATS support (Greenhouse, Lever)
- Job scoring and ranking
- Per-job cover letter generation via LLM