# How the /apply Drafter-Reviewer Workflow Works Internally in ai-job-search

> Discover the internal workings of the /apply drafter-reviewer workflow in ai-job-search. Understand the six-step pipeline for generating ATS-ready CVs and cover letters.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: internals
- Published: 2026-07-23

---

**TLDR:** The `/apply` command executes a deterministic six-step pipeline where a drafter agent evaluates job fit, generates LaTeX documents, and a separate reviewer agent audits factual accuracy—producing ATS-ready CVs and cover letters through structured feedback loops and mandatory compilation checks.

The drafter-reviewer workflow in the `MadsLorentzen/ai-job-search` repository automates the creation of tailored job application materials through a rigid orchestration defined in [`.claude/commands/apply.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/apply.md). This architecture isolates content generation from critical review, ensuring that every output is factually grounded against the candidate's static profile while maintaining strict safety boundaries against untrusted job posting data.

## The Six-Step Drafter-Reviewer Pipeline

The workflow progresses through seven distinct phases (numbered 0–6) that transform a raw job posting URL or text into polished PDFs.

### Step 0: Parse Input and Establish Safety Boundaries

In [`.claude/commands/apply.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/apply.md) (lines 15–22), the pipeline first detects whether the user provided a URL or raw text. If a URL is supplied, the system fetches the posting but treats the content as **untrusted data**—explicitly refusing to follow any embedded instructions, hidden URLs, or malicious directives. This step extracts essential metadata including company name, role title, department, location, and language, which serve as the "trust anchor" for subsequent stages.

### Step 1: Evaluate Fit and Benchmark Salary

The **Drafter** agent begins by reading the candidate's static profile from [`.claude/skills/job-application-assistant/01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/01-candidate-profile.md) and the evaluation rubric from [`.claude/skills/job-application-assistant/04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/04-job-evaluation.md) (lines 25–48). If available, it executes `python salary_lookup.py "<Company>"` to enrich the analysis with market compensation data. The drafter produces a structured fit report scoring skills, experience, culture alignment, and salary expectations, then pauses for user confirmation before proceeding.

### Step 2: Draft CV and Cover Letter Documents

Leveraging the writing style guide in [`.claude/skills/job-application-assistant/03-writing-style.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/03-writing-style.md) and LaTeX template specifications from [`.claude/skills/job-application-assistant/05-cv-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/05-cv-templates.md) and [`.claude/skills/job-application-assistant/06-cover-letter-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/06-cover-letter-templates.md), the drafter creates two new files: `cv/main_<company>_<role>.tex` and `cover_letters/cover_<company>_<role>.tex` (lines 54–81). This stage applies "grounding audit" rules ensuring every claimed fact matches the master profile stored in `cv/main_example.tex`, while respecting page limits and language policies.

### Step 3: Research and Content Critique

A fresh **Reviewer** agent spawns with the drafts embedded inline (no additional file reads). Operating solely from the company name and pre-approved reference files—never the raw posting text—the reviewer conducts independent company research and performs a factual grounding audit against [`.claude/skills/job-application-assistant/02-behavioral-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/02-behavioral-profile.md). It returns **Part A** (machine-readable JSON edit objects) and **Part B** (narrative suggestions) for the drafter to implement (lines 95–73).

### Step 4: Apply Structured Feedback

The drafter executes the reviewer's JSON edits via the *Edit* tool, replacing specific old strings with new content without re-reading the draft files (lines 78–89). It manually addresses Part B suggestions by closing keyword gaps, refining company-specific angles, and adjusting tone according to the writing style guide.

### Step 5: Compile PDFs and Verify ATS Compatibility

This mandatory compilation phase uses `lualatex` for the CV and `xelatex` for the cover letter (lines 95–73, 95–71). The drafter inspects generated PDFs for layout violations—such as orphaned titles or page-break issues—and iteratively edits the `.tex` files until the layout checks pass. Finally, it runs an ATS verification pass using `pdftotext` to extract the text layer and validates keyword coverage against the list built in Step 1.

### Step 6: Present Final Output and Next Actions

