Legend text was saved to answers.json with duplicated text and "Required" suffix, causing pattern mismatches on future runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 but runs standalone with Node.js.
What it does
- Searches LinkedIn and Wellfound on a schedule with your configured keywords and filters
- Filters jobs using Claude AI batch scoring — only applies to roles that match your profile
- 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 asks Claude for a suggestion, messages you on Telegram, and saves your reply for all future jobs
- Deduplicates across runs and search tracks so you never apply to the same job twice
- Retries failed applications up to a configurable number of times before giving up
- Recovers from browser crashes, session timeouts, and network errors automatically
Quick start
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:
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 for stealth browser sessions that bypass bot detection.
npm install -g @onkernel/cli
kernel login
# Create a residential proxy
kernel proxies create --type residential --country US --name "claw-apply-proxy"
# Create authenticated browser profiles
kernel auth connections create --profile-name "LinkedIn-YourName" --domain linkedin.com
kernel auth connections create --profile-name "WellFound-YourName" --domain wellfound.com
# Complete initial login flows
kernel auth connections login <linkedin-connection-id>
kernel auth connections login <wellfound-connection-id>
Add the profile names, connection IDs, and proxy ID to config/settings.json.
3. Set up Telegram notifications
- Message @BotFather on Telegram to create a bot
- Copy the bot token to
settings.json->notifications.bot_token - Message @userinfobot to get your user ID
- Add it to
settings.json->notifications.telegram_user_id
4. Create .env
echo "KERNEL_API_KEY=your_kernel_api_key" > .env
echo "ANTHROPIC_API_KEY=your_anthropic_api_key" >> .env
The .env file is gitignored. ANTHROPIC_API_KEY is optional but enables AI keyword generation and AI-suggested answers.
5. Verify setup
node setup.mjs
Validates config, tests logins, and sends a test Telegram message.
6. Run
node job_searcher.mjs # search now
node job_filter.mjs # AI filter + score jobs
node job_applier.mjs --preview # preview queue without applying
node job_applier.mjs # apply now
node telegram_poller.mjs # process Telegram answer replies
node status.mjs # show queue + run status
7. Schedule (OpenClaw crons)
Scheduling is managed via OpenClaw cron jobs:
| Job | Schedule | Description |
|---|---|---|
| Searcher | 0 */12 * * * |
Search every 12 hours |
| Filter | 30 * * * * |
AI filter every hour at :30 |
| Applier | disabled by default | Enable when ready |
| Telegram Poller | * * * * * |
Process answer replies every minute |
The lockfile mechanism ensures only one instance of each agent runs at a time.
How it works
Search flow
- Runs your configured keyword searches on LinkedIn and Wellfound
- Paginates through results (LinkedIn) and infinite-scrolls (Wellfound)
- Classifies each job: Easy Apply, external ATS (Greenhouse, Lever, etc.), or recruiter-only
- Filters out excluded keywords and companies
- Deduplicates against the existing queue by job ID and URL
- Saves new jobs to
data/jobs_queue.jsonwith statusnew - Sends a Telegram summary
Filter flow
- Submits jobs to Claude AI via Anthropic Batch API (50% cost savings)
- Scores each job 1-10 based on match to your profile and search track
- Jobs below the minimum score (default 5) are marked
filtered - Cross-track deduplication keeps the highest-scoring copy
- Two-phase design: submit batch → collect results (designed for cron)
Apply flow
- Processes Telegram replies first — saves new answers, flips answered jobs back to
new - Picks up all
newandneeds_answerjobs, sorted by priority (Easy Apply first) - Reloads
answers.jsonbefore each job (picks up Telegram replies mid-run) - Opens a stealth browser session per platform (LinkedIn, Wellfound, external)
- For each job:
- LinkedIn Easy Apply: navigates to job, clicks Easy Apply, fills the multi-step modal (Next → Review → Submit), handles post-submit confirmation dialogs
- Wellfound: navigates to job, clicks Apply, fills the form, submits
- Detects and skips recruiter-only listings, external ATS jobs, and honeypot questions
- Selects resume from previously uploaded resumes (radio buttons) or uploads via file input
- On unknown required fields: asks Claude for a suggested answer, messages you on Telegram with the question + AI suggestion, moves on
- Failed jobs are retried on the next run (up to
max_retries, default 2) - Browser crash recovery: detects dead sessions and creates fresh browsers automatically
- 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:
- Claude generates a suggested answer based on your profile and resume
- Telegram message sent with the question, options (if select), and AI suggestion
- You reply with your answer, or reply "ACCEPT" to use the AI suggestion
- The Telegram poller (cron, every minute) saves your answer to
answers.jsonand flips the job back tonew - Next applier run retries the job with the saved answer
- Every future job with the same question is answered automatically
Over time, all common questions get answered and the applier runs fully autonomously.
Patterns support regex:
[
{ "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 |
no limit | Cap applications per run |
max_retries |
2 |
Times to retry a failed application |
enabled_apply_types |
["easy_apply"] |
Which apply types to process |
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_filter.mjs AI filter + scoring agent
├── job_applier.mjs Apply agent
├── telegram_poller.mjs Telegram answer reply processor
├── setup.mjs Setup wizard
├── status.mjs Queue status report
├── lib/
│ ├── constants.mjs Shared constants and defaults
│ ├── browser.mjs Kernel/Playwright browser factory
│ ├── session.mjs Kernel Managed Auth session refresh
│ ├── env.mjs .env loader (no dotenv dependency)
│ ├── form_filler.mjs Form filling with pattern matching
│ ├── ai_answer.mjs AI answer generation via Claude
│ ├── filter.mjs AI job scoring via Anthropic Batch API
│ ├── keywords.mjs AI-generated search keywords
│ ├── linkedin.mjs LinkedIn search + job classification
│ ├── wellfound.mjs Wellfound search
│ ├── queue.mjs Job queue with atomic writes
│ ├── lock.mjs PID-based process lock
│ ├── notify.mjs Telegram Bot API (send, getUpdates, reply)
│ ├── search_progress.mjs Per-platform search resume tracking
│ ├── telegram_answers.mjs Telegram reply → answers.json processing
│ └── apply/
│ ├── index.mjs Apply handler registry + status normalization
│ ├── easy_apply.mjs LinkedIn Easy Apply (multi-step modal)
│ ├── wellfound.mjs Wellfound apply
│ ├── greenhouse.mjs Greenhouse ATS (stub)
│ ├── lever.mjs Lever ATS (stub)
│ ├── workday.mjs Workday ATS (stub)
│ ├── ashby.mjs Ashby ATS (stub)
│ └── jobvite.mjs Jobvite ATS (stub)
├── 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)
└── telegram_offset.json Telegram polling offset (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 |
already_applied |
Duplicate detected, previously applied |
filtered |
Below AI score threshold |
duplicate |
Cross-track duplicate (lower-scoring copy) |
skipped_honeypot |
Honeypot question detected |
skipped_recruiter_only |
LinkedIn recruiter-only listing |
skipped_external_unsupported |
External ATS (Greenhouse, Lever, etc.) |
skipped_easy_apply_unsupported |
LinkedIn job without Easy Apply button |
skipped_no_apply |
No apply button found on page |
no_modal |
Easy Apply button found but modal didn't open |
stuck |
Modal progress stalled after repeated clicks |
incomplete |
Modal flow didn't reach submit |
ATS support
| Platform | Status |
|---|---|
| LinkedIn Easy Apply | Full |
| Wellfound | Full |
| Greenhouse | Stub |
| Lever | Stub |
| Workday | Stub |
| Ashby | Stub |
| Jobvite | Stub |
External ATS jobs are queued and classified — stubs will be promoted to full implementations based on usage data.
Roadmap
- LinkedIn Easy Apply (multi-step modal)
- Wellfound apply
- Kernel stealth browsers + residential proxy
- AI job filtering via Anthropic Batch API
- Self-learning answer bank with Telegram Q&A loop
- AI-suggested answers via Claude
- Telegram answer polling (instant save + applier safety net)
- Browser crash recovery
- Retry logic with configurable max retries
- Preview mode (
--preview) - Configurable application caps and retry limits
- External ATS support (Greenhouse, Lever, Workday, Ashby, Jobvite)
- Per-job cover letter generation via LLM
- Indeed support