ワンクリックで
auto-apply
// Search a job board and autonomously apply to matching jobs one at a time, until paused, exhausted, or the max-applications cap is hit.
// Search a job board and autonomously apply to matching jobs one at a time, until paused, exhausted, or the max-applications cap is hit.
Apply to a single job (URL or pasted page) with fit review, or drain the pending queue when no argument is given.
Migrate a React @tanstack/react-form codebase from the prop-drilled `useForm` + erased-form-type pattern to the official `createFormHook` composition API (`useAppForm` / `withForm` / `field.X`). Use when a project threads a `form` object (often cast to an `any`-erased type like `ReactFormExtendedApi<any,...>`) through field-wrapper components that take `form`+`name` props, and you want typed field names/values, no casts, and reusable bound field components. Triggers: "migrate forms to createFormHook", "adopt useAppForm/withForm", "remove AnyReactForm cast", "type-safe tanstack form fields".
Re-score a run's skipped jobs and promote the eligible ones to `approved` for later applying. Recovers jobs wrongly dropped for location, a sparse JD, 1099, or seniority. Does not apply.
Resume an interrupted or paused JobPilot run by id. Re-flips the run to in_progress and replays the apply loop on any remaining approved jobs without re-asking for fit confirmation.
Search a chosen job board via Playwright, rank results by fit against the user's resume, and save them to the run so the user can review.
Parse a resume's uploaded PDF into structured JSON (basics, experience, projects, skills, education) and save it to the editor.
| name | auto-apply |
| description | Search a job board and autonomously apply to matching jobs one at a time, until paused, exhausted, or the max-applications cap is hit. |
| argument-hint | <search_query --board <domain> [--min-score N] [--max-apps N]> OR 'resume' OR 'retry-failed <run-id>' |
Keep the chosen board open in tab 1; for each result that qualifies, apply in a second tab, then close it and move to the next job. No batch pre-discovery and no per-job approval — launching the run is the confirmation. Pause only for 2FA / payment. A CAPTCHA is not a pause — skip the job; the user finishes it later via the apply skill. Live view at http://localhost:8000/runs/<run-id>.
JOBPILOT_API=http://localhost:8000
Follow plugin/skills/shared/setup.md. Read data.autoApply (defaults applied per field):
| Setting | Default | Notes |
|---|---|---|
minMatchScore | 70 | Qualification threshold (0–100). Inline --min-score overrides. |
maxApplicationsPerRun | null (unlimited) | Stop after this many successful applies. Inline --max-apps overrides; omit → unlimited. |
defaultStartDate | "2 weeks notice" | Default start-date answer. |
Inline argument overrides take precedence. --board <domain> is required unless the argument is resume or retry-failed <run-id>.
"resume" → list incomplete runs (GET /api/runs?status=in_progress), ask which to resume, replay the apply loop on remaining applying/approved/pending jobs."retry-failed <run-id>" → fetch the run; for every failed job, PATCH back to approved, read retryNotes, then replay the apply loop on them.To recover wrongly-skipped jobs, use the dedicated rescan-skipped skill (it re-scores and promotes to approved; apply them afterward).
curl -fsS "$JOBPILOT_API/api/runs?status=in_progress"
If a run's query matches, ask "Found an incomplete run from <startedAt>. Resume or start fresh?" Resume → replay the loop.
Otherwise the web UI already created the run row when the user submitted /runs/new — confirm it exists and use that runId. If invoked manually (rare), create one:
SLUG=$(echo "<query>" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/-\+/-/g; s/^-//; s/-$//')
RUN_ID=$(date -u +%Y-%m-%dT%H-%M-%S_${SLUG})
# maxApplications is OPTIONAL — omit the field entirely for unlimited mode.
curl -fsS -X POST "$JOBPILOT_API/api/runs" \
-H 'content-type: application/json' \
-d "$(jq -n --arg id "$RUN_ID" --arg q "<query>" --arg board "<domain>" \
--argjson minScore <n> \
'{runId:$id, query:$q, source:"auto-apply", config:{board:$board, minScore:$minScore}}')"
Surface live view: http://localhost:8000/runs/<RUN_ID>.
Extract title/role, keywords, location, preferences. If vague, ask before searching.
Resolve the board:
curl -fsS "$JOBPILOT_API/api/job-boards" | jq --arg d "<domain>" '.data[] | select(.domain == $d)'
If no row matches, PATCH the run to failed with failReason:"Board <domain> not configured" and stop.
browser_navigate to searchUrl (this is tab 1 — keep it open for the whole run).plugin/skills/shared/auth.md — logs in, and registers a new account when none exists, without asking.browser_snapshot narrowed to the results list (per plugin/skills/shared/browser-tips.md) to read { title, company, location, url } per row.Walk the tab-1 results top to bottom. For each result:
Dedupe in-board by normalized title+company. Then check previously-applied:
URL_ENCODED=$(jq -rn --arg v "<job-url>" '$v|@uri')
TITLE_ENCODED=$(jq -rn --arg v "<title>" '$v|@uri')
COMPANY_ENCODED=$(jq -rn --arg v "<company>" '$v|@uri')
curl -fsS "$JOBPILOT_API/api/applied/check?url=$URL_ENCODED&title=$TITLE_ENCODED&company=$COMPANY_ENCODED"
If data.applied, add the job with status:"skipped", skipReason:"Already applied (<kind>)" and move on — don't open a tab.
If the listing row lacks enough detail, read it from the tab-1 snapshot (don't navigate away). Build the digest and score server-side:
FIT=$(curl -fsS -X POST "$JOBPILOT_API/api/score-fit" \
-H 'content-type: application/json' \
-d "$(jq -n --argjson digest "$DIGEST" '{digest:$digest}')")
SCORE=$(echo "$FIT" | jq -r '.data.score')
CONF=$(echo "$FIT" | jq -r '.data.confidence')
If CONF >= 0.7 and SCORE is ≥10 from minMatchScore either side, use it directly; otherwise rescore using strongMatches/partialMatches/gaps. A thin/generic row is not a skip — read the full posting (tab-1 detail or briefly in tab 2), rebuild the digest, and rescore first. Below minMatchScore after a fair read → add with status:"skipped", skipReason:"Below minimum match score (X < Y)" and move on (no tab). Otherwise add it (status applying) and apply (2.3):
DIGEST=<stringified digest>
curl -fsS -X POST "$JOBPILOT_API/api/runs/$RUN_ID/jobs" \
-H 'content-type: application/json' \
-d "$(jq -n --arg key "<stable-id>" --arg title "<title>" --arg company "<company>" \
--arg location "<location>" --arg url "<url>" --arg board "<board>" \
--arg matchReason "<one line>" --argjson score <0-100> --arg digest "$DIGEST" --arg desc "<posting text, when read>" \
'{key:$key, title:$title, company:$company, location:$location, url:$url, board:$board, matchScore:$score, matchReason:$matchReason, status:"applying", digest:$digest, description:$desc}')"
Valid skip reasons (exact phrasing):
Already applied (<kind>) — dedupe (2.1).Below minimum match score (X < Y) — only after reading the actual posting.US citizenship required, Active security clearance required. Never infer from industry or company name.CAPTCHA — apply manually via the apply skill / Payment required (surface during apply, not scoring).Never skip for: onsite/hybrid/other city when willingToRelocate is true or preferredLocations is empty/"Anywhere" (score on fit, not geography); a sparse JD (read and rescore first); 1099/contractor work; defense/federal industry absent a JD-stated citizenship/clearance requirement; a role below your level (Junior/Mid when your résumé is Senior) or one asking fewer years than you have — over-qualification is full marks on experience; judge on tech-stack fit.
Open a second tab: browser_tabs(action:"new"), then browser_navigate it to the job URL. Tab 1 stays on the results.
browser_snapshot the header, browser_click the Apply / Easy Apply control's ref. browser_wait_for. If a new tab appeared (ATS portal), browser_tabs(action:"select", index:<new>).plugin/skills/shared/auth.md. On unrecoverable login failure for the domain: POST /result outcome:"failed", failReason:"Login failed for <domain>", close apply tab(s), continue.browser_snapshot the apply form before tailoring or filling. If it shows a CAPTCHA (reCAPTCHA / hCaptcha / Turnstile, "I'm not a robot", image/puzzle), skip: POST /result outcome:"skipped", skipReason:"CAPTCHA — apply manually via the apply skill", close apply tab(s), continue. Checking here avoids wasted tailoring/filling.tailor-resume skill with $DIGEST (empty → fall back to the job URL). Capture the variant id + PDF URL. No usable base → POST /result outcome:"failed", failReason:"No tailorable resume base", close apply tab(s), continue.plugin/skills/shared/form-filling.md. Upload the tailored variant. If the form has a cover-letter field (textarea or file upload), generate one via the cover-letter skill with $DIGEST and fill it per form-filling.md (paste text, or upload a generated PDF). Use autoApply.defaultStartDate; ask once for salary expectation and remember it for the run.browser_wait_for, then a narrowed browser_snapshot: a success confirmation = applied; a populated error = failure with that message as failReason. A CAPTCHA at this stage is a skip (2.4), not a failure.POST to /api/runs/$RUN_ID/jobs/<key>/result (atomically updates the Job, creates Application on applied, marks the queue, recomputes summary):
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# applied
jq -n --arg t "$NOW" --argjson score <0-100> '{outcome:"applied", appliedAt:$t, matchScore:$score}'
# skipped (CAPTCHA appeared — leave for manual apply via the apply skill)
jq -n '{outcome:"skipped", skipReason:"CAPTCHA — apply manually via the apply skill"}'
# failed (unexpected page, validation, crash)
jq -n --arg r "<reason>" --arg notes "<actionable retry context>" '{outcome:"failed", failReason:$r, retryNotes:$notes}'
Close all tabs with index ≥ 1: browser_tabs(action:"close", index:<i>) descending, then browser_tabs(action:"select", index:0). Pace 3–5s before the next job.
Before picking the next result, refetch the run (GET /api/runs/<RUN_ID>):
run.status === "paused" → user stopped from the UI. POST /result outcome:"skipped", skipReason:"Run paused by user" for any in-flight applying job and exit cleanly.config.maxApplications set AND summary.applied >= config.maxApplications → end the loop.NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -fsS -X PATCH "$JOBPILOT_API/api/runs/$RUN_ID" \
-H 'content-type: application/json' \
-d "$(jq -n --arg t "$NOW" '{status:"completed", completedAt:$t}')"
Print a summary table, link to http://localhost:8000/runs/<RUN_ID>, suggest retry-failed <RUN_ID>, the rescan-skipped skill on <RUN_ID> to recover dropped jobs, or a new search.
plugin/skills/shared/auth.md: register when missing (without asking), forgot-password via the get-code skill when stale./result outcome:"failed", failReason:"Payment required".get-code skill for <board-domain> (see plugin/skills/shared/auth.md); only ask the user when it returns nothing. 2FA — pause and ask (one-time per board). CAPTCHA — never pause: skip the job (detect up front per step 2.3.3) for manual apply later./result for terminal outcomes.status === "paused" → exit cleanly.paused, ask the user to re-upload.