# Linkedin Apply

> Scrape a LinkedIn job offer and score it against your profile using Claude LLM — produces APPLY / REVIEW / SKIP decision with 4-dimension scores. Triggers: "linkedin apply" | "analyze job" | "job match" | "linkedin-apply" | "score this job" | "should i apply for this job" | "evaluate this job posting" | "is this job a good fit" | "check this job offer" | "rate this job".

- Skill: `roxabi/linkedin-apply` (Agent Skill)
- Install (CLI): `npx skillmds@latest add roxabi/linkedin-apply`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roxabi/linkedin-apply/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Roxabi (https://skillmd.com/u/roxabi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/roxabi/linkedin-apply

---


# LinkedIn Apply Skill

Analyze a LinkedIn job offer against your candidate profile and get an LLM-powered decision: **APPLY**, **REVIEW**, or **SKIP**.

Let:
  VL := ~/.roxabi-vault/linkedin-apply/
  SD := `$CLAUDE_PLUGIN_ROOT/scripts/`

## Phase 1 — Validate Arguments and Prerequisites

Extract LinkedIn URL from $ARGUMENTS. ¬URL → "Usage: /linkedin-apply <linkedin-job-url>" → stop.

URL valid ⟺ contains `linkedin.com` ∧ (`/jobs/view/` ∨ `/jobs/`).

Check prerequisites:
1. `VL/candidate.yaml` exists — ¬exists → run `/linkedin-apply-init` first
2. `python3 -c "import playwright, playwright_stealth, jinja2" 2>&1` — missing → `pip install playwright playwright-stealth jinja2 pyyaml`
3. Playwright Chromium installed — missing → `playwright install chromium`

## Phase 2 — Scrape the Job

```bash
cd <scripts_dir> && python3 scraper.py "<url>"
```

Capture stdout (JSON) + stderr (logs). Errors: `SessionExpiredError` → run `/linkedin-apply-init`; `PlaywrightNotAvailableError` → install playwright; `JobNotFoundError` → job no longer available; other → show message. Parse JSON output → job data dict.

## Phase 3 — Load Candidate and Criteria

```bash
cat ~/.roxabi-vault/linkedin-apply/candidate.yaml
```

Criteria (user override ≻ plugin default): `VL/criteria.yaml` if ∃, else `<plugin_dir>/config/criteria.yaml`.

## Phase 4 — Run LLM Matching

CV path: `~/.roxabi-vault/cv/cv_data.json` if ∃, else construct minimal JSON from `candidate.yaml`.

```bash
# Tempfile per ${CLAUDE_PLUGIN_ROOT}/../shared/references/tempfile-convention.md
[[ "$JOB_ID" =~ ^[A-Za-z0-9_-]+$ ]] || { echo "Invalid job id: $JOB_ID" >&2; exit 1; }
TMPDIR=$(mktemp -d -t "linkedin-apply-job-${JOB_ID}-XXXXXX")
trap 'rm -rf "$TMPDIR"' EXIT
JOB_JSON="$TMPDIR/job.json"
echo '<job_json>' > "$JOB_JSON"
cd <scripts_dir> && python3 matcher.py \
  --job-json "$JOB_JSON" \
  --cv-json <cv_data_path> \
  --criteria-yaml <criteria_path> \
  --output json
```

## Phase 5 — Save Results

```bash
cd <scripts_dir> && python3 -c "
import json, sys
from storage import save_analysis
# reconstruct dataclasses and call save_analysis(job, match)
"
```

## Phase 6 — Display Results

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  DECISION: APPLY  |  Score: 7.8/10
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Job:     <title> at <company>
  URL:     <url>
  Type:    <Easy Apply / External (ATS: greenhouse)>

  Scores:
    Tech:             8/10
    Seniority:        7/10
    Culture:          8/10
    Responsibilities: 8/10

  Dealbreakers: PASS

  Highlights:
  + <highlight 1>
  + <highlight 2>

  Prepare for:
  ? <expected question 1>
  ? <expected question 2>

  Summary: <analysis_summary>

  Saved to: ~/.roxabi-vault/linkedin-apply/applications/<path>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

SKIP → show dealbreaker issues clearly. REVIEW → highlight what's uncertain/missing.

## Notes

- Scraper uses visible browser (¬headless) for anti-bot reliability
- Browser profile `~/.config/linkedin_browser_profile` persists session between runs
- Results stored in `VL/applications/YYYY-MM/`
- Phase 1 only: scraping + matching + display. Auto-application (Easy Apply form filling) is Phase 2.

$ARGUMENTS

