| name | apply |
| description | Apply to a job the hybrid way — auto-draft an EMAIL application (into the user's Gmail drafts, resume attached) when the ad accepts email, or hand back a ready-to-submit pack (direct link + cover letter on the clipboard + prepared screening answers) for form-only jobs. Use for "/jobfinder:apply adzuna:12345" or "apply to this job for me". |
jobfinder: apply
Get one job application to the finish line with as little tedium as possible,
without ever submitting something the user hasn't approved. $ARGUMENTS is the
job id (e.g. adzuna:12345 or wfa:987).
The hard constraint, stated honestly: search sources only index jobs — the
real submission happens on the employer's own site or inbox. So there are two
channels, and this skill routes to whichever the ad actually supports:
- email — the ad names an application address → build a ready-to-send email
(resume attached) in the user's Gmail drafts. They review and hit Send.
- form — submission is a web form → do not auto-submit. Hand back a pack
that makes finishing it a few seconds: the direct link, the cover letter already
on the clipboard, the resume path, and prepared answers to paste.
Steps
-
Load grounding. Read ~/.jobfinder/profile/profile.md (and
profile.json). If missing, tell the user to run /jobfinder:profile first —
you cannot write a real application without their background. Do one job at a
time; never batch-apply.
-
Prep the listing.
uv run "${CLAUDE_PLUGIN_ROOT}/scripts/apply.py" prep <id>
This returns the job, a first-guess channel, any emails found in the stored
text, the resume_files, and the applications_dir to write into. If the id
isn't found, the user needs to /jobfinder:search for it first.
-
Read the FULL ad — the stored description is usually a truncated snippet,
so the channel guess isn't final until you've seen the whole thing:
adzuna: → WebFetch the listing url (the Adzuna redirect) for the full
description and any "how to apply" instructions.
wfa: → uv run "${CLAUDE_PLUGIN_ROOT}/scripts/wfa.py" --detail <id>.
Pipe the full ad text through apply.py scan (stdin) to catch an application
email the snippet missed. Recompute the channel: email if any genuine
application address exists, otherwise form. Treat a generic info@ / a
third-party recruiter address with caution — call it out rather than assuming.
-
Draft the application, grounded in the profile. Never fabricate. Do not
invent employers, dates, skills, metrics, or work rights. If the ad
requires something the user lacks (visa/citizenship, a hard credential, a
red-line), stop and tell them — don't paper over the gap or auto-apply
around it. Produce and save into applications_dir:
cover-letter.md — Australian conventions: concise, one page, no US
spelling, addressed to the hiring team, mirroring the ad's priorities with
real evidence from the profile. Where the employer would be impressed by
AI-tool fluency (AI-forward companies, ads that name Claude Code / LLM
tooling), add one tasteful, honest line noting the application itself was put
together with Claude Code — it shows the skill rather than just claiming it.
Skip it for conservative or anti-AI employers, where it reads as a gimmick.
answers.md — prepared answers to the usual screening questions: work
rights, notice period, salary expectation (AUD), preferred location / remote,
relocation. Pull each from the profile; where the profile doesn't say, write
NEEDS USER INPUT rather than guessing.
-
Route by channel.
Email channel — for each application address:
- Pick the resume from prep's
resume_primary (already the best real file,
with transcription artifacts excluded). If resume_primary is null or
resume_note warns there's no real resume, stop and tell the user —
don't attach a .txt transcription artifact or a stand-in. Offer to
generate a clean PDF from their profile, or to have them drop a real
PDF/DOCX into ~/.jobfinder/profile/raw/ (then re-run). Only continue once a
genuine resume exists.
- Encode it:
uv run "${CLAUDE_PLUGIN_ROOT}/scripts/apply.py" b64 <resume_primary>.
This refuses transcription artifacts by design (override needs --force).
- Show the user the whole email first — to, subject, body, and the
attachment filename — and get an explicit "yes". Suggested subject:
Application for <title>, <Full Name>.
- On approval, create it with the Gmail
create_draft tool: to = the
address, subject, body = the cover letter (plain text) plus htmlBody
(same text as HTML, with any URLs left as plain text so Gmail doesn't rewrite
them into google.com/url?q= redirects), attachments = a list of the b64
result objects ({filename, mimeType, content}). Gmail here is draft-only
by design — tell them it's sitting in Gmail Drafts, ready, and the final
Send is their click (an email to a real employer is irreversible, so a
human presses the button).
- Then
tracker.py note <id> "email application drafted to <addr>" and offer
/jobfinder:track applied <id> once they've sent it.
Form channel — never auto-submit:
- Put the cover letter on the clipboard:
printf '%s' "<cover letter>" | uv run "${CLAUDE_PLUGIN_ROOT}/scripts/apply.py" clip
- Present the finish-in-seconds pack: the direct apply URL, "your cover
letter is on the clipboard — paste it", the resume file to upload, and
the prepared screening answers. One line on what to do: open link →
paste → attach resume → submit.
- Then
tracker.py note <id> "apply pack prepared (form)" and offer
/jobfinder:track applied <id> after they submit.
-
Log and close out. Confirm which channel was used, where the draft/pack is,
and the single next action. Don't inflate — if the match is weak or the ad
wants something the user lacks, say so plainly.
Note on true hands-off sending
Auto-sending email on approval (no manual Send) is possible but needs the user
to add SMTP credentials (e.g. a Gmail app password) to ~/.jobfinder/config.json
— a stored secret. Until they opt into that, the email channel stops at a
ready-to-send Gmail draft. Web-form jobs can never be truly hands-off: their
forms, logins, and CAPTCHAs live on the employer's site.