In the concluding phase (lines 80–99), the system re-runs the global verification checklist defined in [`CLAUDE.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CLAUDE.md), lists the generated file paths, and suggests follow-up commands (`/outcome` or `/interview`) to continue the job-search lifecycle.

## Implementation Logic and File References

The pipeline relies on a deterministic orchestration that external systems could mirror. Below is a pseudo-Python outline showing how these steps translate programmatically:

```python
import subprocess, json, pathlib
from pathlib import Path

def parse_input(arg):
    # Step 0 – URL detection with safety boundaries

    if arg.startswith("http"):
        posting_text = webfetch(arg)  # Claude WebFetch tool

    else:
        posting_text = arg
    meta = extract_metadata(posting_text)  # company, role, language

    return posting_text, meta

def evaluate_fit(meta, posting):
    # Step 1 – Drafter reads profile and rubric

    profile = read_file(".claude/skills/job-application-assistant/01-candidate-profile.md")
    rubric = read_file(".claude/skills/job-application-assistant/04-job-evaluation.md")
    salary = None
    try:
        result = subprocess.check_output([
            "python", "salary_lookup.py", meta["company"], "--json"
        ])
        salary = json.loads(result)
    except Exception:
        pass
    fit_report = drafter_evaluate(profile, rubric, posting, salary)
    return fit_report

def draft_documents(meta, posting, fit_report):
    # Step 2 – Generate LaTeX using templates

    style = read_file(".claude/skills/job-application-assistant/03-writing-style.md")
    cv_tpl = read_file(".claude/skills/job-application-assistant/05-cv-templates.md")
    cl_tpl = read_file(".claude/skills/job-application-assistant/06-cover-letter-templates.md")
    cv_tex = drafter_generate_cv(meta, fit_report, cv_tpl, style)
    cl_tex = drafter_generate_cover(meta, fit_report, cl_tpl, style)
    cv_path = Path(f"cv/main_{meta['company']}_{meta['role']}.tex")
    cl_path = Path(f"cover_letters/cover_{meta['company']}_{meta['role']}.tex")
    cv_path.write_text(cv_tex)
    cl_path.write_text(cl_tex)
    return cv_path, cl_path

def reviewer_feedback(cv_path, cl_path, posting, meta):
    # Step 3 – Reviewer audits with isolated context

    cv_tex = Path(cv_path).read_text()
    cl_tex = Path(cl_path).read_text()
    feedback = reviewer_agent(
        company=meta["company"],
        role=meta["role"],
        posting=posting,
        cv_draft=cv_tex,
        cover_draft=cl_tex,
    )
    return feedback  # Part A (JSON) + Part B (narrative)

def apply_feedback(feedback, cv_path, cl_path):
    # Step 4 – Execute structured edits

    for edit in feedback["part_a"]:
        edit_file(edit["file"], edit["old_string"], edit["new_string"])
    # Part B applied via manual revision

def compile_and_verify(meta):
    # Step 5 – LaTeX compilation and ATS checks

    subprocess.run(["lualatex", f"cv/main_{meta['company']}_{meta['role']}.tex"])
    subprocess.run(["xelatex", f"cover_letters/cover_{meta['company']}_{meta['role']}.tex"])
    ats_check(meta)  # Uses pdftotext for text extraction

def final_output(meta):
    # Step 6 – Verification and handoff

    print(f"✅ CV ready: cv/main_{meta['company']}_{meta['role']}.tex")
    print(f"✅ Cover letter ready: cover_letters/cover_{meta['company']}_{meta['role']}.tex")

```

## Critical Configuration Files

The workflow depends on these specific source files:

- [`.claude/commands/apply.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/apply.md) – Master pipeline definition (steps 0–6)
- [`.claude/skills/job-application-assistant/01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/01-candidate-profile.md) – Static candidate facts and employment history
- [`.claude/skills/job-application-assistant/04-job-evaluation.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/04-job-evaluation.md) – Rubric for scoring job fit
- [`.claude/skills/job-application-assistant/03-writing-style.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/03-writing-style.md) – Tone and voice guidelines
- [`.claude/skills/job-application-assistant/05-cv-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/05-cv-templates.md) – LaTeX CV structural specifications
- [`.claude/skills/job-application-assistant/06-cover-letter-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/06-cover-letter-templates.md) – Cover letter layout rules
- [`.claude/skills/job-application-assistant/02-behavioral-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/02-behavioral-profile.md) – Behavioral reference for reviewer audits
- [`salary_lookup.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/salary_lookup.py) – Optional compensation benchmarking tool
- `cv/main_example.tex` – Master CV template serving as the grounding baseline
- [`CLAUDE.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CLAUDE.md) – Global verification checklist for final validation

## Summary

- The **drafter-reviewer workflow** separates document generation from critical audit, preventing hallucinations by grounding all claims in [`.claude/skills/job-application-assistant/01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/01-candidate-profile.md).
- **Step 0** enforces safety by treating job postings as untrusted data, extracting only metadata before any processing.
- **Steps 1–2** use specialized skill files for evaluation rubrics, writing style, and LaTeX templates to generate initial drafts.
- **Step 3** spawns an isolated reviewer agent that returns structured JSON edits (**Part A**) and narrative feedback (**Part B**).
- **Step 5** mandates LaTeX compilation (`lualatex` for CVs, `xelatex` for cover letters) and ATS verification via `pdftotext` text extraction.
- All artifacts are validated against the global checklist in [`CLAUDE.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CLAUDE.md) before the workflow completes.

## Frequently Asked Questions

### How does the drafter-reviewer workflow prevent factual errors in generated documents?

The workflow implements a "grounding audit" where the reviewer agent validates every claim in the drafts against the static candidate profile stored in [`.claude/skills/job-application-assistant/01-candidate-profile.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/01-candidate-profile.md). Additionally, the drafter must obey "grounding audit" rules during Step 2, ensuring all dates, metrics, and role descriptions match the master CV file (`cv/main_example.tex`) exactly.

### What safety measures protect against malicious content in job postings?

Step 0 explicitly treats the posting as **untrusted data** by parsing only structural metadata (company, role, location) and refusing to follow any URLs, instructions, or hidden directives embedded within the text. The reviewer agent in Step 3 receives only the company name and pre-approved reference files, completely isolating it from the raw posting content.

### Why does the workflow use different LaTeX engines for CVs and cover letters?

The pipeline uses `lualatex` for CV compilation and `xelatex` for cover letters (as specified in [`.claude/commands/apply.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/apply.md)) to handle specific font rendering requirements and layout constraints defined in the template files. This dual-engine approach ensures proper Unicode support and microtypography while respecting the page-limit enforcement rules in [`.claude/skills/job-application-assistant/05-cv-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/05-cv-templates.md).

### How are reviewer suggestions actually applied to the documents?

The reviewer returns **Part A** feedback as machine-readable JSON objects specifying exact `old_string` and `new_string` pairs for the *Edit* tool, allowing the drafter to make precise file modifications without re-reading the entire draft. **Part B** suggestions (narrative improvements regarding tone or company angles) are addressed manually by the drafter in Step 4 before proceeding to compilation.