一键导入
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.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Search a job board and autonomously apply to matching jobs one at a time, until paused, exhausted, or the max-applications cap is hit.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Apply to a single job (URL or pasted page) with fit review, or drain the pending queue when no argument is given.
Write a tailored one-page cover letter from a job description and the user's resume, humanized for natural tone.
Parse a resume's uploaded PDF into structured JSON (basics, experience, projects, skills, education) and save it to the editor.
Fetch the latest verification code or magic link from the connected mailbox for a given board domain. Called by apply / auto-apply for 2FA and account-creation flows.
Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's comprehensive "Signs of AI writing" guide. Detects and fixes patterns including: inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, negative parallelisms, and excessive conjunctive phrases.
Produce a tailored interview prep sheet (behavioral, technical, system design, company) from a job description and the user's resume.
| 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 <campaign-id>' |
Keep the chosen board open in tab 1; for each result that qualifies, delegate the application to the job-worker subagent (it works in its own tab and returns a compact result), then move to the next job. No batch pre-discovery and no per-job approval - launching the campaign is the confirmation. Pause only for 2FA / payment. A CAPTCHA is not a pause - attempt the solve-captcha skill; if unsolved, skip the job (never pause) and the user finishes it later via the apply skill. Live view at $JOBPILOT_WEB/campaigns/<campaign-id>.
JOBPILOT_API="${JOBPILOT_API:-https://jobpilot.suxrobgm.net}"
Follow ../../shared/setup.md. Read autoApply (defaults applied per field):
| Setting | Default | Notes |
|---|---|---|
minMatchScore | 60 | Qualification threshold (0-100). Inline --min-score overrides. |
maxApplicationsPerCampaign | 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 <campaign-id>.
"resume" → list incomplete campaigns (GET /api/campaigns?status=in_progress), ask which to resume, replay the apply loop on remaining applying/approved/pending jobs."retry-failed <campaign-id>" → fetch the campaign; 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 -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/campaigns?status=in_progress"
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/campaigns?status=paused"
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/campaigns?status=interrupted"
If any matches, ask "Found an incomplete campaign from <startedAt> (status: <status>). Resume or start fresh?" Resume → inject the resume skill with that campaignId.
Otherwise the web UI already created the campaign row when the user submitted /campaigns/new - confirm it exists and use that campaignId. Capture its selected base resume: RESUME_ID=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/campaigns/$CAMPAIGN_ID" | jq -r '.config.resumeId // ""') (empty → fall back to the primary downstream). If invoked manually (rare), create one:
SLUG=$(echo "<query>" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/-\+/-/g; s/^-//; s/-$//')
CAMPAIGN_ID=$(date -u +%Y-%m-%dT%H-%M-%S_${SLUG})
# resumeId is REQUIRED for auto-apply - default to the profile's primary.
RESUME_ID=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/profile" | jq -r '.profile.primaryResumeId // ""')
# maxApplications is OPTIONAL - omit the field entirely for unlimited mode.
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/campaigns" \
-H 'content-type: application/json' \
-d "$(jq -n --arg id "$CAMPAIGN_ID" --arg q "<query>" --arg board "<domain>" --arg rid "$RESUME_ID" \
--argjson minScore <n> \
'{campaignId:$id, query:$q, source:"auto-apply", config:{board:$board, resumeId:$rid, minScore:$minScore}}')"
Surface live view: $JOBPILOT_WEB/campaigns/<CAMPAIGN_ID>.
Extract title/role, keywords, location, preferences. If vague, ask before searching.
Resolve the board:
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/job-boards" | jq --arg d "<domain>" '.[] | select(.domain == $d)'
If no row matches, PATCH the campaign to failed with failReason:"Board <domain> not configured" and stop.
browser_navigate to searchUrl (this is tab 1 - keep it open for the whole campaign).../../shared/auth.md - logs in, and registers a new account when none exists, without asking.browser_snapshot narrowed to the results list (per ../../shared/browser-tips.md) to read { title, company, location, url } per row. This is one viewport - scroll/paginate per Pagination & infinite scroll in ../../shared/browser-tips.md as the loop drains rows (see 2.5); never treat the first batch as all jobs.Walk the tab-1 results top to bottom; at the last loaded row, scroll/page for more (1.2 step 4) before concluding. 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 -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/applied/check?url=$URL_ENCODED&title=$TITLE_ENCODED&company=$COMPANY_ENCODED"
If .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 (../../shared/digest-schema.md), then score server-side:
FIT=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/score-fit" \
-H 'content-type: application/json' \
-d "$(jq -n --argjson digest "$DIGEST" --arg rid "$RESUME_ID" \
'{digest:$digest} + (if $rid=="" then {} else {resumeId:$rid} end)')")
SCORE=$(echo "$FIT" | jq -r '.score')
CONF=$(echo "$FIT" | jq -r '.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 (from the tab-1 detail, or open it briefly if truly needed), rebuild the digest, and rescore first. Below minMatchScore after a fair read → add with status:"skipped", skipReason:"Below minimum match score ($SCORE < $MIN_SCORE)" and move on (no tab). Otherwise add it (status applying) and apply (2.3):
DIGEST=<stringified digest>
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/campaigns/$CAMPAIGN_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}')"
Follow ../../shared/eligibility.md.
job-worker)Hand the job to the job-worker subagent and wait for its compact result. It opens its own tab and runs auth, CAPTCHA, tailoring, form-fill, and submit in isolated context, so the form/posting snapshots never enter this conversation. One worker at a time - the browser is shared; never delegate the next job until this one returns.
Delegate with input JSON:
{ "mode": "apply", "campaignId": "<CAMPAIGN_ID>", "jobKey": "<key>", "url": "<job-url>",
"board": "<domain>", "digest": <DIGEST>, "resumeId": "<RESUME_ID>",
"defaultStartDate": "<autoApply.defaultStartDate>", "salaryExpectation": <remembered-or-null>,
"preSubmitReview": false }
It returns one of applied / failed / skipped / needs_user - handle in 2.4.
The worker already closed its apply tab(s); re-select tab 0. Map its outcome to a terminal write - POST /api/campaigns/$CAMPAIGN_ID/jobs/<key>/result (atomically updates the Job, creates the Application on applied, marks the queue, recomputes the 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 (e.g. CAPTCHA - leave for manual apply via the apply skill)
jq -n --arg r "<skipReason>" '{outcome:"skipped", skipReason:$r}'
# failed (login failure, unexpected page, validation, crash)
jq -n --arg r "<failReason>" --arg notes "<retryNotes>" '{outcome:"failed", failReason:$r, retryNotes:$notes}'
needs_user:
reason:"salary" → ask the user once, remember the answer for the campaign, and re-delegate 2.3 with salaryExpectation set.reason:"2FA" → PATCH the campaign paused, tell the user what's needed, and exit the loop (the worker left its tab open).reason:"payment" → never pay: POST /result outcome:"failed", failReason:"Payment required", and continue.Pace 3-5s before the next result.
The loop ends only on one of these. Before picking the next result, refetch the campaign (GET /api/campaigns/<CAMPAIGN_ID>):
status === "paused" → POST /result outcome:"skipped", skipReason:"Campaign paused by user" for any in-flight applying job, exit.config.maxApplications set AND summary.applied >= config.maxApplications → end. Unset (default) = no cap; keep going.../../shared/browser-tips.md (2 consecutive scrolls with no new rows). First batch done ≠ exhausted. With maxApplications unset, paginate until genuinely dry.NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X PATCH "$JOBPILOT_API/api/campaigns/$CAMPAIGN_ID" \
-H 'content-type: application/json' \
-d "$(jq -n --arg t "$NOW" '{status:"completed", completedAt:$t}')"
Print a summary table, link to $JOBPILOT_WEB/campaigns/<CAMPAIGN_ID>, suggest retry-failed <CAMPAIGN_ID>, the rescan-skipped skill on <CAMPAIGN_ID> to recover dropped jobs, or a new search.
../../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 ../../shared/auth.md); only ask the user when it returns nothing. 2FA - the worker returns needs_user reason:"2FA"; pause and ask (one-time per board). CAPTCHA - never pause: the job-worker detects it up front and attempts solve-captcha; an unsolved CAPTCHA comes back as a skipped outcome (manual apply later), never a pause.job-worker's own tab, which it closes before returning. Delegate sequentially - one worker at a time (shared browser)./result for terminal outcomes.status === "paused" → exit cleanly.paused, ask the user to re-upload.skipped write carries a non-empty skipReason (2.2a). No valid reason → not a skip.