Skip to content

Apply

Apply is JobCtrl's employer-facing boundary. Use this page to configure the profile facts an application may use, review generated materials, choose the approval and automation mode, adopt an optional system browser, and connect Gmail for bounded verification or an explicitly approved email application. The practical review sequence is in Daily Workflow; the enforcement model is in Security.

For the retry and ambiguity model behind employer-facing work, read At-most-once Job Application Submission.

Command spelling

Command blocks on this page use the canonical installed spelling, jobctrl <command>. Contributors running from source can use the checkout-prefixed commands in Local Development.

Default supervised apply path Rehearse first. Approve the exact run. Submit at most once. With approval required, human decisions and code-enforced controls alternate before a live submission.
Human gate Code-enforced boundary
  1. You verify · JobCtrl prepares
    Prepare the exact candidateJobCtrl selects the current materials, application URL, profile version, and canonical profile answers.

    Missing required attestation → stop. JobCtrl never guesses.

  2. JobCtrl enforces
    Dry-run the routeThe agent rehearses the route while the browser blocks form submit and other mutating or data-bearing channels.

    Submission is physically blocked.

  3. You decide
    Review and approveYou inspect the rehearsal and bind approval to the exact materials generation, profile version, URL, and evidence.

    Human gate: missing or stale scope fails closed.

  4. JobCtrl enforces
    Make one live attemptAfter an atomic claim, JobCtrl records durable submit intent and may cross the employer-facing boundary once.

    At most once; never a blind retry.

One attempt, two safe terminal pathsAn uncertain result is never treated as permission to submit again.
  • Recorded outcomeA confirmed terminal result is written to the local event and read model.
  • needs_verificationAn ambiguous crash parks the job for human checking, with no automatic retry.

Candidate Profile Application Fields

Your active Candidate Profile is stored in the jobctrl.db SQLite database; there is no second JSON-backed runtime profile. Values entered through the Profile screen or jobctrl init are validated and saved to SQLite.

The Candidate Profile includes these application_attestations fields for legal or screening questions that Apply automation is not allowed to infer:

  • age_18_plus
  • background_check_consent
  • felony_conviction
  • previously_worked_at_employer

Use true or false only when the answer is explicitly true or false for you. Leave unknown answers as null; live apply automation fails with missing_profile_data:<field> instead of guessing. jobctrl doctor warns when required attestations are incomplete, and Apply Review surfaces the same missing fields before approval when the local profile row has unknown values.

The profile also supports application_preferences.how_heard for common “How did you hear about us?” questions. It is a preference, not a legal attestation; leave it empty when there is no truthful answer.

Materials And Resume Rendering

Use Settings → Model selection → AI execution policy for the primary/fallback tailoring generators, the tailoring judge model, and its minimum score. These non-secret desired values are stored in config.json and apply to newly started workflows. Provider credentials remain on the secret boundary described in Configuration.

The default resume renderer is HTML/CSS printed through Playwright. Apply Review loads the generated HTML source into a rich-text editor so text, formatting, and hyperlink edits, comments, validation, final PDF rendering, and layout boxes stay tied to the same material generation.

Apply Review keeps its pending queue to the left at working desktop widths. The selected application's decision, evidence, and materials then occupy one full-width sequence rather than competing columns. When the surface becomes narrow, the queue moves above the review and the approval/defer/decline actions wrap below their binding explanation; no decision or audit section is removed.

Approval And Automation Modes

