Skip to main content
Admin Modules

Applyr Auto-Apply Engine

Multi-platform job auto-apply engine with AI-powered form filling, anti-bot detection, pipeline tracking, and session management

February 25, 2026

Applyr Auto-Apply Engine

The Jobs page (/admin/jobs) manages Applyr, an automated job application engine that handles multi-platform submissions (LinkedIn Easy Apply and Indeed Apply), AI-powered form filling, anti-bot detection hardening, pipeline tracking, and follow-up scheduling. The engine runs as an OpenRC-managed service on callisto (10.42.0.100) and communicates with the Arcturus-Prime admin UI via a secure API proxy.

Architecture

Arcturus-Prime Admin UI (/admin/jobs)
       |
       | HTTPS via Cloudflare Pages proxy
       | Auth: Cloudflare Access + API key injection
       v
API Proxy (/api/jobs/[...path].ts)
       |
       | HTTP to 10.42.0.100:8585
       | X-Api-Key injected server-side
       v
Applyr FastAPI Service (port 8585)
       |
       ├── Patchright (undetected Chromium)
       ├── Claude Haiku (cover letters, screening answers)
       ├── SQLite (application tracking)
       └── Evidence capture (screenshots + HTML)

The API proxy at /api/jobs/[...path].ts handles authentication via Cloudflare Access, injects the X-Api-Key header server-side (so the key never reaches the browser), and forwards all requests to the Applyr service. SSE streams for real-time status updates are passed through without timeout.

Supported Platforms

PlatformStatusApply MethodAnti-Detection
LinkedInWorkingEasy Apply modalStandard (Patchright)
IndeedHardenedIndeed Apply (iframe)Full anti-bot stack

Anti-Bot Detection (Indeed)

Indeed uses Cloudflare WAF + Turnstile for aggressive bot detection. The engine implements eight layers of protection:

