apply-job
Skill4 starsChanged 11 days agoMITView on GitHub
End-to-end job application workflow. Gates on app limits and evaluate-job verdict, tailors artifacts, builds the render, fills the employer's ATS form (LinkedIn postings go to the employer's own link, or wait for you), submits when the application is complete and verified (or stops for the user in manual mode), verifies the result, and updates the tracker.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- aaronwskinner/claude-skillstheir own version, first seen Jun 1, 2026
- theaayushstha1/job-applier-agenttheir own version, first seen Mar 8, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- theaayushstha1/job-applier-agent★ 17same name
- aaronwskinner/claude-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- templates/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- templates/CLAUDE.mdCLAUDE.md
- answer-questionSkill
- calendar-syncSkill
- coach-gapsSkill
- company-healthSkill
- configureSkill
- discover-companiesSkill
- email-commsSkill
- evaluate-jobSkill
- ingest-mailSkill
- ingest-messagesSkill
- ingest-profileSkill
- intake-extractSkill
- interview-prepSkill
- optimize-linkedinSkill
- reevaluate-strategySkill
- relationship-sourcingSkill
- report-issueSkill
- research-boardsSkill
- research-companySkill
- research-compSkill
- resume-extractSkill
- schedule-meetingSkill
- search-jobsSkill
- setup-searchesSkill
- sync-statusSkill
- tailor-applicationSkill
- track-outcomesSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
49 rules. About 0 are common, 20 are distinctive, and 29 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈20
Distinctive · estimate
This author's own choices. Worth a look.
29
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Use this skill when the user asks to apply, submit, fill an application, or gives a JD URL with clear application intent.
- Very specific: line 29
Mode detection: run careerrat data status.
- Distinctive: line 38
Look up the target company by name.
- Very specific: line 39
If status: blocked or the company's cap is already hit: halt.
- Very specific: line 40
If status: caution: warn and confirm before proceeding.
- Very specific: line 41
If a new cap or cooldown is stated by the user mid-flow (e.g.
- Very specific: line 43
Run careerrat tracker --summary to confirm current application-limits context and echo the result.
- Very specific: line 49
Check workspace/tracker.json for a cached evaluate-job result for this posting (field fitBasis: 'evaluated'):
- Distinctive: line 51
Cache hit and not stale: verify the GATE line is KEEP or user-approved REVIEW, then continue.
- Very specific: line 52
No cache or stale: run evaluate-job and wait for its output.
- Very specific: line 99
Read candidate/targeting.yml#excluded_companies.
- Very specific: line 123
Read: candidate/writing-style.md, candidate/honesty.yml, candidate/evidence.yml.
- Distinctive: line 127
Run placeholder lint on every produced artifact:
- Very specific: line 140
Read candidate/profile.yml#candidate.toolchain (enum: pandoc | libreoffice | word | markdown-only).
- Distinctive: line 146
After building, eyeball the output: page count reasonable, fonts render, no non-ASCII artifacts, no widow/orphan headings, hyphens display …
- Distinctive: line 163
Run it once the form is filled and every upload is attached.
- Distinctive: line 173
Do not work around the refusal, do not click Submit, and do not re-run the command hoping it passes without changing the form.
- Distinctive: line 197
Immediately halt: if the page shows a captcha or Cloudflare human-check, an application-limit blocker, a required manual exercise …
- Distinctive: line 204
Write the blocked state immediately:
- Distinctive: line 226
Re-entry point: when the user clears the blocker and returns, resume at STEP 7 (Fill) — the tail of the form is the re-entry point, not the …
- Very specific: line 237
Use the sanitized agent-visible application defaults supplied by CareerRat:
- Very specific: line 266
JS-rendered portals: if the page body is blank after load, read document.body.innerText via the session browser's script-evaluate …
- Distinctive: line 267
Country/Location required field: verify country and location fields are populated before attempting submit — a missing country silently …
- Very specific: line 269
Do not fabricate values. Read honesty constraints from candidate/honesty.yml throughout the fill (STEP 10 governs this continuously).
- Very specific: line 271
If the posting is on LinkedIn (linkedin.com), do not apply through LinkedIn.
- Distinctive: line 281
Check these in order and stop at the first real one:
- Very specific: line 287
If you find one (an https link that is not on linkedin.com):
- Distinctive: line 294
Do this and stop:
- Very specific: line 313
After Submit (the careerrat apply submit click, or the user's report after a refusal or in manual mode), read the retained supervised …
- Very specific: line 318
Verification-code protocol (M17): if the page shows an emailed verification-code prompt (detected by submitGuard/BLOCKER_SIGNALS in …
- Very specific: line 319
Run careerrat automation status --json and inspect capabilities.mail_access.
- Very specific: line 321
If mail_access is not allowed: true for that platform, it is turned off: halt and ask the user to provide the code manually, and give the …
- Distinctive: line 330
Never trust HTTP 200 alone — read the page content.
- Very specific: line 331
If no confirmation is found: do not mark as Applied and never retry the submit.
- Very specific: line 353
If no matching row exists: create a new entry in applications[] with at minimum company, role, and id (generate a short slug, e.g.
- Very specific: line 354
DB workspace: create it with careerrat data app upsert --data '{"id":"<slug>","company":"<Company>","role":"<Role>"}' (it refuses an id …
- Very specific: line 355
Legacy workspace (no DB): create the entry directly in the tracker.json edit below.
- Distinctive: line 423
Write Contract, AGENTS.md).
- Distinctive: line 437
Legacy workspace (no DB): run validation and snapshot in sequence:
- Distinctive: line 480
If any new application limit was learned during the apply flow (e.g.
- Very specific: line 482
Commit with a Conventional Commits message (e.g.
- Distinctive: line 484
Interview recording (opt-in): When the user books an interview, recommend they record it with a tool such as Granola or equivalent.
- Very specific: line 491
If no recording, omit the field or set it to "".
- Very specific: line 499
Never claim tools, frameworks, or technologies not listed in candidate/honesty.yml#tools.confirmed.
- Very specific: line 500
Never add an Education section or degree claim if candidate/honesty.yml#education.add_education_section is false.
- Distinctive: line 501
Never fabricate metrics, employers, projects, security clearances, or work authorization status.
- Very specific: line 502
Never reveal profile.compensation.current_base in any form field, cover letter, résumé, tracker note, or outbound artifact.
- Very specific: line 503
Never write superlatives or invented outcomes in tracker notes or cover letters.
- Distinctive: line 504
If a form field asks for information the candidate has not provided and you cannot derive it safely: stop, surface the question, and wait …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
apply-job
Use this skill when the user asks to apply, submit, fill an application, or gives a JD URL with clear application intent.
Runs under AGENTS.md. These contracts bind without being restated here: Privacy Invariant (
current_basenever outbound), Honesty Firewall, Placeholder/Bracket Ban, Gate Write-back, Domain-Neutral Rule, Browser Automation Contract, Activity Pulse logging, Tracker verify+snapshot, and Sent-Clears-Draft. Inline reminders at point-of-use are intentional; standalone restatements point back to the relevant AGENTS.md section. Barecandidate/,workspace/,config/, and.internal/paths below are symbolic; resolve them per AGENTS.md's Path Resolution rule.
Inputs
| File | Purpose |
application-limits config (candidate/application-limits.yml only in legacy/export mode) | Per-company caps and cooldowns (step-zero gate) |
| Agent-visible application defaults | Sanitized expected_base, applicant contact facts, and screening answers supplied by CareerRat. Never read raw candidate/form-defaults.yml. |
candidate/targeting.yml | excluded_companies, cut_signals, fit_bands, role_buckets[].priority (OE bucket policy) |
candidate/profile.yml | compensation.expected_base (fallback when form-defaults.yml#expected_base absent), compensation.minimum_base, compensation.minimum_annual_earnings, compensation.oe_min_base, compensation.oe_max_base, candidate.toolchain, contact facts |
candidate/honesty.yml | tools.confirmed, claims.do_not_fabricate, education policy |
candidate/writing-style.md + workspace/writing-samples/ | Voice calibration for tailor step |
workspace/tracker.json | Application row to update after submission |
STEP 0 — Application-limits gate
Mode detection: run careerrat data status. Exit 0 → DB workspace — every
write step below gives the careerrat data <verb> command (Data Write Contract,
AGENTS.md). Nonzero exit → legacy workspace (no DB yet) — every write step below
gives the existing direct JSON-edit instructions, unchanged.
DB mode: read application limits from careerrat data candidate get --json
(application-limits.companies[]). Legacy mode: read
candidate/application-limits.yml if present; skip silently when absent.
- Look up the target company by name.
- If
status: blockedor the company's cap is already hit: halt. Reportreapply_afterdate if set. Offer a bypass option (user must confirm explicitly to override). - If
status: caution: warn and confirm before proceeding. - If a new cap or cooldown is stated by the user mid-flow (e.g. "limit me to 2 apps there per quarter"): use confirm-first friction. In DB mode write it with
careerrat data candidate limits upsert --data '<json row>'; in legacy mode write the compatibility YAML. Echo the durable row that was written.
Run careerrat tracker --summary to confirm current application-limits context and echo the result.
STEP 1 — Body-read gate (evaluate-job)
Check workspace/tracker.json for a cached evaluate-job result for this posting (field fitBasis: 'evaluated'):
- Cache hit and not stale: verify the GATE line is KEEP or user-approved REVIEW, then continue.
- No cache or stale: run
evaluate-joband wait for its output.
Required gate output format (from AGENTS.md Gate Contract):
GATE: KEEP|CUT|REVIEW - reason
FIT: high|med|stretch <score> - why | caveats: ... | priority: ...
COMP: clear|review|below-floor|OE-bucket - reason
COMP ANCHOR: <expected salary to state> - <rationale>
ACTION: apply-now|hold|manual|cut- GATE: KEEP → proceed.
- GATE: REVIEW → confirm with user before proceeding.
- GATE: CUT →
DB workspace: run
careerrat data app set-status <id> cutThen check for a backing comm thread (
jobs[id].commwherecomm.statusis not alreadyclosed). If one exists, compose two calls back-to-back (there is no combined app+comm transaction, so "same write" becomes "same turn"):Patch the comm row locally (read the current shape from
workspace/tracker.json#communications[], setstatus: "closed",nextActionDue: null,nextAction: null,draft: null, carrying every other field over unchanged), then persist the whole row:careerrat data comm upsert --data '<patched full comm row JSON>'Append the note:
careerrat data comm append-message <comm-id> --data '{"direction":"note","at":"<ISO>","body":"Role cut — <reason from GATE line>. No further action."}'
Then verify:
careerrat data verify(re-exports + domain integrity) andcareerrat tracker --verify(schema-level parity). Halt, stop.- Legacy workspace (no DB): write
status: cutto the tracker row. In the sametracker.jsonwrite, check for a backing comm thread (jobs[id].commwherecomm.statusis not alreadyclosed). If one exists, setcomm.status = "closed",comm.nextActionDue = null,comm.nextAction = null,comm.draft = null, and append tocomm.messages[]:{ direction: "note", at: "<ISO>", body: "Role cut — <reason from GATE line>. No further action." }. Runnpm run verify:tracker && careerrat tracker --verify && careerrat trackerafter the write. Halt, stop.
STEP 2 — Comp and exclusions gate
Read candidate/targeting.yml#excluded_companies. If the company is on the list: halt and report.
Honor the evaluated compensation floor before filling. compensation.minimum_base is a guaranteed-base floor. compensation.minimum_annual_earnings is an annual cash earnings floor that may include wages, tips, commission, and recurring cash bonuses. Equity and benefits never count toward either floor. If the comparable posted range is confirmed below the applicable floor and the role does not qualify under an OE-bucket policy: cut. If tips, commission, bonus, or total earnings are not stated clearly enough to compare, keep the role in review instead of guessing.
OE-bucket check: scan candidate/targeting.yml#role_buckets[] for an entry whose priority is oe. If one matches this role, apply candidate/profile.yml#compensation.oe_min_base and compensation.oe_max_base as the floor/target range instead of minimum_base. If the offer falls within [oe_min_base, oe_max_base] the comp gate passes for this bucket regardless of the standard floor.
STEP 3 — FIT rating confirmation
Confirm evaluate-job emitted a FIT line with a tier (high/med/stretch) and score. If it did not, read the persisted app.fitBucket from the tracker row (it reflects the bands in effect at evaluation time). Only if both the FIT line and fitBucket are absent, derive a tier from the gate-output score using candidate/targeting.yml#fit_bands (default: high ≥ 85, med ≥ 65, stretch < 65).
Apply priority logic:
high→ proceed to tailor immediately.med→ confirm with user or hold per their stated preference.stretch→ confirm with explicit justification before continuing.
STEP 4 — Tailor
Check if workspace/writing-samples/ contains files newer than candidate/writing-style.md. If so: run npm run calibrate:style first, then confirm it completed before continuing. If it reports "Not calibrated" (no writing samples yet), draft in a plain, neutral voice and ask the user for a few samples; don't treat the style as set.
Read: candidate/writing-style.md, candidate/honesty.yml, candidate/evidence.yml. Then run careerrat learnings read "<role>" (the helper resolves the family from targeting.yml and exits 0 silently if no file exists yet — a missing file is normal).
Invoke tailor-application to produce workspace/tailored/<Company>_<Role>.md and a cover letter whenever the application accepts one (a cover-letter field — required or optional — or an email/attachment channel). Default to including a cover letter; it is a second ATS keyword-matching surface. Only skip it when the application has no way to accept one.
Run placeholder lint on every produced artifact:
careerrat lint-placeholders workspace/tailored/<Company>_<Role>.md
careerrat lint-placeholders workspace/tailored/<Company>_<Role>_cover.mdBlock on any lint hit — do not proceed to Build until lint is clean.
STEP 5 — Build
Read candidate/profile.yml#candidate.toolchain (enum: pandoc | libreoffice | word | markdown-only).
pandoc: runpandoc <artifact>.md -o <artifact>.docx, then convert to PDF withsoffice --headless --convert-to pdf <artifact>.docx. Confirm both files exist before proceeding.libreoffice/word: runsoffice --headless --convert-to pdf <artifact>on the source file.markdown-only: skip the build; the.mdfile is the upload artifact.
After building, eyeball the output: page count reasonable, fonts render, no non-ASCII artifacts, no widow/orphan headings, hyphens display as bullets. Run lint-placeholders against the built PDF/DOCX path too if the tool supports it.
STEP 6: Submission
Fill the form completely, then stop. You never click the final Submit control yourself, in any submit mode, on any site. Not with a click, not with Enter in a field, not by evaluating script that submits the form. Submitting goes through one command, which reads the live form in the session browser, runs CareerRat's submit checks, and presses Submit only when they all pass:
careerrat apply submit <application-id> --page <session-browser-page-id> --json--page is the page id of the tab you filled (the one orca tab create returned).
Run it once the form is filled and every upload is attached. Read the JSON:
state: "submitted": the site showed a confirmation page. Go to STEP 9 and record Applied.state: "submitted-unverified": Submit was pressed and no confirmation page was seen. Keep the application's current status (the command already setstatusNoteandnextAction), never run the command or click again, and never passsubmitted-unverifiedtoapp set-status. Tell the user to check the site.state: "refused"(exit 1): nothing was pressed.reasons[]names each failed check bycodewith a plainmessageand, where relevant, theitems(field or upload labels) that are missing. The command has already set the job tomanual-applywith a note of what's missing. Do not work around the refusal, do not click Submit, and do not re-run the command hoping it passes without changing the form. Tell the user what's missing, return or focus the retained supervised browser page, and let them finish and press Submit themselves. You may fix a refusal that is yours to fix (a blank required field you can truthfully answer, an upload not yet attached) and run the command again, once.
The checks the command runs (the same ones the browser app's apply driver enforces): the submit
mode is auto (careerrat automation status; submit: manual is set with
careerrat automation submit manual --write and always refuses, so the person presses Submit),
the posting is not on LinkedIn (never auto-submitted), Submit was not already pressed once, the
evaluate gate is KEEP or a user-approved REVIEW, no NEEDS YOU answer is open, the page is the
posting's own site or a known ATS, no captcha, verification-code, attestation or other human
step is showing, no required field is blank, every required upload is attached, and exactly one
positively labelled submit-application control exists. Only the Orca session browser can be
reached from the command; with any other provider it refuses with SESSION_UNREACHABLE and the
person submits.
submit: manual means the same thing for you as a refusal: show a summary of what will be
submitted, return or focus the retained supervised browser page, and let the user press Submit
themselves, then report back.
Either way, never treat an executor's claim of submission as permission to mark the application Applied. Only a seen confirmation page or the user's report does that.
Immediately halt if the page shows a captcha or Cloudflare human-check, an application-limit blocker, a required manual exercise (assessment, coding exercise, Ashby/Workday exercise, etc.), a required account creation or password reset, an ATS spam-rejection, or any auth prompt that is not a supported emailed verification-code flow. When halting on one of these blockers:
- DB workspace:
Write the blocked state immediately:
careerrat data app set-status <id> manual-apply careerrat data app set-fields <id> --data '{"note":"manual-apply: <blocker type> — <url>"}'(e.g.
"manual-apply: Cloudflare human-check — <url>","manual-apply: Ashby take-home exercise required — <url>","manual-apply: Workday account creation required — <url>","manual-apply: ATS spam-flag — contact recruiter directly — <url>").manual-applymeans the human must finish applying; it is ACTIVE and stays visible on the dashboard, never archived. Run both calls back-to-back, never deferring the second.- Comm-thread write-back (blocker case): check whether a backing comm thread exists —
jobs[id].commwherecomm.statusisneeds-reply,drafted, orcomm-dueandcomm.nextActionorcomm.messages[]references this application or posting. If one exists, compose two calls (there is no single partial-patch verb, so this is "same turn," never deferred):Patch the comm row locally (read the current shape, do NOT change
status— the human still needs to act — rewritenextActionto describe the manual step and advancenextActionDueforward by one day so it doesn't show overdue while the human clears the blocker), then persist:careerrat data comm upsert --data '<patched full comm row JSON>'Append the block-reason note:
careerrat data comm append-message <comm-id> --data '{"direction":"note","at":"<ISO>","body":"Application preparation blocked: <blocker type> — manual apply required at <url>"}'
- Report what was found and ask the user how to proceed.
- Re-entry point: when the user clears the blocker and returns, resume at STEP 7 (Fill) — the tail of the form is the re-entry point, not the beginning of the workflow. Re-read the current page state via the session browser before continuing.
- Legacy workspace (no DB):
- Write the blocked state to the application's tracker row immediately — set
status: "manual-apply"(NOT "blocked") and add anotewith the specific blocker type, any visible context text, and the apply URL so the human can finish (e.g."manual-apply: Cloudflare human-check — <url>","manual-apply: Ashby take-home exercise required — <url>","manual-apply: Workday account creation required — <url>","manual-apply: ATS spam-flag — contact recruiter directly — <url>").manual-applymeans the human must finish applying; it is ACTIVE and stays visible on the dashboard, never archived. - Comm-thread write-back (blocker case): in the same
tracker.jsonwrite as step 1, check whether a backing comm thread exists —jobs[id].commwherecomm.statusisneeds-reply,drafted, orcomm-dueandcomm.nextActionorcomm.messages[]references this application or posting. If one exists: do NOT flipcomm.statustowaiting(the human still needs to act), but append anote-direction entry tocomm.messages[]recording the block reason and the manual-apply URL (e.g.{ direction: "note", at: "<ISO>", body: "Application preparation blocked: Cloudflare — manual apply required at <url>" }), rewritecomm.nextActionto describe the manual step (e.g."Complete application manually — Cloudflare block at <url>"), and updatecomm.nextActionDueforward by one day so the due date does not show as overdue while the human clears the blocker. Write app row + comm update atomically — no partial writes. - Report what was found and ask the user how to proceed.
- Re-entry point: when the user clears the blocker and returns, resume at STEP 7 (Fill) — the tail of the form is the re-entry point, not the beginning of the workflow. Re-read the current page state via the session browser before continuing.
- Write the blocked state to the application's tracker row immediately — set
STEP 7 — Fill
Use the sanitized agent-visible application defaults supplied by CareerRat:
name,email,phone,location,linkedin,github,portfoliowork_authorization,requires_sponsorshipexpected_base— use this for any salary form field. Never read or echoprofile.compensation.current_basein any form field.current_employer,current_title— these are pre-reviewed onboarding/form-default values. Fill them without per-application interruption unless the sanitized defaults setconfirm_current_role: true/confirm_current_disclosure: true, the page asks for a different/new disclosure, or the value is missing.screening_answers— exact pre-reviewed answers for recurring custom questions.
The deterministic application filler, not this skill, owns voluntary self-identification. Its local resolver receives the private policy and exact answers without placing either in agent context. Never read, request, log, or update those settings, and never draft or infer a demographic answer. Treat the app-owned fill plan as final for those controls.
Navigate to the application URL using the session browser. Keep the provider on auto
unless the user explicitly changes it; CareerRat resolves the current session browser
(see docs/BROWSER.md). Take an accessibility snapshot of the current DOM before each
interaction to confirm page state.
ATS detection: call hostnameToPortal(url) (from src/core/apply/form-fill.mjs) on the application URL to identify the ATS. Use the matching PORTAL_RECIPE from that module as the canonical field-mapping reference for that platform. If the hostname is unknown, genericMatch applies — the patterns below are drawn from those generic defaults and the known recipes.
Site gotchas (check FIRST): read PORTAL_RECIPES[hostnameToPortal(url)].quirks (both exported from src/core/apply/form-fill.mjs) and apply every quirk listed before and during the fill. That is the platform's list of known traps; a host with no recipe has none. Two cases change your whole approach: (1) portal: "aggregator" means the URL is a job board, not the apply form — extract the outbound "Apply on …" link, navigate to the real ATS, and re-run detection there; (2) Workday quirks mean account creation / RTF rejection — expect a manual-apply halt. This list is the durable home for per-site gotchas; when you learn a new reliable one, add it to the recipe's quirks so the next run inherits it, rather than relearning it.
Field confirmation before fill: call buildFillPlan with formDefaults, profile, and honesty. Treat requiresConfirmation: true as a hard pause, but do not create a pause just because a field is custom or unfamiliar. The resolver now fills configured screening_answers, routine availability/location questions, and honesty-backed tool yes/no questions before returning skip.
Screening answer posture: answer as the candidate from the sanitized application defaults, candidate/profile.yml, candidate/honesty.yml, candidate/evidence.yml, generated tailored artifacts, and the JD. If the answer is directly supported, fill it in first person and continue. Voluntary self-identification questions are never screening prompts and stay outside packet/AI drafting. Do not stop on vague/ordinary prompts like "Why are you interested?", "Years of experience?", "Are you open to travel?", or "Describe relevant experience" when the evidence/profile already supports a truthful answer. Flag the question with the literal NEEDS YOU: <reason> marker, instead of guessing, when the answer would require fabricating, guessing a number/date/security clearance/tool depth, revealing private current compensation, contradicting honesty.yml, or making a materially new disclosure that is not in onboarding data. The marker waits on the operator for that one job only: keep filling and tailoring everything else, never submit that job while the marker remains, and carry on with other jobs.
Known ATS form patterns (domain-general — apply regardless of ATS platform):
- Async react-select dropdowns: click the combobox to open, wait for options to render, filter by visible text, click the option by text. Never type then press Enter — this fires before async data loads.
- Async location typeahead: set via native-setter +
dispatchEvent, wait for the suggestion list to appear, then click the matching suggestion. - File upload (modal-first pattern): open the Attach or Upload modal first, then trigger the file-upload action via the session browser — direct upload without the modal fails silently on many ATS platforms.
- Yes/No toggle buttons (commit pattern): toggle to the opposite state, then toggle back to the intended state to commit React internal state. A single click may not register.
- JS-rendered portals: if the page body is blank after load, read
document.body.innerTextvia the session browser's script-evaluate capability to confirm the page rendered before interacting. - Country/Location required field: verify country and location fields are populated before attempting submit — a missing country silently blocks submission on several ATS platforms.
Do not fabricate values. Read honesty constraints from candidate/honesty.yml throughout the fill (STEP 10 governs this continuously).
If the posting is on LinkedIn (linkedin.com), do not apply through LinkedIn. Branch to STEP 7b instead of filling or submitting here.
STEP 7b — LinkedIn postings (employer link, or you apply)
CareerRat never applies through LinkedIn. LinkedIn's User Agreement bans automation and restricts accounts that use it. That covers Easy Apply and every other apply control on linkedin.com. There is no setting that turns this back on, and an old submit: auto or platforms.linkedin value in automation.yml changes nothing here. Enter this step whenever the posting URL is on linkedin.com. All earlier gates (limits, evaluate-job, honesty) still apply before this step.
1. Look for the employer's own application link
Check these in order and stop at the first real one:
- The saved job record (
applyUrl,externalApplyUrl,link,url,sourceUrl). Alinkedin.com/jobs/view/externalApply/...?url=link holds the employer's link in itsurlvalue. - The JD you already captured.
- The posting's public page, read the same way intake reads any public page (
resolveJobUrl(url, { employerLinkLookup: true })). No login, no session browser, no clicks. Do not click Apply on LinkedIn to find the link.
If you find one (an https link that is not on linkedin.com):
- Save it on the job:
careerrat data app set-fields <id> --data '{"link":"<employer link>"}'. The LinkedIn address stays insourceMeta.linkedinUrl. - Go back to STEP 7 and continue the normal ATS flow at that link. The usual submit rules in STEP 6 apply to that employer site.
2. No employer link, or Easy Apply only
Do this and stop:
- Open the LinkedIn job page in the person's browser (just open the tab). Make no clicks. Do not open the Easy Apply modal. Do not fill, upload or submit anything on linkedin.com.
Record that the job is waiting on the person:
careerrat data app set-status <id> manual-apply careerrat data app set-fields <id> --data '{"statusNote":"Waiting on you: apply on LinkedIn, then tell CareerRat you applied.","nextAction":"Apply on LinkedIn, then tell me when you've applied"}'(Legacy workspace with no DB: set
status: "manual-apply"with the same note and next action on the tracker row.) Keep any existing status that is alreadyappliedor later.- Tell the person: the page is open, you can't apply for them on LinkedIn, and you're waiting for them to say they applied.
Nothing is recorded as applied until the person says so. When they do, record it with the normal outcome path (application.record-external, or careerrat data app set-status <id> applied) and continue into STEP 9 with channel: "LinkedIn".
STEP 8 — Verify
After Submit (the careerrat apply submit click, or the user's report after a refusal or in manual mode), read the retained supervised browser page
and confirm the application advanced past the submit step. Never open the posting
in a new profile for this verification, and never click Submit as part of checking:
- Read the current URL and page text. Check both the URL path (segments:
/confirmation,/thank-you,/submitted,/complete,/success) AND the visible page text ("Application received", "We got your application", "Thanks for applying", or similar) — a match on either constitutes a confirmation signal.confirmationCheck({ pageText, currentUrl, refs })insrc/core/apply/form-fill.mjs(one options object) encodes this logic; mirror that dual-signal check when evaluating manually. - Verification-code protocol (M17): if the page shows an emailed verification-code prompt (detected by
submitGuard/BLOCKER_SIGNALS inform-fill.mjs— phrases such as "verification code", "enter the code", "check your email"), handle it through the narrow mail-access path:- Run
careerrat automation status --jsonand inspectcapabilities.mail_access. - Infer the mail platform from the recipient address when obvious using
inferMailAccessPlatformFromEmail()fromsrc/core/automation/mail-access.mjs:gmail.com/googlemail.com→gmail;outlook.com/hotmail.com/live.com/msn.com→outlook; other real email domains →webmail. If there is no recipient address, ask the user which provider holds the code. Usewebmailfor any provider other than Gmail/Outlook. - If
mail_accessis notallowed: truefor that platform, it is turned off: halt and ask the user to provide the code manually, and give the commands that turn it on (ifreadErroris set, say the settings file can't be read and name it):careerrat automation enable mail_access --writecareerrat automation enable mail_access <gmail|outlook|webmail> --write
- If
mail_accessis allowed, use the session browser to open the provider and followbuildVerificationCodeMailPlan()/classifyMailAccessBlocker()/extractVerificationCodes()fromsrc/core/automation/mail-access.mjs. Forwebmail, pass the visible provider name asproviderNamewhen known. Read only the specific recent verification-code message for this current application or sign-in flow. Do not browse the broader inbox. Do not send, delete, reply, archive, or mark messages read. - Halt immediately on a mail login wall, mail 2FA prompt, captcha, or unexpected interstitial. Set
status: "manual-apply"with a note such as"manual-apply: mail_access blocked — Gmail login wall, provide code manually"and ask the user to provide the code manually. - Once the code is obtained, return to the original application page, enter the code, and continue verification.
- Run
- Take a screenshot of the confirmation state. Save it under
workspace/captures/(gitignored scratch) — never the repo root orworkspace/root — and don't commit it. See the Browser Automation Contract (Local-only + safety) in AGENTS.md. - Never trust HTTP 200 alone — read the page content.
- If no confirmation is found: do not mark as Applied and never retry the submit. In auto mode record
submitted-unverified. Report what the page shows and ask the user to check the site.
STEP 9 — Update tracker and commit
Row-write is the gating action (hard invariant). An application is NOT applied
until its row exists in workspace/tracker.json#applications[]. Write the row
first — before logging the Activity Pulse event, before committing, before
telling the user the application is submitted. Never log an applied activity
event, never commit, and never report success for a posting that does not have a
verified row. In a batch apply session, write each row immediately after its own
STEP 8 confirmation — do not defer the row-writes to the end of the batch (that
is exactly how confirmed submissions end up with an applied activity event and
no tracker row). The Activity Pulse log, the commit, and the success report all
depend on 9b passing.
9a — Write (or create) the row
Locate the application row in workspace/tracker.json:
- Match
applications[]entry byidfield first; if noid, match bycompany+role(case-insensitive). - If no matching row exists: create a new entry in
applications[]with at minimumcompany,role, andid(generate a short slug, e.g.<company-slug>-<role-slug>). Do not halt — the row must exist before writing status.- DB workspace: create it with
careerrat data app upsert --data '{"id":"<slug>","company":"<Company>","role":"<Role>"}'(it refuses an id that already exists, so an existing row is never wiped; for an existing row useapp set-fields), then continue with the field writes below against that same<id>. - Legacy workspace (no DB): create the entry directly in the
tracker.jsonedit below.
- DB workspace: create it with
app.note validation (run before writing the row): note is the INTERNAL submission one-liner — ≤60 chars, one topic, ATS/system + "submitted" only (e.g. "Greenhouse — submitted, confirmation received."). It must NOT contain application-form answer text, work-arrangement Q&A (onsite/hybrid/remote phrases), interview-loop descriptions, or language copied from the JD or form fields. Anything that fails this check routes to statusNote (live state text), compNote (comp intel), or is dropped entirely. A polluted note produces phantom interview Focus cards on the dashboard — prevent it here.
The matched (or newly created) row must end up with:
status: "awaiting"channel: <source — e.g. "direct", "referral", "LinkedIn", "job board name">note: <ATS/system + "submitted" — one clause, ≤60 chars, e.g. "Greenhouse — submitted, confirmation received.">(singularnote, the app-level field — notnotes, which is aconversations[]field; this is search/internal text and is never rendered on a card)statusNote: <present-tense live state, ≤120 chars, e.g. "Applied via Greenhouse — awaiting review.">(this is what the jobs card shows)- Route everything else per the Tracker Content Register in AGENTS.md — comp/OTE caveats →
compNote; why-it-fits/risks →roleFit.why[]/roleFit.risks[]. Never pack multiple topics into one field. artifacts.jd: the full posted job description body — required, never just the link (the live posting disappears when the req closes or the login expires; this embedded copy is the durable record). Carry it over from theworkspace/jobs/fileevaluate-jobsaved; if for some reason it's missing, capture it now per the JD-body capture invariant in AGENTS.md (escalate to the logged-in session browser before giving up) before writing the row.artifacts.coverLetter: the tailored cover letter text submittedartifacts.resume: the résumé filename usedartifacts.resumeNote: one line on tailoring approach (e.g. "led with operations leadership + cost-reduction wins")link: the canonical posting URL
Apply the write per mode:
DB workspace: compose two calls, back-to-back (there is no single verb covering every field above, so "one write" becomes "same turn — never leave the second call for later"):
careerrat data app set-status <id> awaiting --note "<statusNote text>"Sets
statusandstatusNote(via--note) in one atomic transaction, bumps the stamp, refreshes analytics (a new submission is outcome-changing per the Data Write Contract), and logs the activity event.careerrat data app set-fields <id> --data-file <patch.json>A shallow-merge patch (one level) carrying every field
set-statusdoesn't cover (set-statusalready stampsappliedAtwhen the row moves to an applied status):channel,note(the internal one-liner — not the same asset-status's--note, which writesstatusNote),link,compNote/roleFit.why[]/roleFit.risks[](Content Register routing), andartifacts: {jd, coverLetter, resume, resumeNote}(merges one level into the existingartifactsobject). Use--data-filerather than an inline--datastring given the JD-body and cover-letter text length. Not outcome-changing — the precedingset-statuscall already refreshed analytics.app register-artifact <id> --kind resume --path "<resume filename>" --note "<tailoring approach>"is the more purpose-built verb for a single artifact (it stampsartifacts.resume+artifacts.resumeGeneratedAt+artifacts.resumeNotein one call) and is a reasonable substitute for theresume/resumeNotepiece of the patch above — it fits well because a résumé filename is a short, path-like value. It's a poorer fit forartifacts.jd/artifacts.coverLetter, which hold full body text (not a path) that's awkward to pass through a single--pathargv token, so those two stay in theset-fieldspatch. Don't run both against the sameartifactskey in the same turn — pick one path per field.
- Legacy workspace (no DB): edit the matched (or newly created) row directly with the fields above, in one
tracker.jsonwrite. Also setappliedAtto the current ISO datetime (keep an existing one), since noset-statuscall stamps it here and the dashboard would otherwise fall back tocreatedAt.
Comm-thread write-back (submission case): after setting the fields above, before closing the write, check whether this submission fulfills an open comm thread — jobs[id].comm where comm.status is needs-reply, drafted, or comm-due and comm.nextAction or comm.messages[] references this application or posting (the D.E. Shaw / Avature supplemental-form case: a recruiter-requested data-completion tracked in comm must flip to waiting in the same write that records the submission). If such a thread exists:
comm.status = "waiting"comm.nextActionDue = nullcomm.draft = null(if a draft backed it)comm.nextAction = "Awaiting review after submission"(or similar present-tense state)- append to
comm.messages[]:{ direction: "note", at: "<ISO>", body: "Submitted application via <ATS/channel>. Confirmation received." } - DB workspace: compose two calls, back-to-back in the same turn as the app calls above:
Patch the comm row locally (read the current shape from
workspace/tracker.json#communications[], apply thestatus/nextActionDue/draft/nextActionrules above, carrying every other field over unchanged), then persist the whole row:careerrat data comm upsert --data '<patched full comm row JSON>'Append the note:
careerrat data comm append-message <comm-id> --data '{"direction":"note","at":"<ISO>","body":"Submitted application via <ATS/channel>. Confirmation received."}'
- Legacy workspace (no DB): set the fields above in the SAME
tracker.jsonwrite as the app-row change, and append thecomm.messages[]note in that write too.
One-write invariant:
- DB workspace: the two app calls and the two comm calls each atomically persist and auto-export on their own — there is no single combined transaction, so the completeness guarantee is on running all four calls back-to-back in the same turn, never deferring any of them. Then run 9b once.
- Legacy workspace (no DB): the app row update and any comm-thread write-back above must land in a single
tracker.jsonwrite. Do not write the app row, run verify, then write the comm record — that leaves ghost CTAs visible between writes. Write both, then run 9b once.
9b — Verify the row landed (gate)
DB workspace: each
careerrat data <verb>call in 9a already persisted and auto-exportedworkspace/tracker.json+workspace/activity.jsonl(Data Write Contract, AGENTS.md). Run:careerrat data verify careerrat tracker --verifycareerrat data verifyre-exports then runs the same domain-integrity check asnpm run verify:tracker;careerrat tracker --verifycovers the ajv schema-level checkdata verifydoesn't run. Both must exit 0 and the row must now be present (confirm byid). If either fails or the row is missing, stop — fix it with anotherapp set-fields/comm upsertcall (never hand-edittracker.json, it is a regenerated file) and re-run 9b. Do not proceed to 9c.Legacy workspace (no DB): run validation and snapshot in sequence:
npm run verify:tracker careerrat tracker --verify && careerrat trackerBoth must exit 0 and the row must now be present (confirm by
id). If the row is missing or validation fails, stop — the application is not recorded. Fix the row write and re-run 9b before doing anything else. Do not proceed to 9c.
9c — Documents copy (only after 9b passes)
Copy every submitted artifact to the job's Documents folder — PDF for formatted documents, .txt for plain-text answers — under the job folder (Artifact Contract: organized by company, job, then round). Use the real company name (no brackets). Example filenames:
~/Documents/careerrat/<Company>/job-<application-id>/<Company> - Resume.pdf~/Documents/careerrat/<Company>/job-<application-id>/<Company> - Cover Letter.pdf~/Documents/careerrat/<Company>/job-<application-id>/<Company> - Application Answers.txt(if short-answer fields were submitted)
If tailor-application already exported these files to the job folder this turn (it runs the same export), verify the files are present rather than re-copying. The contract is satisfied when the files exist — one copy is enough. workspace/ remains the source of truth; ~/Documents/careerrat/ is the convenience copy the candidate can find without opening the tracker. Unknown detail → generic slug; never a bracket placeholder.
9d — Log and commit (only after 9b passes)
Then log the submission to the Activity Pulse feed (the dashboard's live timeline — see Activity Pulse in AGENTS.md). One event per submission; the id is content-derived, so a re-run never double-logs:
DB workspace: the 9a
app set-statuscall already auto-logged one genericstatus_changeevent in the same transaction. For the richer, submission-specific type, log an additional event (this verb only logs, it never bumps the stamp):careerrat data activity append --data '{"type":"applied","actor":"agent","title":"Applied — <Company>","summary":"<Role> · via <channel>","refs":{"applicationId":"<application id>","company":"<Company>","role":"<Role>","url":"<posting URL>"}}'Legacy workspace (no DB):
careerrat activity append --type applied --actor agent \ --title "Applied — <Company>" --summary "<Role> · via <channel>" \ --company "<Company>" --role "<Role>" --app-id <application id> --url "<posting URL>" --write
If any new application limit was learned during the apply flow (e.g. a blocker message or FAQ stating a reapply window): record it on the tracker row and, after confirmation, persist the durable limit with careerrat data candidate limits upsert --data '<json row>' in DB mode. Legacy compatibility writes remain confirm-first.
Commit with a Conventional Commits message (e.g. feat(apply): submit <Role> at <Company>).
Interview recording (opt-in): When the user books an interview, recommend
they record it with a tool such as Granola or equivalent. Surface the consent
caveat: in all-party-consent jurisdictions (e.g. many U.S. states, Germany, and
others) every participant must give explicit consent before recording starts —
never record automatically. If the user opts in and provides a transcript or
notes after the interview, use interview-prep STEP 5–6 to capture them; the
conversations[] entry should include "recording": "<tool> (consent given)".
If no recording, omit the field or set it to "".
STEP 10 — Honesty hard-stop (continuous, runs throughout all steps)
These constraints run throughout the entire workflow — not just at one step:
- Never claim tools, frameworks, or technologies not listed in
candidate/honesty.yml#tools.confirmed. - Never add an Education section or degree claim if
candidate/honesty.yml#education.add_education_sectionisfalse. - Never fabricate metrics, employers, projects, security clearances, or work authorization status.
- Never reveal
profile.compensation.current_basein any form field, cover letter, résumé, tracker note, or outbound artifact. - Never write superlatives or invented outcomes in tracker notes or cover letters. Evidence must trace to
candidate/evidence.ymlor stated user facts. - If a form field asks for information the candidate has not provided and you cannot derive it safely: stop, surface the question, and wait for the user.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

