| name | browser-submit |
| description | ATS form submission via Playwright with persistent authenticated profile. Converts `blocked_on: human` manual-submit tasks to `blocked_on: browser-submit` auto-resolved tasks. Orchestrated by cruise-control; never invoked directly for first applications in a company (dry-run + human-approve-first gate).
|
| triggers | ["browser submit","auto-apply","submit application","fill ATS","playwright submit"] |
browser-submit — Career OS Skill (v0.1 skeleton)
Purpose
Close the loop from "resume generated + portal answers drafted" → "application submitted" without requiring a human click. Uses Playwright with a persistent Chrome profile (authenticated once, reused forever) to fill + submit ATS forms across Greenhouse, Ashby, Workday, Lever, amazon.jobs, and LinkedIn Easy Apply.
GATE: dry-run before live-submit
Always dry-run first: navigate → fill everything → screenshot the pre-submit state → STOP. User reviews screenshot → one-approve → actual submit. Only after TWC has seen a clean submit for this ATS+company combination is the dry-run gate relaxed for subsequent applications to that same ATS.
Irreversible actions need irreversible-action discipline (P11 + Kinetic Framework).
Architecture
cruise-control (orchestrator)
↓
browser-submit (this skill)
↓ — ATS-type routing by URL pattern —
├── adapters/greenhouse.js (boards.greenhouse.io/*, *.greenhouse.io, my.greenhouse.io)
├── adapters/ashby.js (jobs.ashbyhq.com/*)
├── adapters/workday.js (*.myworkdayjobs.com/*)
├── adapters/lever.js (jobs.lever.co/*)
├── adapters/amazon-jobs.js (amazon.jobs/*)
├── adapters/linkedin-easy.js (linkedin.com/jobs/view/*)
└── adapters/generic.js (fallback: best-effort heuristic form fill)
↓
Playwright (~/Library/Caches/twc-playwright-profile)
Input contract
type BrowserSubmitTask = {
company: string;
role: string;
ats_url: string;
ats_type: "greenhouse" | "ashby" | "workday" | "lever" | "amazon-jobs" | "linkedin-easy" | "generic";
resume_pdf_path: string;
cover_letter_path?: string;
portal_answers: Record<string, string>;
mode: "dry-run" | "submit";
screenshot_dir: string;
};
Output contract
type BrowserSubmitResult = {
submitted: boolean;
dry_run: boolean;
steps: Array<{ step: string; screenshot: string; elapsed_ms: number }>;
confirmation_id?: string;
error?: string;
requires_human: Array<{ reason: string; at_url: string }>;
};
After a submit-mode run returns submitted: true with a non-empty
confirmation_id, emit the local-only VOW signal. Do not emit this for dry-run
results, failed submits, or submits without confirmation:
bun "$CLAUDE_PLUGIN_ROOT/src/telemetry/nsm.ts" browser-submit "$(jq -nc \
--argjson submitted true \
--arg confirmation_id "{confirmation_id}" \
'{submitted:$submitted,confirmation_id:$confirmation_id}')"
Persistent profile bootstrap
First-time setup (human, one-time per ATS):
- Run:
node "$(ls -v ~/.claude/plugins/cache/xos/career-intelligence/*/skills/browser-submit/browser-submit-probe.js 2>/dev/null | tail -1)" https://app.greenhouse.io test-gh.png
- Playwright opens Chromium with empty persistent profile
- Human logs into Greenhouse (or signs in via Google SSO)
- Close browser
- Profile at
~/Library/Caches/twc-playwright-profile now has auth cookies
- All subsequent Greenhouse submissions reuse these cookies automatically
Repeat for each ATS that requires auth. Most direct company ATS pages (boards.greenhouse.io/company-name) do NOT require auth — only the user's Greenhouse profile page does.
Safety invariants
- NEVER auto-click submit on a role until TWC has seen a passing dry-run for this (ATS, company) combination
- Screenshots stored per-step for audit
- Every submit writes to
brain/logs/browser-submit-log.jsonl with hash of inputs
- If CAPTCHA detected → abort + return
requires_human: [{ reason: "captcha", at_url }]
- If unexpected element (unknown required field) → abort + return
requires_human: [{ reason: "unmapped field", at_url }]
- Rate limit: max 1 submit per 90 seconds (avoid bot-detection heuristics)
Observability
Every invocation logs to $CAREER_HOME/brain/logs/browser-submit-dispatch-log.jsonl with:
{ ts, kind: "browser-submit", company, role, ats_type, mode, success, duration_ms }
Metrics roll up weekly: success rate per ATS, avg duration, requires-human rate, dry-run→submit conversion rate.
Phase roadmap
- Phase 0 (tonight): probe script proves persistent-profile + navigation + screenshot pipeline works.
- Phase 1 (tomorrow): Greenhouse adapter — fill standard fields, upload resume, portal-answers, dry-run stop at submit button.
- Phase 2 (this week): Ashby, Workday, amazon.jobs adapters. Wire into cruise-control.
- Phase 3 (next week): LinkedIn Easy Apply + generic adapter. CAPTCHA detection. Rate limiting.
- Phase 4: Drop dry-run gate after N successful submits per (ATS, company) combo — auto-submit for subsequent roles at same employer.
Related
- Orchestrator:
cruise-control
- Upstream:
resume-engine (generates PDF), application-qa (generates portal answers)
- Downstream:
apply-tracker (records applied status + confirmation_id)
- Infra:
skills/browser-submit/browser-submit-probe.js (bundled in plugin), Microsoft @playwright/mcp (alternative MCP path)