LayerProtection
BrowserPatchright — patches CDP Runtime.enable leak at source level (the #1 detection vector)
FingerprintReal Chrome channel with native UA/sec-ch-ua headers (no spoofing — avoids Cloudflare platform mismatch detection), randomized viewport from 5 common resolutions, --disable-blink-features=AutomationControlled
HeadersAccept-Language: en-US,en;q=0.9 — all other headers use Chrome’s real values to avoid fingerprint mismatch
BehaviorBezier curve mouse movement, per-character typing with random pauses and rare typo+correction, variable-speed scrolling
SessionWarmup browsing on Indeed homepage before applying, cookie persistence
CAPTCHAAuto-detection of 9 Cloudflare challenge indicators, 120-second manual solve window in headful mode
LoginGoogle SSO detection + passwordless verification code flow with 300-second timeout, 60-second manual fallback
Resilience57 fallback CSS selectors across 13 element types, retry with exponential backoff (3 attempts)
Rate Limiting3–10 second extra delay between Indeed page loads

CAPTCHA Handling

When Indeed presents a Cloudflare challenge:

  1. The engine detects it automatically (checks for Turnstile iframe, challenge wrapper, page title changes)
  2. In headful mode (default), the engine pauses and waits up to 120 seconds
  3. The user solves the CAPTCHA manually in the visible browser window on callisto
  4. The engine detects resolution and continues the apply flow
  5. Failed applications retry up to 3 times with exponential backoff

Passwordless Login (Indeed)

Indeed uses passwordless authentication for Gmail accounts. The login flow handles three authentication paths:

PathDetectionHandling
Traditional passwordinput[type="password"] visibleFill password, submit, check for challenge
Google SSO”Continue with Google” / “Sign in with a code instead” textClick “Sign in with a code instead” → verification code flow
Direct verification code”Enter the code” / input[inputmode="numeric"]Wait for manual code entry

Flow:

Enter email → Submit → Challenge check

Password field? → Traditional login
Google SSO page? → Click "Sign in with a code" → Wait for verification code
Verification code page? → Wait for manual code entry (300s timeout)
None detected? → Wait 5s, re-check → 60s manual fallback

The engine detects Google SSO with 3 selectors, verification code pages with 18 selectors (text patterns + input attributes), and uses 4 fallback selectors for the Continue button. In headful mode, the user enters the verification code from their email in the visible browser window.

Browser Fingerprint Strategy

The engine uses the real Chrome binary (chrome channel in Patchright) with no user-agent or header spoofing. Previous versions spoofed Windows UA headers on Linux, which Cloudflare Turnstile detected as a platform mismatch (error 600010). The current approach:

  • Real Chrome channel with native UA and sec-ch-ua headers
  • --disable-blink-features=AutomationControlled to suppress automation flags
  • Patchright patches CDP Runtime.enable leak at source level
  • No user_agent, sec-ch-ua, sec-ch-ua-platform, or sec-ch-ua-mobile overrides
  • Randomized viewport and locale/timezone set to match physical location

Human Behavior Simulation

All interactions use human-like behavior rather than Playwright’s instant .fill() and .click() methods:

  • Mouse movement: Bezier curves with random control points and offset jitter, not instant teleportation
  • Typing: Per-character input with variable delays (50–150ms), occasional longer pauses (300–800ms), and rare typo-then-backspace sequences
  • Clicking: Mouse moves to element first, waits 100–300ms, then clicks with random offset within element bounds
  • Scrolling: Variable-speed smooth scrolling with random pixel amounts, not instant scrollTo()

LinkedIn Form Handling

LinkedIn Easy Apply uses a multi-step modal with different HTML structures for different question types:

Standard Screening Questions

LinkedIn’s built-in questions (work authorization, visa sponsorship, years of experience, etc.) use fieldset[data-test-form-builder-radio-button-form-component] with a dedicated title element. These are detected and answered via keyword matching against the screening KB.

Employer Custom Questions

Employer-specific questions (“Do you agree to our salary range?”, “Please confirm you won’t use AI tools”, etc.) appear on the “Answer these questions from the employer” step. These use plain input[type="radio"] elements without the data-test-form-builder attributes.

The engine handles these with a separate bare radio handler that:

  1. Extracts the question text by trying <legend>, long <span> elements (>10 chars), and non-radio <label> elements — avoiding grabbing “Yes”/“No” option labels as the question
  2. Matches agreement/consent patterns — questions containing “agree”, “acknowledge”, “consent”, “confirm”, or “indicate yes” automatically return “Yes”
  3. Falls back to AI if no pattern matches
  4. Auto-selects first option as last resort (logged for KB expansion)

Field Detection Hierarchy

For each [data-test-form-element] on the current modal step:

PriorityTypeSelectorHandler
1Select (dropdown)select_handle_select()
2Radio (standard)fieldset[data-test-form-builder-radio-button-form-component]_handle_radio()
3Radio (employer)input[type="radio"] (bare)_handle_bare_radio()
4Text inputinput[type="text"]_handle_text_input()
5Number inputinput[type="number"]_handle_text_input()
6Textareatextarea_handle_textarea()
7Checkboxinput[type="checkbox"]_handle_checkbox()

AI Content

Resume Tailoring

The engine generates ATS-optimized resumes tailored for each specific job using Claude Haiku. The prompt strategy:

  • Cherry-picks the 4 most relevant positions from the master resume
  • Mirrors exact keywords and terminology from the job posting
  • Uses the formula: [Action Verb] + [What You Did] + [Quantified Impact] for each bullet
  • Lists 15-25 skills ordered by job posting relevance
  • Includes both acronyms and full names (e.g., “Amazon Web Services (AWS)”)
  • Generates 8-12 additional ATS keywords matching the candidate’s background

A supplementary data/candidate_context.md file provides background context (current situation, personal projects, gap framing) that the AI uses to position the candidate effectively.

AI Content Viewer

The admin UI provides full visibility into what the AI generates for each application. In the History and Queue tabs, each application card has a sparkle wand button that opens a detail modal with three tabs:

  • Resume — Professional summary, skill tags, work experience entries with rewritten bullets, and additional ATS keywords
  • Cover Letter — Full generated cover letter
  • Answers — Screening question/answer pairs from the application form

This allows reviewing AI output before approving queued applications and auditing what was sent for submitted applications.

Session Management

Starting a Session

From the admin UI, click Start to open the session configuration modal:

  1. Profile Selection — choose a candidate profile from data/profiles/. The dropdown shows the candidate name with a subtitle showing current title, location, years of experience, and the number of screening answer categories. Profiles contain identity info, screening answers, career context, and resume references. The selected profile’s screening answers override the default KB during the session.

  2. CSV Selection — choose a scored CSV from data/scored_jobs/. The dropdown shows filename, job count, file size, and last modified date. CSVs come from JobSpyAdvanced-Lab and must include columns: title, company, location, job_url, site, total_score, description.

  3. Score Threshold — minimum total_score for auto-submission (default: 150). Jobs below this threshold are queued for manual review instead of auto-submitted.

  4. Max Applications — maximum applications per session (default: 25).

  5. Platform Selection — checkboxes for LinkedIn and Indeed. Both are checked by default. The engine filters jobs by the site column in the CSV to match selected platforms.

Session Flow

Load CSV → Filter by threshold + platform

For each job:
  1. Navigate to job URL
  2. Detect apply method (Easy Apply / External ATS)
  3. Fill application form (AI-powered answers)
  4. Capture evidence (screenshot + HTML)
  5. Submit or queue for review
  6. Rate-limit delay (Indeed: 3-10s extra)

Session complete → summary stats emitted via SSE

Stopping a Session

Click the Stop button (replaces Start when running). The engine finishes the current application, then stops. The progress bar shows X / Y applications processed.

Vision-Driven Form Filling

When enabled (APPLYR_VISION_ENABLED=true), the engine uses Claude’s vision API to understand each form step visually rather than relying solely on CSS selector pattern matching.

How It Works

On each form step:

  1. Screenshot the modal/form area
  2. Send the screenshot + candidate profile + job context to Claude’s vision API
  3. Receive structured JSON: field type, label, answer, confidence for every visible field
  4. Fill each field using Playwright, matching vision labels to DOM elements via fuzzy matching
  5. Fall back to legacy selector-based logic for any field where vision confidence is below the threshold

Why Vision

The legacy approach uses hardcoded CSS selectors to discover fields and extract labels. This breaks when:

  • Employers use custom question formats without standard data-test attributes
  • Radio button labels are in unexpected DOM locations
  • Form layouts change between platform updates
  • Questions are ambiguous without visual context

Vision sees the form exactly as a human would, and uses the candidate’s full profile to choose correct answers.

Configuration

KeyDefaultDescription
APPLYR_VISION_ENABLEDfalseEnable vision form filling (env var)
APPLYR_VISION_MODELclaude-haiku-4-5-20251001Vision model (env var)
vision_confidence_threshold0.5Below this, fall back to legacy selectors

Fallback Chain

For each form field, the system tries:

  1. Vision answer (confidence >= threshold) — fill using AI’s visual interpretation
  2. Legacy selector — fall back to the original CSS-based field detection + pattern matching + AI text fallback
  3. If the entire vision API call fails, the full step falls back to legacy automatically

Watchdog & Self-Recovery

The engine has a multi-layer resilience system that prevents stuck states and automatically recovers when form fills hang.

Per-Job Timeout

Each job application is wrapped in asyncio.wait_for() with a configurable timeout (default: 180 seconds). If a single job exceeds this limit, the attempt is cancelled, the browser state is recovered, and the engine continues to the next job. The timeout SSE event includes the job title and company.

Watchdog Task

A background asyncio task monitors heartbeat timestamps every 10 seconds. The apply chain emits heartbeats at key checkpoints:

CheckpointWhen
detect_methodAfter determining Easy Apply vs External
generating_resume / resume_generatedBefore/after AI resume tailoring
generating_cover_letter / cover_letter_generatedBefore/after AI cover letter
starting_form_fillBefore entering the form stepper
linkedin_form_step_NEach LinkedIn Easy Apply form step
indeed_step_NEach Indeed Apply form step
evidence_capturedAfter screenshot/HTML evidence saved

If no heartbeat is received for 90 seconds (configurable), the watchdog emits a watchdog_alert SSE event — an early warning before the hard timeout fires.

Circuit Breaker

Tracks consecutive failures. After 5 consecutive failures (configurable), the engine pauses for 120 seconds and emits a circuit_breaker SSE event. This prevents burning through the job list when something systemic is wrong (login expired, network issue, platform changes).

Recovery

On timeout, the engine:

  1. Presses Escape to dismiss any open dialogs
  2. Calls dismiss_modal() to close LinkedIn/Indeed modals
  3. Navigates to a neutral page (LinkedIn feed or Indeed homepage)
  4. If navigation fails, recreates the browser page entirely
  5. Waits 3 seconds for the page to settle, then continues to the next job

Stop Signal

The stop signal (ctx.check_stop()) is checked before each expensive operation and inside form loops. This makes stop responsive within 2-3 seconds, even mid-form-fill.

Config Keys

KeyDefaultDescription
per_job_timeout180Max seconds per single job application
watchdog_stall_timeout90Seconds of no heartbeat before SSE alert
circuit_breaker_threshold5Consecutive failures before pausing
circuit_breaker_pause120Seconds to pause after circuit breaker trips

Application Pipeline

The pipeline tracks applications through stages with a visual funnel:

StageDescriptionTransition
PendingLoaded from CSV, not yet processedAuto → Submitted/Queued/Failed
QueuedBelow threshold, awaiting manual reviewApprove → Submitted, Reject → Skipped
SubmittedApplication sent successfullyManual → Screening/Interview/Offer/Rejected/Ghosted
ScreeningRecruiter/phone screen stageManual → Interview/Rejected
InterviewTechnical, onsite, panel roundsManual → Offer/Rejected
OfferReceived an offerTerminal
RejectedGot a rejectionTerminal
GhostedNo response after follow-up windowAuto (30 days) or manual

External ATS Handling

Jobs that don’t have Easy Apply get marked as external_opened — the engine navigates to the ATS URL but cannot fill external forms. These show in the queue with a “Confirm Submitted” button for manual confirmation after the user applies on the external site.

Review Queue

Jobs below the score threshold land in the review queue. Each queued application shows:

  • Job title and company
  • Score (highlighted if near threshold)
  • Job URL (external link)
  • Approve button — submits the application
  • Skip button — marks as skipped

Follow-Up Tracking

The follow-up panel shows applications past their follow-up date without a status change. A banner at the top of the page shows the count of due follow-ups. Each entry shows the company, position, follow-up date, current status, and notes.

Auto-Ghost

The Mark Ghosted button in the header batch-marks all submitted applications older than 30 days with no status change as “ghosted”. This keeps the pipeline clean and the response rate metric accurate.

Stats Dashboard

The stats row shows aggregate counts across all application history:

  • Total — all applications ever tracked
  • Submitted — successfully sent
  • Screening — in recruiter/phone screen
  • Interview — in interview rounds
  • Offers — received offers
  • Rejected — got rejections
  • Ghosted — no response after 30 days
  • Response % — (screening + interview + offer + rejected) / submitted

Service Management

OpenRC Service

Applyr runs as an OpenRC service on callisto with supervise-daemon for automatic restart:

  • Auto-start: enabled at the default runlevel — starts on boot
  • Auto-restart: supervise-daemon respawns the process on crash (non-zero exit) with a 3-second delay
  • Respawn limits: max 10 restarts within 60 seconds before giving up

GUI Controls

  • Restart button: the header shows a “Restart” button when the service is online. It triggers a restart via the service-restart endpoint (exits with code 1 so supervise-daemon respawns).
  • Offline auto-retry: when the health check fails, the UI shows a pulsing reconnect indicator with a 5-second countdown. It auto-retries and reloads the page when the service comes back.
  • Uptime display: the subtitle shows “Engine idle · Up Xh Ym” when no session is running.

Service Commands

sudo rc-service applyr start
sudo rc-service applyr stop
sudo rc-service applyr restart
sudo rc-service applyr status

API Endpoints

All endpoints are proxied through /api/jobs/* with Cloudflare Access auth and server-side API key injection.

Session Control

MethodPathDescription
POST/api/jobs/startStart apply session (body: csv_path, threshold, max_applications, platforms, profile)
POST/api/jobs/stopStop current session
POST/api/jobs/panic-stopEmergency browser kill (force-closes Patchright)
GET/api/jobs/statusSession status (running, progress, current job)
GET/api/jobs/status/streamSSE event stream for real-time updates

Application Management

MethodPathDescription
GET/api/jobs/queueReview queue (below-threshold applications)
POST/api/jobs/queue/{id}/approveApprove queued application
POST/api/jobs/queue/{id}/rejectReject queued application
GET/api/jobs/historyApplication history (limit param)
POST/api/jobs/{id}/statusTransition application status (query: status, notes, follow_up_date)
POST/api/jobs/{id}/confirm-externalConfirm external ATS submission
GET/api/jobs/{id}/ai-contentTailored resume, cover letter, screening answers
GET/api/jobs/{id}/evidenceGet screenshot/HTML evidence paths
GET/api/jobs/{id}/full-auditCombined: DB record + audit trail + AI content + evidence
GET/api/jobs/evidence-file/{id}/{filename}Serve evidence screenshot/HTML file (binary)

Pipeline & Stats

MethodPathDescription
GET/api/jobs/statsAggregate application statistics
GET/api/jobs/pipelinePipeline funnel with response rate
GET/api/jobs/follow-upsApplications needing follow-up
POST/api/jobs/mark-ghostedAuto-ghost 30-day stale applications

Profiles

MethodPathDescription
GET/api/jobs/profilesList candidate profiles (slug, name, title, location, experience, answer count)
GET/api/jobs/profiles/{slug}Get full profile detail (excludes screening_answers for size)

Data & Config

MethodPathDescription
GET/api/jobs/scored-csvsAvailable scored CSV files
GET/api/jobs/platformsAvailable platforms and status
GET/api/jobs/logsEngine log buffer (since/limit params)
GET/api/jobs/session-logsSession log file list
GET/api/jobs/session-logs/{name}Read specific session log
GET/api/jobs/config/pathsCurrent file path configuration
PUT/api/jobs/config/pathsUpdate file paths at runtime

Service Management

MethodPathDescription
GET/api/jobs/healthHealth check (no auth required)
GET/api/jobs/service-statusPID, uptime, memory usage
POST/api/jobs/service-restartRestart service (supervise-daemon respawn)

Configuration

Key settings in applyr/config.py:

KeyDefaultDescription
headlessFalseKeep false for CAPTCHA solving
indeed_warmup_enabledTrueBrowse Indeed before applying
indeed_challenge_wait_timeout120Seconds to wait for CAPTCHA solve
indeed_verification_code_timeout300Seconds to wait for email verification code
indeed_max_retries3Retry attempts per application
indeed_page_load_delay(3, 10)Extra seconds between page loads
indeed_retry_backoff_base5Exponential backoff base (seconds)
auto_submit_threshold150Score threshold for auto-submission

Environment Variables

Applyr Service (.env)

VariableRequiredDescription
ANTHROPIC_API_KEYYesClaude API key for AI form filling
INDEED_EMAILFor IndeedIndeed login email
INDEED_PASSWORDOptionalIndeed password (not needed for Gmail — uses passwordless verification code)
LINKEDIN_EMAILFor LinkedInLinkedIn login email
LINKEDIN_PASSWORDFor LinkedInLinkedIn login password
APPLYR_API_KEYYesShared API key (must match Arcturus-Prime)
RESUME_PATHYesPath to .docx resume
RESUME_PDF_PATHYesPath to .pdf resume
APPLYR_DATA_DIRNoData directory (default: ./data)

Arcturus-Prime (.env)

VariableRequiredDescription
AUTOAPPLY_API_KEYYesMust match APPLYR_API_KEY
JOBS_API_URLYesApplyr service URL (e.g., http://10.42.0.100:8585)

Application Intelligence Center

The audit page (/admin/applyr-audit) provides a comprehensive view of every detail Applyr sends to employers. It replaces the basic audit viewer with a full intelligence interface for reviewing applications, preparing for interviews, and troubleshooting issues.

Stats Bar

The top bar shows aggregate counts across all audited applications: Total, Submitted, Queued, External, Failed, Skipped. Each counter is color-coded to match the status pill colors.

Search & Filtering

A toolbar below the stats bar provides:

  • Text search — filters by job title or company name (instant, client-side)
  • Status filter — dropdown for submitted/queued/external/failed/skipped
  • Platform filter — LinkedIn or Indeed
  • Sort — newest first, oldest first, highest score, company A-Z

List View

Each application card shows the job title, company, platform, score, apply method, and time ago. Completeness indicators show whether the application has a cover letter, screening answers, and audit trail data. Error badges appear when the application has an error message.

Detail View — 9 Tabs

Clicking an application loads its full data via the combined full-audit endpoint (single API call) and presents it across nine tabs:

TabContent
Employer BriefSummary card with what was sent: key answers, selections made, cover letter excerpt. “Copy Briefing” button formats a plain-text summary for interview prep.
TimelineChronological event log from application start through form steps, field fills, and final result. Each event shows a timestamp and context.
FieldsAll field interactions grouped by form step in collapsible sections. For select/radio fields: shows “Chose X from [A, B, C, D]” format. Source badges: default (green), AI (purple), vision (cyan), fallback (amber), prefilled (gray).
AnswersQ&A pairs merged from two sources: audit trail field interactions (text/textarea types) and the database answers_json column. Deduplicated. “Copy All” button.
Cover LetterFull text with character count, generation timestamp, and copy button.
ResumeWhich resume was used (master vs tailored), filename. If a tailored resume exists, shows summary and skills.
EvidenceInline screenshots from each form step (served via the evidence-file endpoint). Click to expand in lightbox overlay. HTML snapshots open in new tab.
ErrorsError cards with message, context, step number, and timestamp. Shows a green “no errors” message when clean.
RawFull JSON audit data with copy button.

Evidence File Serving

Screenshots and HTML snapshots are stored in data/evidence/YYYY-MM-DD/audit_{company}_{timestamp}/ directories. The API serves them via GET /api/jobs/evidence-file/{app_id}/{filename} with path traversal protection. The Arcturus-Prime proxy passes binary responses through using arrayBuffer() instead of text() to avoid corrupting image data.

Real-Time Logs

The Logs tab streams engine activity in real-time via polling (/api/jobs/logs). Log entries include:

  • Session start/stop events
  • Per-job progress updates (job title, company, score)
  • Apply results (submitted, failed, queued, skipped)
  • Error details with stack context
  • CAPTCHA detection and resolution events
  • Warmup activity

The log buffer holds the last 500 entries in memory. Session logs are also persisted to disk at data/session_logs/.

jobsautomationapplyrapplicationspipelinetrackingindeedlinkedinpatchright