add-api-route
Add a new JobPilot API route or module the standard way - contracts schema, controller, service, response schema, optional web hook. Use for "add an endpoint",…
Apply to a single job (URL or pasted page) with fit review, or score and apply the job links pasted into an apply campaign when no argument is given.
$ npx -y skills add suxrobGM/jobpilot --skill apply --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/applyContext preview
The summary Claude sees to decide when to auto-load this skill.
Apply to a single job (URL or pasted page) with fit review, or score and apply the job links pasted into an apply campaign when no argument is given.
name: apply description: Apply to a single job (URL or pasted page) with fit review, or score and apply the job links pasted into an apply campaign when no argument is given. argument-hint: "[job_url_or_pasted_job_page | campaign <id>] (omit to pick up your pasted links)"
Two modes, one shared apply loop:
`queued` rows → ranked table approval → apply all.
User approves once up front. No per-job confirmation after that.
Follow `../_shared/setup.md` to load profile, resume, credentials. Shared campaign mechanics (applied-check, result writes, worker input, rules) live in `../_shared/campaign-flow.md`.
Read `autoApply` for config (defaults applied per field):
| Setting | Default | Notes | | ---------------------------- | ------------------ | ------------------------------------------------------------------------------------------------ | | `minMatchScore` | 60 | Batch-mode threshold (0-100); the campaign's `config.minScore` wins when set. Ignored in single-job mode. | | `maxApplicationsPerCampaign` | `null` (unlimited) | Sent as `config.maxApplications` when set; omit for unlimited batch. Single-job mode forces `1`. | | `defaultStartDate` | `"2 weeks notice"` | Default start-date answer. |
For ATS portals (Greenhouse, Lever, Workday, etc.) the apply step lands on a domain that isn't in `/api/job-boards`; the `job-worker` handles login/registration there per `../_shared/auth.md`.
---
If the argument is pasted content (HTML / text), extract description, Apply URL, company, title. If no Apply URL can be found, stop: **"I need either a job URL or content with a visible Apply link."**
**URL input** → delegate to the `job-worker` subagent with `mode:"review"` so the posting snapshot stays out of this conversation: `{ "mode":"review", "url":"<job-url>", "resumeId":"<primary-or-empty>" }`. Use its returned `matchScore`/`strongMatches`/`partialMatches`/`gaps`/`blockers`/`visaRisk`/`recommendation` to fill the review below; keep its `digest` as `DIGEST` for 1.4.
**Pasted input** → parse the fields yourself (the content is already in hand), build the digest (`../_shared/digest-schema.md`), and `POST /api/score-fit {digest, minScore:<minMatchScore>}` for the score. Its `fit.verdict` drives the recommendation below: `trust` → report the score as-is; `deliberate` → reason from `strongMatches`/`partialMatches`/`gaps` first. Keep the digest in `DIGEST=...` for 1.4.
## Job Fit Review: [Title] at [Company] **Match Score: X/100** **Strong Matches:** [skill - evidence] **Partial Matches:** [skill - what's adjacent] **Gaps:** [skill - what's missing] **Visa/Sponsorship Risk:** [if mentioned] **Verdict:** [1-2 sentence recommendation]
Ask: **"Want me to proceed with the application?"** - `yes`/`go` continue, anything else stop.
Run the applied-check (`../_shared/campaign-flow.md`) with url + title + company. If `.applied === true`, surface the match (title + company + appliedAt + `.match.kind`) and ask whether to proceed anyway. Stop on no.
CAMPAIGN=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/campaigns" \
-H 'content-type: application/json' \
-d "$(jq -n --arg query "<title> at <company>" \
'{query:$query, source:"apply", config:{maxApplications:1}}')")
CAMPAIGN_ID=$(echo "$CAMPAIGN" | jq -r '.campaignId')JOB_KEY=$(date -u +%s)-single
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 "$JOB_KEY" --arg title "<title>" --arg company "<company>" \
--arg location "<location>" --arg url "<job-url>" --arg board "<board>" \
--arg matchReason "<one-line verdict>" --argjson score <0-100> \
--arg digest "$DIGEST" --arg desc "<posting text>" \
'{key:$key, title:$title, company:$company, location:$location, url:$url, board:$board, matchScore:$score, matchReason:$matchReason, status:"approved", digest:$digest, description:$desc}')"Keep `$CAMPAIGN_ID` and `$JOB_KEY`. Live view: `$JOBPILOT_WEB/campaigns/<CAMPAIGN_ID>`. Jump to **Phase 5**.
---
The campaign already exists (the new-campaign dialog creates it); its rows start `queued` - a bare URL with a hostname placeholder title. This skill never creates one. Phase 3 turns those rows into real, scored `pending` jobs.
Skip this when the `campaign <id>` dispatch already set `CAMPAIGN_ID`. Otherwise take the newest `apply` campaign still holding queued rows (`.items` is newest-first):
CAMPAIGN_ID=$(curl -fsS -
An AI agent that applies to jobs for you, on the Claude or Codex subscription you already have.
Repo: suxrobGM/jobpilot
Add a new JobPilot API route or module the standard way - contracts schema, controller, service, response schema, optional web hook. Use for "add an endpoint",…
Review and clean up the given file(s)/folder(s)/module(s): rate organization, find dead code, duplication, coupling, over-engineering, deep nesting, structural…
Create and apply a Prisma migration safely against the remote (tunneled) PostgreSQL. Use for schema changes - "create a migration", "apply migrations", "add a…
Add knip to a TypeScript project and use it to remove dead exports, collapse pass-through barrels, and narrow every export to what another file actually…
Bump the unified JobPilot version (host + plugin), update the changelog, commit, and tag a new release
Rebuild the .NET terminal host, restart it, and wait for /healthz. Use after any C# change under apps/terminal, or when asked to "restart the terminal host".