Apply can rehearse browser forms and send an exact approved email application. Use dry runs and narrow targets before employer-facing work. Persistence follows the editing surface: every value shown on /discovery is stored in SQLite, while every non-secret desired value under /settings/** is stored in config.json.

SettingWhere to edit itStorageDefaultWhat it does
autoApplyDiscovery → Automation settingsSQLite jobctrl.dbfalseWhen true, a running worker keeps exactly one continuous Apply workflow active for eligible prepared jobs only while auto-apply-browser is explicitly ready. The loop appears in Runs as the standing apply loop. It can run transport-locked rehearsals and exact-approved email sends; browser forms still require manual final submit. Turning it back off cancels that loop.
applyApprovalRequiredDiscovery → Automation settingsSQLite jobctrl.dbtrueWhen true, live claims wait for Apply Review approval and the standing loop parks unapproved jobs. Turning it off removes that claim-time gate, but does not grant browser-submit authority or bypass the owned email sender's exact recipient/attachment approval.
minFitScoreDiscovery → Automation settingsSQLite jobctrl.db7Minimum score for jobs claimed by apply automation, including the standing loop.
applyConcurrencySettings → Generalconfig.json1Number of concurrent apply workers used by apply automation. The standing loop re-reads this setting when it polls.
applyMaxBudgetUsdSettings → General → Application runtimeconfig.json5Per-application AI-agent budget cap in USD.
applyTimeoutSecondsSettings → General → Application runtimeconfig.json900Time limit in seconds for one application-agent run.

Maximum AI budget per application

This setting caps AI-agent spend for one newly started Apply job. 0 is a zero-dollar cap, not an unlimited budget. The per-application cap works inside the shared daily LLM budget; neither setting authorizes submission.

Apply agent timeout

This setting bounds one newly started application-agent run from 60 to 3,600 seconds. It is separate from Temporal activity timeouts and does not change the standing loop's concurrency.

Combinations matter:

  • autoApply: false, applyApprovalRequired: true is the default supervised mode: no standing loop exists, and approved browser forms still end at manual final submit.
  • autoApply: true, applyApprovalRequired: true is a supervised standing loop: unapproved jobs are parked; approved email candidates can use the owned sender, while browser forms stop for manual completion.
  • autoApply: true, applyApprovalRequired: false removes the claim-time approval wait. It does not create autonomous browser submit authority, and the owned email sender still requires an exact recipient/attachment approval.

The daily LLM ceiling and shared setting precedence remain in Configuration. Approval is bound to the current materials, profile, application URL, and qualifying dry-run evidence as described in Security.

Dry-run navigation is intentionally strict: the browser permits one GET to the exact reviewed application URL and records that grant in the receipt. Replays, HEAD, path/query changes, redirects, and later document navigation are blocked. A multi-page application therefore needs another reviewed target or a supervised live/manual path; dry-run does not learn navigation authority from the page or model.

The rehearsal prompt is inspection-only. It contains the reviewed application URL, but no applicant profile, job-description, resume, cover-letter, other generated prose, local artifact path, or artifact-upload capability. Reviewed materials remain local for manual completion, so a hostile page cannot reflect their contents back into model-visible DOM. The agent also has no generic typing, form-fill, keypress, saved-credential, or Gmail verification-code capability. Stop at that boundary and complete any private or write-bearing fields manually.

Final Browser Submission Is Manual

The page-reading model never owns the final browser commit. Every model-driven browser session is transport-locked. A live browser-form claim stops with trusted_final_submit_required before the prompt is rendered, Chrome launches, or the agent starts; direct saga and adapter calls enforce the same boundary. Review the rehearsal and complete the employer form yourself.

This conservative boundary remains in place until JobCtrl has a trusted, canonical final-form manifest that a human can review and a one-shot mediator below the model can submit without giving page content generic click authority. The separate Gmail path does not use browser click authority: JobCtrl's owned sender rechecks its capability, records submit intent immediately before the send, and accepts only the exact recipient and attachment approved in Apply Review.

Repeat-Application Protection

Before every live application claim, JobCtrl compares the target with confirmed prior applications. The Apply Review panel shows the prior job, confirmation fact and date, relationship reason, canonical identity evidence, evidence fingerprint, and the related audit entries.

  • Blocked means the prior application belongs to the same canonical job or an accepted duplicate identity, even when another source or apply URL rediscovered it.
  • Confirmation required means the employer identity matches and the role title is materially equivalent after conservative normalization. Similar employer names do not match, and clearly distinct roles remain eligible.
  • Override ready means the user recorded a reasoned confirmation that is bound to this target, one selected prior application, and the current evidence fingerprint. It authorizes one live claim only.
  • Override consumed means that confirmation has already been claimed. A later live attempt needs a fresh confirmation against the then-current evidence.

Recording a confirmation requires an explicit reason. Apply Review shows a loading state while saving it, refreshes the evidence afterward, and reports a stale-evidence error when the relationship changed before the write completed. It also preserves ordinary error feedback rather than enabling live submit on a failed write. Dry runs remain available because they cannot submit and do not establish application history.

Only canonical identity plus a confirmed ApplicationSubmitted, manual ApplicationManuallyMarked, reviewed applied_confirmation outcome, or legacy applied fact can trigger this protection. Pending Gmail suggestions, notes, failed pre-submit attempts, submit intent alone, and dry runs do not. The worker enforces the decision in the same SQLite claim transaction that protects approval binding, at-most-once submit intent, and needs_verification; turning off applyApprovalRequired does not turn off repeat protection.

Browser Apply Automation

Use Settings → Browser & extension to inspect the managed core browser, enable or disable auto-apply and authenticated LinkedIn capabilities, copy a LinkedIn profile with explicit consent, and pair or rotate the extension token. The screen may passively detect supported Chrome/Chromium installations, but the safe list exposes only an opaque browser kind and display label—never a local executable path. Detection does not launch, adopt, or persist a browser. Choose a detected browser and select Enable to adopt it explicitly. JobCtrl resolves the selection again at enable time and fails closed if the installation has disappeared. Advanced: enter executable path is the explicit manual fallback; an adopted path remains write-only and is not shown again.

Each Apply browser is confined to the canonical origin of the reviewed application URL. Unexpected public redirects, popups, or ATS handoffs to a different origin are blocked rather than silently inheriting the approval. Review the new application destination and start a fresh run when a legitimate provider transition requires another origin.

Origin confinement limits rehearsal navigation; it does not authorize final browser submission. The final browser action remains manual as described above.

CAPTCHA handling during a rehearsal is also bounded. With CapSolver explicitly configured, JobCtrl may send a supported visible hCaptcha, reCAPTCHA, or Turnstile widget's site key and page URL through the owned solver at most once. The provider key and returned token stay outside the model prompt. An unsupported challenge, missing configuration, or failed solve stops the apply path; solving a challenge never grants form-entry or final-submit authority.

When Chrome or Chromium has a standard local default profile, Settings lists it by browser label and can copy it without asking you to navigate to or paste a filesystem path. Only the opaque detected-browser ID crosses the API boundary; the worker resolves the standard profile location again at copy time, copies only Default, and sanitizes the required Chrome root metadata so sibling profiles are excluded. A manual source profile path remains an advanced fallback, is cleared after the request, and is never returned, logged, or persisted. Both paths retain separate affirmative consent: selecting or enabling a browser does not grant profile-copy consent. Rotating the pairing token takes effect immediately. Existing extensions are disconnected; the UI never exposes the token's file path. The CLI commands below remain an equivalent manual-path operator surface.

The same explicitly enabled and separately consented authenticated LinkedIn profile can recover a full LinkedIn posting and its external application URL. Because this is the user's owned authenticated session, JobCtrl does not apply the anonymous robots.txt verdict to that recovery. Public-destination checks, per-host pacing, run request budgets, and audit history remain enforced. This recovery cannot fill or submit an application; apply still requires the normal dry-run, approval, and submission gates.

VariableDefaultWhat it does
JOBCTRL_CLAUDE_BINunsetExplicit apply-agent Claude runtime override. By default apply uses a system claude when present, then the pinned Claude Agent SDK bundled binary.
JOBCTRL_TRUSTED_JOB_SITE_CREDENTIAL_ORIGINSunsetComma-separated exact HTTP(S) origins that may receive the saved job-site password, for example https://jobs.example.com. The current application origin must also match one entry; a job or page URL cannot enroll itself. When unset or unmatched, the credential tool is not exposed and login fails closed. Restart the worker after changing it.
CAPSOLVER_API_KEYunsetConfigure from Settings → Credentials on macOS or the environment elsewhere. It explicitly opts a started apply run into sending a supported widget's site key and page URL to CapSolver. Restart the relevant Python worker after a Keychain edit. The owned solver keeps keys and tokens out of the model prompt; unsupported, unconfigured, or failed solves stop the apply path.
JOBCTRL_LINKEDIN_APPLY_RESOLVERcapability-controlledSet to 0 to disable authenticated LinkedIn posting and outbound apply-URL recovery after it has been explicitly enabled. It cannot enable the feature or grant profile-copy consent by itself.
JOBCTRL_LINKEDIN_APPLY_CHROME_PROFILEbrowser defaultChrome profile name inside the resolver user-data directory.
JOBCTRL_LINKEDIN_APPLY_HEADLESSvisible ChromeSet to 1 to run the resolver headless.

The source checkout installs managed Playwright Chromium for discovery, enrichment, and PDF rendering. The bundled release contains exactly one managed Playwright Chromium headless shell for those core paths and no full Chrome/Chromium application. System Chrome/Chromium is optional in both modes and may be passively detected for display, but is never automatically adopted for authenticated operations. Non-secret desired capability choices, including the explicitly adopted executable configuration, saved under Settings → Browser & extension are stored in config.json:

bash
jobctrl capability list
jobctrl capability enable auto-apply-browser --browser-path /path/to/Chrome
jobctrl capability enable authenticated-linkedin-browser --browser-path /path/to/Chrome \
  --copy-profile-from /path/to/Chrome-profile --consent-copy-profile
jobctrl capability disable auto-apply-browser

The managed optional browser-pack choice intentionally reports unavailable until JobCtrl has a signed pack supply chain; the command does not download an unsigned browser. Authenticated LinkedIn resolution remains unavailable until a separate, explicitly consented copy of an existing profile exists under $JOBCTRL_DIR/browser-profiles/linkedin-apply-url-resolver. JobCtrl never persists the source-profile path, and --yes cannot imply profile-copy consent.

Capability changes are live through /v1/browser-capabilities. The extension pairing token remains a separate private local artifact managed through /v1/extension/pairing-token, and copied browser-profile contents remain under $JOBCTRL_DIR/browser-profiles/; neither is embedded in config.json.

Gmail Connector And Sending Boundary

VariableDefaultWhat it does
JOBCTRL_GMAIL_DIR~/.jobctrl/gmailFirst-party Gmail connector auth directory.
JOBCTRL_GMAIL_OAUTH_CLIENT_PATH$JOBCTRL_GMAIL_DIR/oauth-client.jsonGoogle OAuth Desktop client file.
JOBCTRL_GMAIL_TOKEN_PATH$JOBCTRL_GMAIL_DIR/token.jsonToken written by jobctrl gmail-auth.

Authenticate with:

bash
jobctrl gmail-auth
jobctrl doctor

Before running the first command, enable the Gmail API in a Google Cloud project, create an OAuth Desktop app client, and save its downloaded JSON as $JOBCTRL_GMAIL_OAUTH_CLIENT_PATH. The command opens Google's consent flow and writes a private local token to $JOBCTRL_GMAIL_TOKEN_PATH; jobctrl doctor then re-checks readiness.

The connector requests gmail.readonly and gmail.send. Read-only access is used for bounded verification-code and outcome lookups. Send access is used only for the owned email-application path after a dry-run records the recipient and attachment candidate and Apply Review approves that exact binding. Raw Gmail bodies stay local and are not copied into events, telemetry, broad projections, or logs.

To disconnect, delete the local token and revoke the OAuth client's access in your Google Account's third-party access controls. Re-run jobctrl gmail-auth to grant access again. Removing only the local token prevents JobCtrl reuse but does not revoke Google's server-side grant.

Outreach Follow-Ups

Outreach follow-ups are surfaced-only reminders — JobCtrl never sends and has no outreach-send capability. Their posture:

  • Conservative cadence defaults. When you schedule a follow-up without picking a date, JobCtrl suggests one 7 calendar days after the application was submitted for the first nudge, and 14 calendar days for a subsequent nudge if you have logged no reply. Every suggested date is fully editable per thread — the suggestion is only a starting point.
  • Default-off automation. Any optional recurring follow-up reminder is disabled by default (reminders_enabled = false, mirroring discovery scheduling_enabled). Even when enabled it only surfaces due items in the Follow-ups list and badge — it never sends and never acts on your behalf.
  • A follow-up is due purely as a read-time computation over its date and the clock; marking one done or dismissing it is always your explicit action.