Skip to content
Back to skills

Auto Apply

ASecurity

Search a job board and autonomously apply to matching jobs one at a time, until paused, exhausted, or the max-applications cap is hit.

  • 80 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added May 27, 2026
developmentrustgobashapi

Works with

  • terminal
  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add suxrobGM/jobpilot --skill auto-apply --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Auto Apply?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Auto Apply
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/suxrobgm-auto-apply/badge)](https://www.skillsdirectory.com/skills/suxrobgm-auto-apply)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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: "<query> --board <domain> [--campaign <campaign-id>] [--min-score N] [--max-apps N] | resume | retry-failed <campaign-id>"
---

# Auto-apply - Search + Apply On Demand

Keep the chosen board open in tab 1; for each result that qualifies, delegate the application to the
`job-applier` 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>`.

## Setup

Follow `../_shared/setup.md`. Shared campaign mechanics (applied-check, result writes, worker
input, rules) live in `../_shared/campaign-flow.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>`.

### Campaign Modes

- `"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 retryable `failed` job, POST its
  `/retry` command with the current `retryNotes`, then replay the apply loop on those approved rows.
- Otherwise → search query → Phase 0.

To recover wrongly-`skipped` jobs, use the dedicated `rescan-skipped` skill (it re-scores and
promotes to `approved`; apply them afterward).

## Phase 0: Resolve the Campaign

**`--campaign <id>` passed** (the web UI always passes it - it created the campaign when the user
submitted `/campaigns/new`): use it as `CAMPAIGN_ID` and skip the checks below.

**No `--campaign`** (manual run): look for an unfinished campaign first.

```bash
jobpilot-api GET /api/campaigns --query status=in_progress --query source=auto_apply
jobpilot-api GET /api/campaigns --query status=paused --query source=auto_apply
```

A match in `.items` → ask **"Found an incomplete campaign from `<startedAt>` (status: `<status>`).
Resume or start fresh?"** Resume → run the `resume-campaign` skill with that `campaignId`. Otherwise
create one; `resumeId` defaults to `user.primaryResumeId`, and leave `maxApplications` out for
unlimited:

```bash
jobpilot-api POST /api/campaigns --data '{"query":"<query>","source":"auto_apply","config":{"board":"<domain>","resumeId":"<RESUME_ID>","minScore":<n>}}'
```

Read `.campaignId` as `CAMPAIGN_ID`.

Either way, read the campaign's `config.resumeId` as `RESUME_ID` (`GET /api/campaigns/$CAMPAIGN_ID`;
absent → the primary) and surface the live view: `$JOBPILOT_WEB/campaigns/<CAMPAIGN_ID>`.

## Phase 1: Open the Board (tab 1)

### 1.1 Parse Query

Extract title/role, keywords, location, preferences. If vague, ask before searching.

### 1.2 Search the Chosen Board

Resolve the board:

```bash
jobpilot-api GET /api/job-boards
```

Pick the row whose `.domain` equals `<domain>`. If none matches, command the campaign to `failed`
with `POST /api/campaigns/$CAMPAIGN_ID/status {"status":"failed"}` and stop.

1. `browser_navigate` to `searchUrl` (this is **tab 1** - keep it open for the whole campaign).
2. Follow `../_shared/auth.md` - logs in, and **registers a new account when none exists, without
   asking**.
