# /outcome - Record the Result of an Application You are recording what happened to a job application: progress updates (interview invitations, stages completed, offers) and final resolutions (hired, rejected, no response). The data lands in two places the framework already reads but nothing systematically writes: - `job_search_tracker.csv` - the status column that `/scrape` and `/rank` use for dedup and exclusion - `documents/applications/_/` - the per-application archive (posting, submitted drafts, `outcome.md`) that `/setup` Path A mines to calibrate `04-job-evaluation.md` and surface STAR candidates `/outcome` writes the data; `/setup` interprets it. This command never edits the evaluation framework or profile files itself. The command also owns the stretch *before* there is an outcome to record: the **follow-up branch** (Step 2b) surfaces open applications that have gone quiet, drafts a brief follow-up note in the user's voice, and logs it - so the chase and the resolution it eventually leads to live in one flow. Follow these steps **in order**. --- ## Step 0: Parse Input `$ARGUMENTS` may contain: - Nothing → list open applications and ask which one to update - A company name (optionally with a role), e.g. `/outcome acme` or `/outcome acme ml engineer` → target that application - `followup` → enter the follow-up branch (Step 2b) over every quiet open application, using the default threshold of **10 days** - `followup `, e.g. `/outcome followup 14` → follow-up branch with an N-day threshold - `followup `, e.g. `/outcome followup acme` → draft a follow-up for that application now, regardless of threshold - `stale` or `sweep` → enter the stale application sweep branch (Step 2c) over open applications quiet for **60+ days** - `stale ` or `sweep `, e.g. `/outcome stale 90` → stale sweep branch with an N-day threshold --- ## Step 1: Load State and Identify the Application 1. Read `job_search_tracker.csv`. If it does not exist, create it with the standard header: ``` date,company,sector,role,role_type,channel,status,contact_person,fit_rating,notes,cv_file,cover_letter_file,source,deadline ``` **If the file exists and its header does not end in `,deadline`, append `,deadline` to the header line only** - no data row is touched. Legacy rows then read as an empty deadline. This is the one edit to an existing tracker this command may make outside a matched row, and Step 4's "never restructure the CSV" governs that row, not this header line. 2. **With an argument:** match rows case-insensitively on company (and role, if given). One match → proceed. Several → list them and ask. None → the application was made outside the workflow; collect company, role, date applied, channel, and posting URL from the user and add a tracker row. 3. **Without an argument:** list all rows whose status is not final (see **Tracker status vocabulary** below) as a numbered table (company, role, date applied, current status, deadline, days quiet, follow-ups sent) and ask which to update. The two derived columns come straight from existing data: **days quiet** counts from the row's `date` or the latest dated entry in `notes`, whichever is more recent; **follow-ups sent** counts the `followed up YYYY-MM-DD` markers in `notes`. If any open row is 10+ days quiet with fewer than two follow-ups sent, add one line under the table: "Some of these have gone quiet - want a follow-up draft? (Step 2b)". If any open rows are 60+ days quiet, also offer: "You have applications quiet for 60+ days — run `/outcome stale` to batch-resolve them (Step 2c)." If every row is resolved, say so and stop. **`drafted` rows are listed but never counted as quiet** - nothing was sent, so nobody is late replying. List them under their own heading ("Drafted, not yet submitted"), leave **days quiet** and **follow-ups sent** blank, and keep them out of the follow-up offer above. **Deadline urgency is the one clock that does apply to a drafted row.** Show the `deadline` column when the row has one and leave it blank otherwise. Mark a deadline within 7 days with 🔥 and one that has already passed with ⚠, on the same 7-day threshold `/rank` Step 3 uses so the two commands never disagree. A passed deadline on a `drafted` row is the failure this column exists to catch - documents written, never sent, and now unsendable - so name it in one line under the table rather than leaving the user to compare dates. This changes nothing about the follow-up offer: a drafted row is still never chased, because nobody is late replying to something that was never sent. 4. Derive the archive folder name: `documents/applications/_/` by the **Subfolder naming** rule in `documents/README.md`. Check whether the folder and an `outcome.md` already exist - if so, you are updating, not creating. --- ## Tracker status vocabulary Canonical spellings for the tracker CSV `status` column (underscores, never spaces): `drafted` | `applied` | `interview` | `offer` | `hired` | `rejected` | `no_response` | `offer_declined` | `withdrawn` - **Final** (application closed): `hired`, `rejected`, `no_response`, `offer_declined`, `withdrawn` - **Open**: everything else, `drafted` included — a row is active until its status is one of the **Final** values. - **`drafted`** is open but distinct — nothing was sent, so no follow-up is ever due. - Readers must also accept the legacy space spellings `no response` and `offer declined` on read, so that existing trackers keep working without a migration. Never write them — they are the same values as `no_response` and `offer_declined`, not separate statuses, equally **Final**, and every rule that names one applies to the other. > Distinct from the archive `Status:` enum in `documents/README.md` > (`in_progress` | `hired` | `offer_declined` | `rejected` | `no_response` | `interview_only`), > which describes the per-application `outcome.md` file, not this column. The two enums > are never written to the same field. --- ## Step 2: Collect What Happened Ask the user what happened, then classify: **Progress updates** (application still open): - Interview invitation / stage scheduled or completed (phone screen, technical, case, final round) - Offer received (not yet accepted or declined) **Resolutions** (application closed) — these map to the archive `Status:` enum in `documents/README.md` that `/setup` parses (distinct from the tracker CSV column; see **Tracker status vocabulary** above): - `hired` - accepted an offer - `offer_declined` - received an offer, turned it down - `rejected` - explicit rejection at any stage - `no_response` - no reply; if the user is unsure whether to call it, note how long it has been since the last contact and let them decide - do not impose a cutoff - `interview_only` - reached interviews but the process stalled or was abandoned without an explicit rejection Also collect, without interrogating - one or two open questions are enough: - Dates for the stages reached - Any feedback received, verbatim where the user remembers it - What they'd do differently, and any signal about what the company valued (these feed `/setup`'s calibration and STAR-candidate mining, so concrete beats polished) --- ## Step 2b: Follow-Up Branch (chase a quiet application) Enter this branch from the `followup` argument (Step 0) or from the offer under the open-pipeline table (Step 1.3). Standard practice is a brief, polite follow-up one to two weeks after applying, at most twice; this branch operationalizes that. **Candidates.** An application qualifies when its status is neither final nor `drafted`, the threshold has passed since its `date` (or since the last `followed up` marker in `notes`, if any), and it has fewer than **two** logged follow-ups. Parse dates defensively - skip rows whose dates do not parse and say so rather than guessing. Present qualifying applications as a table (company, role, days quiet, follow-ups sent, channel, contact person) and draft only for the ones the user picks. **Threshold.** The 10-day default is deliberately earlier than `/gmail-sync`'s 30-day staleness flag (its Step 9): that check is a read-only alarm that a row has been forgotten entirely; this branch is the proactive nudge while a reply is still plausible. The two numbers serve different moments, which is why they differ. **Drafting.** For each selected application: 1. Read the archive folder: the `job_posting.md`, `cv_draft.tex`, and `cover_letter.tex` that Step 3 maintains are the source of **every claim** the note may make - this is Rule 3 (never fabricate) widened to "no new claims": a follow-up that introduces skills or experience the submitted materials don't contain is a fabrication vector. 2. Apply the writing style rules from `03-writing-style.md` (no cliches, no em-dashes, warm but direct), and match the application's language - draw the register from the archived cover letter. 3. Write roughly **60 to 120 words**: address the `contact_person` from the tracker if present (otherwise the team, in the application's language); one sentence restating interest in the specific role; one concrete value-reminder drawn from the submitted materials; one polite question about the timeline. No pressure, no "just checking in" filler. 4. Shape it for the `channel` column: email (with a subject line reusing the application's headline), LinkedIn message (shorter, no subject), or portal message (plain text). 5. Present the draft and iterate until the user is happy. **Logging.** Once the user confirms they will send it (or have sent it), log it in the same turn - an unlogged follow-up breaks the next run's quiet-days math: - Append `followed up YYYY-MM-DD` to the row's `notes` column (Step 4's rule applies: append a dated note, never restructure the CSV). - Save the final note as `followup_YYYY-MM-DD.md` in the application's archive folder. Safe by documented convention: `/setup` reads only the four named archive files and ignores extras (the same rule that covers `/interview`'s prep files), and `documents/applications/**` is gitignored personal data. If the user decides not to send, log nothing. **Termination.** When an application hits two follow-ups and stays silent, do not offer a third. This is the moment to continue in this same command's Step 2: note how long it has been since last contact and let the user decide whether to record `no_response` - as ever, no imposed cutoff. And if the user mentions an actual response while in this branch (an interview invitation, a rejection), drop out of the branch and record it through the normal Step 2 flow. --- ## Step 2c: Stale Sweep Branch (batch-resolve quiet applications) Enter this branch from the `stale` or `sweep` argument (Step 0), or from the suggestion under the open-pipeline table in Step 1.3. In an extended job hunt, applications that received no response accumulate and clutter the tracker, `/html-report` funnel metrics, and `/notion-sync`. This branch operationalizes batch-cleaning old quiet applications while keeping the user in full control. **Candidates.** An application qualifies when its tracker `status` is open and submitted (`applied` or `interview`), the threshold has passed since its `date` (or since the latest dated entry in `notes`, whichever is more recent), and its status is neither final nor `drafted` (`drafted` applications were never submitted and cannot receive a response). Parse dates defensively — skip unparseable rows with a note. **Threshold.** The default threshold is **60 days** quiet. If the user specified an integer `` (e.g. `/outcome stale 90` or `/outcome sweep 45`), use N days instead. **Presentation.** If no open applications exceed the threshold, report: > "No open applications exceed the -day quiet threshold. Your tracker is up to date!" and stop. Otherwise, present qualifying applications as a numbered table: ``` ## Stale Applications ([K] quiet for [N]+ days) | # | Company | Role | Date Applied | Days Quiet | Follow-ups Sent | Current Status | Proposed Status | |---|---------|------|--------------|------------|-----------------|----------------|-----------------| | 1 | Acme | SWE | 2026-05-10 | 118 | 2 | applied | no_response | | 2 | Beta | MLE | 2026-06-01 | 96 | 1 | applied | no_response | ``` Then ask: > **How would you like to resolve these applications?** > > - **`all`** — Mark all [K] applications as `no_response` and update archives > - **`select`** — Specify which numbers to resolve (e.g. "1, 3" or "1-4") > - **`skip`** — Cancel without making any changes Wait for the user's explicit response before writing anything. **Execution.** For each application the user confirms: 1. **Update Tracker:** update the row's `status` column to `no_response` (using the canonical spelling from **Tracker status vocabulary**). Append `stale resolved no_response (YYYY-MM-DD)` to `notes`. Follow Step 4's rule: never restructure the CSV, preserve all other columns intact. 2. **Update Archive:** derive `documents/applications/_/` per the **Subfolder naming** rule. If the folder exists, update or write `outcome.md` with: - `**Status:** no_response` - `**Date resolved:** YYYY-MM-DD` - Append to `## Notes`: `- Stale resolution: marked no_response after [N] days quiet (YYYY-MM-DD)` **Calibration Handoff.** If 3 or more applications were resolved in this sweep, continue to Step 5 to offer calibration handoff. Otherwise present a summary of resolved applications and stop. --- ## Step 3: Archive the Application Materials Create or update `documents/applications/_/`. All content here is personal data - the folder is already gitignored (`documents/applications/**`), so nothing needs redacting. 1. **`cv_draft.tex` and `cover_letter.tex`** - copy (never move) the submitted files. Locate them via the tracker row's `cv_file`/`cover_letter_file` columns; if those are empty, look for `cv/main__.*` and `cover_letters/cover__.*`, deriving `_` by the **Subfolder naming** rule in `documents/README.md`. **Never widen those globs to the company alone** - two roles at one company both match it, and the first hit wins silently. If a file already exists in the archive, leave it - the archived version is what was actually submitted. If nothing matches (application made outside `/apply`), skip with a note rather than widening the search: a sibling role's CV recorded as what you submitted is worse than no file at all. 2. **`job_posting.md`** - if it already exists, leave it. Otherwise try WebFetch on the tracker row's `source` URL and save the posting text, retrying a 403 with browser headers per `.claude/skills/job-application-assistant/09-web-research.md`. If the URL is dead (postings expire fast - this is exactly why the archive matters), ask the user to paste the posting, or write a stub noting the posting is unavailable. **Never reconstruct a posting from memory.** 3. **`outcome.md`** - write or update it in exactly the format documented in `documents/README.md`, so `/setup` Path A parses it without special cases: ```markdown # Outcome: **Status:** in_progress | hired | offer_declined | rejected | no_response | interview_only **Date resolved:** YYYY-MM-DD <- only when resolved; omit while in_progress ## Interview stages reached - [x] Phone screen (YYYY-MM-DD) - [ ] Technical interview - [ ] Case interview - [ ] Final round - [ ] Offer received ## Notes ``` Update rules: tick stage checkboxes as they are reached (add the date in parentheses), append dated entries to Notes, and only change `Status` from `in_progress` to a final value on resolution. Re-running `/outcome` on the same application is idempotent - it appends new information, never duplicates or rewrites history. **Thank-you note trigger:** when this step ticks a newly completed interview stage, offer in the same turn: "Want a short thank-you note for the interviewer? A prompt one is standard practice." If accepted, draft it under Step 2b's drafting and logging rules (same voice, same no-new-claims boundary, same `followup_YYYY-MM-DD.md` archive convention). Recording the stage is the trigger - no scanning for recent stages is ever needed. --- ## Step 4: Update the Tracker Update the matched row's `status` column using the canonical spellings from **Tracker status vocabulary** above (e.g. `drafted` → `applied` → `interview` → `offer` → `hired` / `rejected` / `no_response` / `offer_declined` / `withdrawn`) and append a short dated note to the `notes` column. Never restructure the CSV, reorder rows, or touch other rows. The rewrite touches only the `status` and `notes` columns: preserve every other field of the row, parsed or not, so a value the row carries - the `deadline` written by `/apply` Step 6b, or any column added in the future - is never blanked by a status update. **Moving a row off `drafted`:** rows written by `/apply` Step 6b carry the date the documents were drafted, not the date they were sent. Whenever this step advances such a row to any other status - `applied`, or straight to `interview` or `rejected` when the user reports an outcome for something they submitted without recording it - overwrite its `date` column with the actual submission date. The `date` column is read as "applied on" by `/notion-sync` and drives `/html-report`'s year/season grouping and this command's own days-quiet count, so leaving the draft date in place would misreport the application. --- ## Step 5: Calibration Handoff Count the `outcome.md` files under `documents/applications/` with a **final** status (not `in_progress`). - If 3 or more are resolved (or 2+ share a pattern - same role type rejected twice, same sector going silent), suggest: > "You now have resolved applications on record. Run `/setup` (Path A) to fold them into your evaluation framework - it calibrates fit scoring from what actually got interviews, and mines your interview feedback for STAR examples." - Do **not** write anything into `04-job-evaluation.md` or other skill files yourself. `/setup` Path A owns that merge - it is read-before-write and idempotent, and duplicating its logic here would race it. --- ## Step 6: Confirm Summarize what was recorded: > **Outcome recorded for at .** > > - `documents/applications/_/outcome.md` - status: , > - Archived: > - Tracker: status → > > [Calibration suggestion from Step 5, if triggered] If the update recorded an upcoming or newly scheduled interview stage, also suggest: > "Interview coming up? `/interview ` builds a prep pack for that stage from this application's archive - the posting, the documents you submitted, and any feedback recorded from earlier rounds." If the recorded status is `hired`, congratulate the user warmly first - this is the moment the whole framework exists for. Then add this single line (once; never on re-runs for the same application, and never for any other status): > "If this framework helped you get there, consider [buying it a coffee](https://ko-fi.com/madslorentzen) - it keeps this free for the next job-seeker out there. ☕" --- ## Important Rules 1. **Write data, don't interpret it.** The archive and tracker are the outputs; calibration belongs to `/setup`. This command never edits profile or framework files. 2. **The archived version is the submitted version.** Existing files in the application folder are never overwritten by fresher drafts. 3. **Never fabricate.** A dead posting URL gets a user-pasted copy or an explicit "unavailable" stub, not a reconstruction. Feedback is recorded as the user reports it. 4. **Stay schema-compatible.** `outcome.md` follows the format in `documents/README.md` exactly (`in_progress` is the one addition, for open applications); the tracker keeps its columns. 5. **Idempotent updates.** Re-running on the same application appends new stages and notes; it never duplicates folders, rows, or history. 6. **Follow-ups: draft only, never send.** The follow-up branch produces text for the user to send themselves. It never emails, messages, or submits anything, and it must not be wired to tools that do. 7. **Follow-ups: no new claims.** Every substantive statement in a follow-up or thank-you note comes from the archived submitted materials. Rule 3 applies with no exceptions. 8. **Maximum two follow-ups per application.** After the second silent follow-up, the honest move is recording the resolution, not persistence. 9. **Stale sweep: user confirms before writing.** The stale sweep branch never marks applications as no_response automatically. It always presents the qualifying candidate list and waits for explicit user confirmation (all, select, or skip).