Live application assistant. Reads the active application form in Chrome (via Geometra MCP), loads prior context from the offer evaluation, generates personalized answers, and submits the form in one atomic transaction. When the user is applying to more than one job, this mode is invoked by the orchestrator as a dispatched subagent — never driven from an interactive session directly.
-
[H1] Submit the form with one stable
geometra_run_actionsaction array that chains upload + fill + pick + submit. SetsoftTimeoutMs: 45000,output: "final", andincludeSteps: false; if Geometra returnspaused: truewithresumeFromIndex, immediately continue with the exact sameactionsarray and thatresumeFromIndex. Never split upload / fill / submit across separate direct tools or rebuilt action arrays. why: Greenhouse-style forms regenerate internal field IDs after any DOM-mutating action (especially file uploads); multi-call sequences see stale IDs, enter a retry loop, and burn tens of thousands of tokens. The soft-timeout continuation keeps the MCP call under host timeout without changing the action array or restarting the application -
[H2] Never auto-retry a failed submit. On recovery failure, report the error to the orchestrator and stop. The orchestrator decides whether to re-dispatch. why: duplicate applications are worse than a missed retry — ATS portals often accept a submit whose response was dropped mid-flight, so a retry double-submits. A human must decide.
-
[H3] Outcomes MUST be written as TSV to
batch/tracker-additions/{num}-{slug}.tsv— never append APPLIED / FAILED / SKIP todata/pipeline.md. why:pipeline.mdis the URL inbox ([ ]→[x]); TSVs are the bridge to day files vianpx job-forge merge(see root[H6]in iso/instructions.md) -
[H4] Before dispatching the first subagent in a multi-job run, the orchestrator MUST call
geometra_list_sessionsthengeometra_disconnect({closeBrowser: true}). Every dispatch-round, no exceptions. why: prior aborted subagents leave Chromium sessions stuck in the pool; nextgeometra_connectfails with "Not connected" (see root[H3]) -
[H5] Max 2 parallel
taskdispatches per round. For N jobs, runceil(N/2)sequential rounds of 2. Never emit 3+ dispatches in a single message. Do not start the next round until both current-round subagents return final outcomes (APPLIED,APPLY FAILED,SKIP,Discarded, or a written TSV path); task/session ids are only launch receipts. why: subagent post-cleanup cost and portal state make racing more than 2 unreliable (see root[H1]). A 2026-04-25 OpenCode trace launched round 2 while round 1 was still running, then lost two provider recoveries
-
[D1] Prefer the structured
location_constraintsblock inconfig/profile.ymlover the proselocation.*/compensation.location_flexibilityfields. Fall back to prose only whenlocation_constraintsis absent. why: structured is O(1) field lookup; prose requires LLM interpretation per dispatch. 2026-04-18 empirical: prose path reached the right call but burned interpretation cycles on every candidate. -
[D2] When Geometra MCP is unavailable, ask the candidate to share a screenshot, paste form questions as text, or provide company + role for lookup. why: Geometra is the expected primary path; gracefully degrade without refusing to help.
-
[D3] On a detected role change (role on screen ≠ evaluated role in the report), warn the candidate and ask whether to adapt answers or re-evaluate. Do not silently proceed. why: adapting answers to the wrong role produces mis-targeted cover letters and the candidate won't catch it until the recruiter does
-
[D4] Always pass
imeFriendly: trueonfill_fields— safe default everywhere, load-bearing for Ashby. why: Ashby's React form swallows programmatic text input silently;imeFriendly: truefires composition events that clear React's internal validity state. Zero cost on other portals. Confirmed fix: Supabase #793 (2026-04-19). -
[D5] Fetch
geometra_form_schemaat most once per application, right after the initialgeometra_connect. Operate on labels thereafter. why: schema re-fetches return hundreds of nested field IDs and pollute context; labels don't change mid-flow, so the second fetch is just paying for the same payload twice -
[D5b] Reuse the same Geometra
sessionIdfor every tool call on that job URL until the atomic submit transaction completes (or recovery sequence finishes). Do notgeometra_disconnectmid-flow between schema and submit unless [H1] recovery forces it. why: disconnecting between schema and fill invalidates the SPA session and matches real traces where redundant disconnects outnumbered connects; orchestrator pool cleanup [H3]/[H4] is a separate concern between dispatch rounds, not inside one apply subagent -
[D6] Use
fieldLabeloverfieldIdeverywhere it works. why: labels are stable across DOM refreshes; IDs are regenerated -
[D7] If the orchestrator says a proxy is configured, read the top-level
proxy:block fromconfig/profile.ymland pass that object plusheadless: true,browserMode: "stock",blockDetection: true, andblockedSitePolicy: "manual-handoff"into everygeometra_connectcall — including Call 3 of the recovery sequence — and every Geometra auto-connect call that passespageUrlorurl. If the task prompt includes a legacy inlineproxyobject, pass it through and still set the same headless/browser/block-detection options, but do not echo credentials in status text. If absent, run withheadless: true,browserMode: "stock",blockDetection: true,blockedSitePolicy: "manual-handoff", and no proxy; never invent a proxy URL. why: Geometra MCP >=1.62.3 keeps stock Chromium as the default browser mode, preserves headless operation whenheadless: trueis explicit, and returns structuredblockedSite/manualHandoffmetadata for challenge, CAPTCHA, access-denied, and unsupported-browser states. JobForge should surface those states instead of silently looping on blocked portals. See "BYO Proxy + Block Detection" in modes/reference-portals.md. -
[D8] Upgrade application routing to
@general-paidwhen the offer score is ≥ 4.0/5, the user flags "top-tier", "dream job", or "high-stakes", or the candidate is late-stage/post-screen. why: high-stakes applications need the quality-sensitive prompt and medium reasoning budget even though OpenCode now routes both application tiers through DeepSeek V4 Flash by default -
[D9] If a subagent fails with provider-side errors, do not auto-downgrade or re-dispatch the same URL. Report the provider failure, leave any TSV untouched unless there is a confirmed outcome, and inspect telemetry before retrying. why: OpenCode now pins all JobForge application tiers to DeepSeek V4 Flash; switching
@general-paid→@general-freechanges the prompt/tool budget but not the provider route, so automatic duplicate dispatches add risk without fixing provider availability
geometra_connect:headless: true,browserMode: "stock",isolated: true,blockDetection: true,blockedSitePolicy: "manual-handoff"[D7].- Run
geometra_page_model; do not WebFetch the URL [D5]. - If Geometra is unavailable, ask for screenshot or pasted text [D2].
- Extract company + role; Grep
reports/for a matching evaluation. - Load full report + Section G if present.
- Compare role on screen vs evaluated role [D3].
- If different, pause for the candidate's decision [D3].
- Before dispatch, run Geometra cleanup [H4] and location filter [D1].
- Route high-stakes applications through
@general-paid[D8]. - Extract form questions; classify each Section-G vs new.
- Generate answers from Block B + Block F + Section G + JD.
- Submit via one stable
run_actionsaction array [H1] using labels [D6],imeFriendly: true[D4],softTimeoutMs: 45000,output: "final", andincludeSteps: false. - On session error, run the 4-step recovery; only one retry [H2].
- On provider failure, stop and inspect telemetry before any retry [D9].
- On OTP prompt, fetch the code from Gmail via
gmail_get_message. - Submit the OTP with
geometra_fill_otpand click Submit. - Write outcome as
batch/tracker-additions/*.tsv[H3]. - Cap parallelism at 2 per round [H5]; one in-flight per company.
| If the role on screen... | Action |
|---|---|
| Matches the evaluated report exactly | Proceed with Section G answers |
| Is a closely related variant (same archetype) | Warn, offer to adapt [D3] |
| Is materially different (different archetype) | Warn, offer to re-evaluate [D3] |
| Has no evaluation report | Offer to run auto-pipeline first |
| Location conflicts with profile.yml constraints | Mark Discarded, do not dispatch [D1] |
| otherwise | Ask the candidate what they want |
The apply subagent returns a short structured message to the orchestrator (not prose to the user):
APPLIED <url> — report #NNN, score X.X/5, tenant <ats>
tracker TSV: batch/tracker-additions/<num>-<slug>.tsv
notes: <one-line observation>
Or, on failure:
APPLY FAILED AFTER RECOVERY: <url>
Error 1: <first error>
Error 2: <post-recovery error>
Recommend: inspect telemetry before retrying this URL
Sections below are the detailed runbooks, decision tables, and portal-specific empirical notes for the rules above. The contract is the ## Hard limits / ## Defaults / ## Procedure / ## Routing block above; this material is what the subagent consults during execution.
If the candidate wants to apply to more than one job, this mode MUST delegate each application to its own subagent with max 2 in parallel (Hard Limit #1 in AGENTS.md). For N jobs, run ceil(N/2) sequential rounds of 2. Never drive multi-job applications from a single interactive session: the accumulating Geometra tool results invalidate prompt caching and each message ends up re-processing 100K+ tokens of fresh history — see "Session Hygiene" in .opencode/skills/job-forge.md.
DO NOT dispatch 3+ task calls in one message. Two is the absolute ceiling. This is non-negotiable, even when the user asks for "apply to 10 jobs" — that becomes 5 rounds of 2, not one message with 10 dispatches.
A task/session id is not a result. If OpenCode gives you a ses_...
id or title after dispatch, do not treat that as the subagent return.
Do not create another task to check it. Stop the round, report the
in-flight ids, and resume only after a real outcome is visible in the
subagent return or in an authoritative file (batch/tracker-additions/,
batch/tracker-additions/merged/, or the day file).
For a single application interactively, carry on in the current session — the rule targets multi-job loops.
Before dispatching any batch of apply subagents, cross-check each candidate's location against config/profile.yml. Prefer the structured location_constraints block (deterministic match). Fall back to the prose location.* / compensation.location_flexibility fields only when location_constraints is absent (legacy profiles).
-
Read
config/profile.yml → location_constraints. If present, use the structured fields:location_constraints: remote_us: true | false remote_global: true | false hybrid_cities: [san-francisco, ...] blocked_cities: [new-york, ...] authorized_countries: [US, ...] # ISO-3166 alpha-2 requires_visa_sponsorship: true | false
-
For each candidate, open its evaluation report (
reports/{num}-*.md) and read the Location / Block A content. Extract:mode ∈ {remote, hybrid, onsite},city(lowercase hyphenated),country(ISO-3166 alpha-2 when derivable). -
Apply the filter (decision table):
Role shape Rule Outcome Remote, country ∈ authorized_countries (typically US) remote_us == true→ COMPATIBLEdispatch Remote, country ∉ authorized_countries remote_global == trueAND (requires_visa_sponsorship == falseOR JD mentions sponsorship) → COMPATIBLEdispatch / else skip Hybrid, city ∈ hybrid_citiesCOMPATIBLE dispatch Hybrid or Onsite, city ∈ blocked_citiesINCOMPATIBLE mark Discarded, notelocation mismatch: blocked_city=XHybrid or Onsite, citynot inhybrid_citiesand not inblocked_citiesINCOMPATIBLE by default (hybrid is opt-in per city) mark Discarded, notelocation mismatch: city=X not in hybrid_citiesLocation unclear / ambiguous dispatch with a prompt flag instructing the apply subagent to verify the JD location first and Discard early if confirmed incompatible dispatch-with-flag -
Country/visa: if
requires_visa_sponsorship == falseANDcountry ∉ authorized_countriesAND the JD does NOT explicitly offer sponsorship → INCOMPATIBLE, do NOT dispatch.
When location_constraints is absent, use the prose fields:
- Read
config/profile.ymlforlocation(country, city),compensation.location_flexibility, andvisa_status. - For each candidate, open its evaluation report (
reports/{num}-*.md) and read the Location / Block A content. - Apply the filter:
- If the report says "Remote (US)" / "Remote" / "fully remote" — COMPATIBLE, dispatch.
- If the report says "Hybrid N days in {city}" AND {city} matches
location.cityORlocation_flexibilitysays "open to hybrid in {city}" — COMPATIBLE, dispatch. - If the report says "Hybrid" or "Onsite" at a city NOT in the profile's location set AND
location_flexibilitysays Remote-preferred — INCOMPATIBLE, do NOT dispatch. Mark the tracker entryDiscardeddirectly with notelocation mismatch: profile=X, role=Y. - If unclear or ambiguous — dispatch with a prompt flag telling the apply subagent to verify the JD location first and Discard early if confirmed incompatible.
- Country/visa: if
visa_status: "No sponsorship needed"and the role is outside the authorized country — INCOMPATIBLE, do NOT dispatch.
Why: on 2026-04-18, 5 of 7 candidates dispatched for apply turned out location-incompatible. Each burned an apply-subagent round. The prose-field path reached the right call but cost interpretation cycles per dispatch; the structured path is O(1) field lookup and removes LLM-interpretation risk.
Step 1 — Build the job list (N items)
Step 2 — Dedup: for each candidate, grep all four sources for the URL and for company+role:
data/pipeline.md, all data/applications/*.md day files,
batch/tracker-additions/*.tsv, batch/tracker-additions/merged/*.tsv.
Drop any already APPLIED before counting toward N; pick replacements from the remaining list.
Step 3 — geometra_list_sessions() + geometra_disconnect({closeBrowser: true}) [once, before loop]
Step 4 — For round in ceil(N/2):
pair = jobs[round*2 : round*2 + 2]
# ONE message, 1 or 2 task() calls. Never 3.
task(apply to pair[0])
task(apply to pair[1]) # only if pair has 2
# WAIT for both final outcomes. A session id is not completion.
# Do not dispatch round N+1 while round N is still in flight.
Step 5 — After each round:
write/update batch/postflight-outcomes.json with candidateId, status,
and tracker-tsv artifact path for every terminal outcome.
bash: npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
follow the emitted next action before the next dispatch.
Step 6 — Between rounds: geometra_list_sessions() + geometra_disconnect({closeBrowser: true})
Step 7 — Reconcile outcomes (Hard Limit #6):
bash: npx job-forge merge # TSVs → day file
bash: npx job-forge verify # validate
add merge/verify step observations to batch/postflight-outcomes.json
bash: npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
Step 8 — Summarize outcomes; do NOT auto-retry failures.
If a subagent fails, report it in the summary and let the user decide whether to retry. Never auto-retry — re-running a submit step risks duplicate applications. If a subagent returns SKIP because it discovered a duplicate, treat that as a missed preflight check: finish the current round, then choose a replacement candidate only after re-running dedupe against all four sources.
Outcome routing (Hard Limit #6 in AGENTS.md):
- Subagents write
batch/tracker-additions/{num}-{slug}.tsv— one TSV per job. - Orchestrator runs
npx job-forge mergeonce at the end to consume TSVs into the right day file. - Do NOT append APPLIED / FAILED / SKIP lines to
data/pipeline.md— that file is the URL inbox only.
- Best with Geometra MCP: In headless proxy mode, opencode can interact with the page via
geometra_connect,geometra_form_schema, andgeometra_fill_formwithout opening a visible browser window. - Without Geometra: the candidate shares a screenshot or pastes the questions manually.
1. DETECT → Read active Chrome tab (screenshot/URL/title)
2. IDENTIFY → Extract company + role from the page
3. SEARCH → Match against existing reports in reports/
4. LOAD → Read full report + Section G (if it exists)
5. COMPARE → Does the role on screen match the evaluated one? If changed → warn
6. ANALYZE → Identify ALL visible form questions
7. GENERATE → For each question, generate a personalized answer
8. PRESENT → Show formatted answers for copy-paste
Run geometra_connect against the active page (with Geometra MCP), then geometra_page_model to read the title, URL, and visible content. Do NOT also WebFetch the same URL — Geometra's page model already contains the JD text. Each additional fetch re-pulls the same content into conversation history at full input cost.
Without Geometra: Ask the candidate to do one of these:
- Share a screenshot of the form (Read tool reads images).
- Paste the form questions as text.
- Provide company + role so we can look it up.
- Extract the company name and role title from the page.
- Search in
reports/by company name (Grep case-insensitive). - If there's a match → load the full report.
- If there's a Section G → load the previous draft answers as a base.
- If there's NO match → notify and offer to run a quick auto-pipeline.
Compare the role on screen against the evaluated one. When they differ, do the following.
- Warn the candidate: "The role has changed from [X] to [Y]. Do you want me to re-evaluate or adapt the answers to the new title?"
- If adapt: Adjust the answers to the new role without re-evaluating.
- If re-evaluate: Run a full A-F evaluation, update the report, regenerate Section G.
- Update tracker: Change the role title in the day file under
data/applications/when the row already exists.
Extract ALL visible questions across these field types.
- Free text fields (cover letter, why this role, additional info).
- Dropdowns (how did you hear, work authorization, source).
- Yes/No (relocation, visa sponsorship, current employment).
- Salary fields (range, expectation).
- Upload fields (resume, cover letter PDF).
Classify each question:
- Already answered in Section G → adapt the existing answer
- New question → generate an answer from the report + cv.md
Generate the answer for each question using these inputs in order.
- Report context: Use proof points from Block B, STAR stories from Block F.
- Previous Section G: If a draft answer exists, use it as a base and refine
- "I'm choosing you" tone: Same framework as the auto-pipeline
- Specificity: Reference something concrete from the JD visible on screen
- job-forge proof point: Include in "Additional info" if there's a field for it
Output format:
## Answers for [Company] — [Role]
Based on: Report #NNN | Score: X.X/5 | Archetype: [type]
---
### 1. [Exact form question]
> [Answer ready for copy-paste]
### 2. [Next question]
> [Answer]
...
---
Notes:
- [Any observations about the role, changes, etc.]
- [Personalization suggestions the candidate should review]
When the candidate asks you to actually submit (or when running in auto-pipeline mode at score ≥ 3.0), follow these rules strictly. Greenhouse-style forms regenerate internal field IDs after any DOM-mutating action, especially file uploads. That breaks multi-call fill sequences and forces the model into a retry loop that burns tens of thousands of tokens.
Do the entire submission with one stable geometra_run_actions actions array that chains all steps. Never split upload / fill / submit across separate direct tools, and never rebuild the action array between continuations.
geometra_run_actions({
sessionId: "...",
softTimeoutMs: 45000,
output: "final",
includeSteps: false,
actions: [
{ type: "upload_files", fieldLabel: "Resume/CV", paths: ["/abs/path/cv.pdf"] },
{ type: "fill_fields", imeFriendly: true,
valuesByLabel: { "First Name": "...", "Last Name": "...", ... } },
{ type: "pick_listbox_option", fieldLabel: "Country", value: "United States" },
... (one entry per choice/listbox) ...
{ type: "click", labelOrText: "Submit application" }
]
})
If the response contains paused: true, pauseReason: "soft-timeout", and resumeFromIndex, call geometra_run_actions again immediately with the same sessionId, the exact same actions array, softTimeoutMs: 45000, output: "final", includeSteps: false, and resumeFromIndex set to the returned value. This is a continuation, not a retry, and it is the host-safe path for long Samsara/Greenhouse-style embeds. Stop only if the continued call fails, not merely because it paused.
Always pass imeFriendly: true on fill_fields for Ashby (and safe as a default everywhere). Ashby's React form swallows programmatic text input silently — visible value looks correct, invalidCount stays >0, and Submit fails with "field required" or "flagged as possible spam." imeFriendly: true fires proper composition events that clear React's internal validity state. Confirmed fix: Supabase #793 (2026-04-19). Zero cost on other portals; no reason to leave it off.
Labels are stable across DOM refreshes; IDs are not. If fieldLabel works, use it everywhere. Only fall back to fieldId when two fields share the same label (rare — add a qualifier via sibling text instead).
Trigger: you see ANY of these error strings from a Geometra call:
Not connectedsession expiredunknown sessionFailed to connectWebSocket+closed/error
Recovery sequence — run these FOUR calls in this EXACT order:
Call 1: geometra_list_sessions()
Call 2: geometra_disconnect({ closeBrowser: true })
Call 3: geometra_connect({
pageUrl: "<the same URL as before>",
isolated: true,
headless: true,
slowMo: 350,
browserMode: "stock",
blockDetection: true,
blockedSitePolicy: "manual-handoff",
proxy: <pass through from task prompt if present; omit otherwise>
})
Call 4: geometra_run_actions({
sessionId: "<new sessionId from Call 3>",
softTimeoutMs: 45000,
output: "final",
includeSteps: false,
actions: [... the EXACT same actions array you used before ...]
})
-
Always run all 4 calls. Do not skip Call 1 or Call 2 even if Call 1 shows an empty pool.
-
Do not re-fetch the form schema. Do not call
geometra_form_schemabetween Call 3 and Call 4. Your labels haven't changed, so the sameactionsarray still works. -
Do not edit the actions array. Copy it verbatim from your first attempt. Do not re-pick fieldIds. Do not add or remove actions. Same array in, same array out.
-
Soft-timeout pause is not failure. If Call 4 returns
paused: true, continue with the returnedresumeFromIndexand the exact same action array. Do not count this as the one retry. -
Only ONE retry. If Call 4 ALSO fails, STOP. Return this exact message to the orchestrator:
APPLY FAILED AFTER RECOVERY: <URL> Error 1: <first error message> Error 2: <error after recovery> Recommend: inspect telemetry before retrying this URLDo NOT try a third time. Do NOT try a different approach. The orchestrator will decide whether to retry after inspecting telemetry.
geometra_form_schema returns hundreds of nested field IDs and pollutes context. Fetch it at most once per application, right after the initial geometra_connect, to discover the list of labels. After that, operate on labels only. Do not call geometra_form_schema again "to verify" — you're just paying for the same payload twice.
If you've uploaded a file with a dedicated geometra_run_actions call (e.g., the resume), and THEN try a separate geometra_fill_form or geometra_fill_fields call, the field IDs from the pre-upload schema are already stale. This was the primary failure mode on the Anthropic FDE apply trace — 4 retries, ~10K wasted tokens. The fix is Rule A: do everything in one shot.
Specific portals — Workday "parse my resume", iCIMS multi-step, SAP SuccessFactors — reveal additional fields ONLY after a file upload. In that case, use exactly two run_actions calls: (1) upload + wait_for, (2) fill+submit. After the first call, call geometra_form_schema once to discover the newly-revealed labels, then run the second call using labels. Never more than two phases.
Some Greenhouse tenants (Grafana Labs confirmed, 2026-04-19) render the resume upload as a file input where the default upload_files action readback succeeds but the field stays empty — Submit returns "Resume/CV is required." only after submit is clicked.
Fix: if the resume field shows empty after an upload_files action (either by explicit readback or by a "Resume/CV is required" error post-submit), re-upload using strategy: chooser with x,y coordinates pulled from the upload button's visibleBounds center. Example:
{ type: "upload_files",
fieldLabel: "Resume/CV",
paths: ["/abs/path/cv.pdf"],
strategy: "chooser",
x: 314, y: 474 }
The chooser strategy triggers the native file picker via click-at-coordinates, which bypasses the React-controlled input that silently drops programmatic assignments on some Greenhouse tenants. One retry is enough; if it still fails, mark Failed.
Check for an OTP gate after the candidate (or Geometra) submits — the major portals (Greenhouse, Workday, Lever, Ashby) gate submission behind an email verification code. When an OTP step appears, do this.
- Do NOT stop and ask the candidate to paste the code manually. Use the Gmail MCP.
- Pick the Gmail sender query from the ATS recorded at scan time. The scan subagent records the ATS type in
batch/scan-output-{YYYY-MM-DD}.md(atscolumn) and indata/pipeline.md(| ats={type}suffix). Read that value first — do NOT re-infer the ATS from the URL host when it's already recorded. - Map the
atsvalue to the Gmail sender query (table below). Wait ~5-10 seconds for the email, then callgmail_list_messageswith the matching query. gmail_get_messageon the most recent match, extract the code from the body.geometra_fill_otpto enter it, then submit.
ATS → Gmail sender query lookup (use the ats value recorded at scan time):
ats value |
q for gmail_list_messages |
|---|---|
greenhouse |
from:greenhouse newer_than:10m |
workday |
from:myworkday newer_than:10m |
lever |
from:lever newer_than:10m |
ashby |
from:ashby newer_than:10m |
workable |
from:workable newer_than:10m |
smartrecruiters |
from:smartrecruiters newer_than:10m |
wwr / remoteok |
Follow the apply redirect to the underlying ATS, re-detect the host, then use that row's query. Aggregators do not send OTP emails themselves. |
builtin |
from:builtin newer_than:10m |
| Toast (via Greenhouse + ClinchTalent) | from:toast.mail.clinchtalent.com newer_than:15m OR subject:"verify your login at Toast" newer_than:15m. Default from:greenhouse returns null — Toast routes OTP through ClinchTalent. |
custom / unknown / missing |
newer_than:10m subject:(verify OR code OR confirm) |
Fallback when ats is missing (legacy pipeline entries with no | ats= suffix, or scan-output without an ats column): infer from the URL host — *.greenhouse.io → greenhouse; jobs.ashbyhq.com → ashby; jobs.lever.co → lever; *.myworkdayjobs.com → workday; apply.workable.com / jobs.workable.com → workable; api.smartrecruiters.com / jobs.smartrecruiters.com → smartrecruiters; weworkremotely.com → wwr; remoteok.com → remoteok; builtin.com → builtin; otherwise use the generic verify OR code OR confirm subject query.
Before reporting the submission as failed, always check Gmail. A "submit did nothing" outcome usually means a silent OTP step — not a real failure.
Full OTP recipe and fallback patterns: see "OTP Handling via Gmail MCP" in AGENTS.md.
Select the matching case below — two cases, two different flows. Do NOT mix them.
The row exists. You are UPDATING an existing entry, which is allowed (Pipeline Integrity rule #2 in AGENTS.md):
- Find the existing row in
data/applications/YYYY-MM-DD.md(or an older day file — usergto locate it) - Edit the
Statuscolumn fromEvaluatedtoApplied(orFAILED/SKIP) - Append a confirmation note to the
Notescolumn (e.g. OTP code, confirmation URL) - Do NOT write a TSV. The row is already there.
The row does NOT exist yet. You MUST go through the TSV pathway (Hard Limit #6 + Pipeline Integrity rule #1):
- Write
batch/tracker-additions/{num}-{slug}.tsvwith the canonical 9-column format (see "TSV Format for Tracker Additions" inAGENTS.md) - At the end of the apply run, the orchestrator calls
npx job-forge merge, which inserts the row into today's day file - Do NOT manually add a row to the day file. Do NOT append an
APPLIEDline todata/pipeline.md.
- Update Section G of the report with the final answers
- Suggest next step:
/job-forge contactfor LinkedIn outreach — contact will automatically load this evaluation report and use the top proof points from Block B to craft targeted messages
If the form has more questions than are visible:
- Ask the candidate to scroll and share another screenshot
- Or paste the remaining questions
- Process in iterations until the entire form is covered