3. Fill the search fields and submit.
4. Take a `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 **Stop Conditions**); never treat the first batch as all jobs.

## Phase 2: Apply Loop (on demand)

Walk the tab-1 results top to bottom; at the last loaded row, scroll/page for more (per **Pagination
& infinite scroll** in `../_shared/browser-tips.md`) before concluding. For each result:

### 2.1 Pre-filter (no tab)

Dedupe in-board by normalized title+company, then run the applied-check
(`../_shared/campaign-flow.md`). If `.applied`, record the default already-applied skip and
move on - **don't open a tab.**

### 2.2 Score

If the listing row lacks enough detail, read it from the tab-1 snapshot (don't navigate away). Build
the brief (`../_shared/job-brief.md`), then score server-side. Write `$JOBPILOT_TEMP/fit.json` as
`{"brief": <brief object>, "minScore": <MIN_SCORE>, "resumeId": "<RESUME_ID>"}` (drop `resumeId`
when there is none):

```bash
jobpilot-api POST /api/score-fit --data @"$JOBPILOT_TEMP/fit.json"
```

Read `.score` as `SCORE` and `.verdict` as `FIT_VERDICT`, then branch (eligibility per
`../_shared/eligibility.md` - a thin/generic row is **not** a skip):

- **Confident** - `FIT_VERDICT == "trust"` → use the score directly.
- **Uncertain** - `FIT_VERDICT == "deliberate"` → rescore yourself from
  `strongMatches`/`partialMatches`/`gaps`.
- **Needs the full posting** → delegate the row to `job-scorer` `mode:"score"`
  (`{campaignId:$CAMPAIGN_ID, jobKey:<key>, url, resumeId:$RESUME_ID, minMatchScore:$MIN_SCORE}`)
  instead of opening the posting in this conversation. The worker creates every row non-terminal and
  sends ineligible outcomes to `/result`; eligible rows remain `pending`. PATCH an eligible row to
  `applying`, then go straight to apply (2.3):

```bash
jobpilot-api PATCH /api/campaigns/$CAMPAIGN_ID/jobs/<key> --data '{"status":"applying"}'
```

With a usable score from the listing/tab-1 snapshot alone:

- **Below `minMatchScore` after a fair read** → create as `pending`, POST `/result` with
  `outcome:"skipped"`, `skipReason:"Below minimum match score ($SCORE < $MIN_SCORE)"`, and move on
  (no tab).
- **Qualifies** → add it as `applying` and apply (2.3). Write `$JOBPILOT_TEMP/job.json`:

```json
{
  "key": "<stable-id>",
  "title": "<title>",
  "company": "<company>",
  "location": "<location>",
  "url": "<url>",
  "board": "<board>",
  "matchScore": <0-100>,
  "matchReason": "<one line>",
  "status": "applying",
  "brief": "<brief, stringified>",
  "description": "<posting text, when read>"
}
```

```bash
jobpilot-api POST /api/campaigns/$CAMPAIGN_ID/jobs --data @"$JOBPILOT_TEMP/job.json"
```

### 2.3 Apply (delegate to `job-applier`)

Hand the job to the `job-applier` 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 the `job-applier` input from `../_shared/campaign-flow.md`, passing the `brief`
built in 2.2 and `preSubmitReview: false`. It returns one of `applied` / `failed` / `skipped` /
`needs_user` - handle in 2.4.

### 2.4 Record + Continue

The worker already closed its apply tab(s); re-select tab 0. Map its `outcome` to a terminal
`/result` write and route `needs_user` per `../_shared/campaign-flow.md`, with these
campaign-loop specifics:

- `salary` → re-delegate 2.3 with `salaryExpectation` set.
- `verification` → command the campaign to `paused` through `/status` with
  `{"status":"paused","actor":"agent","reason":"<what's needed, e.g. LinkedIn 2FA>"}`, tell the
  user what's needed, and exit the loop (the worker left its tab open).
- `payment` → record the failure and continue with the next result.

Pace 3-5s before the next result.

### 2.5 Stop Conditions

The loop ends **only** on one of these. Before picking the next result, refetch the campaign (`GET
/api/campaigns/<CAMPAIGN_ID>`):

1. `status === "paused"` → POST `/result` `outcome:"skipped"`, `skipReason:"Campaign paused by
   user"` for any in-flight `applying` job, exit.
2. `config.maxApplications` set AND `summary.applied >= config.maxApplications` → end. Unset
   (default) = no cap; keep going.
3. Board exhausted - per **Pagination & infinite scroll** in `../_shared/browser-tips.md` (2
   consecutive scrolls with no new rows). First batch done ≠ exhausted. With `maxApplications`
   unset, paginate until genuinely dry.

## Phase 3: Summary

```bash
jobpilot-api POST /api/campaigns/$CAMPAIGN_ID/status --data '{"status":"completed"}'
```

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.

## Rules

The shared campaign rules (`../_shared/campaign-flow.md`) apply throughout. On top of them:

1. **Autonomous after launch.** No per-job or batch confirmation; the UI launch is the approval.
2. **Board stays in tab 1.** Each application runs in the `job-applier`'s own tab, which it closes
   before returning.
3. **Respect pause.** Re-read the campaign between jobs; `status === "paused"` → exit cleanly.
4. **Missing resume file** → command the campaign to `paused` through `/status` with
   `{"status":"paused","actor":"agent","reason":"Resume file missing"}`, ask the user to re-upload.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.