How the AI Job Search /apply Command Parses Job Posting Input
The /apply command transforms raw job posting text into structured application records through a six-step pipeline that validates input, archives the raw text, extracts company and role via regex fallbacks, detects URLs and deadlines, and writes a normalized CSV row to documents/applications/.
The AI Job Search repository (MadsLorentzen/ai-job-search) provides a Claude Code command suite for managing job applications through structured markdown and CSV workflows. The /apply command serves as the primary entry point for ingesting job postings, implementing a deterministic parsing strategy specified in .claude/commands/apply.md that extracts only essential metadata while preserving the original posting for downstream workflows.
The Six-Step Parsing Pipeline
The /apply command processes job posting input through a sequential pipeline documented in the command specification and validated by the test suite in tests/test_apply_records_application.py. Each step transforms unstructured text into structured fields while maintaining data integrity.
Step 0: Input Validation and Archiving
The command expects a single string argument containing the complete job posting. It first strips surrounding whitespace and fails fast if the resulting string is empty, enforcing the validation rules defined in .claude/commands/apply.md Step 0.
Immediately after validation, the full posting text is written to documents/postings/<company>_<role>/job_posting.md. This archive serves as the source of truth for subsequent commands such as /outcome and /interview, ensuring downstream steps can reference the original posting without re-fetching external URLs.
Step 1: Company and Role Extraction
The parser identifies the title line containing the company name and role title using an ordered list of regular-expression fallbacks. The implementation in the internal helper parse_posting_title()—which shares regex utilities with tools/security_guards.py—attempts multiple patterns until one matches:
Company – Role(em-dash or hyphen separated)Job Title – Company(reversed order with dash delimiter)- HTML
<h1>tags
The first successful capture wins, making the parser tolerant to variations across different job-portal layouts without requiring strict positional parsing.
Step 2: URL and Deadline Detection
If the posting contains hyperlinks, the parser searches for the first URL matching known job-portal domains and stores it as posting_url. This URL enables re-fetching capabilities if the archived copy needs validation against the live posting.
For deadlines, the parser executes case-insensitive regexes such as Application deadline[:\s]+(?P<date>.+) and Close date[:\s]+(?P<date>.+). When found, dates are normalized to ISO-8601 format (YYYY-MM-DD). If no deadline is present, the field remains blank and later salary negotiation steps skip deadline-based filtering, as asserted in tests/test_apply_records_application.py.
Step 3: Record Assembly and Context Propagation
All extracted values—company, role, posting URL, deadline, and the archive path—are assembled into a 14-column CSV row matching the header defined in apply.md. The row is appended to documents/applications/<company>_<role>/apply.md.
The structured data is then stored in the environment context (apply_context), allowing subsequent commands like /outcome and /interview to reference parsed fields without re-parsing the raw text. This context sharing is validated in tests/test_company_research_cache.py and tests/test_outcome_followup.py.
Implementation Architecture
Regex Pipeline Strategy
The parser implements a regex fallback pattern where an ordered list of patterns executes sequentially until one matches. This approach, shared across tools/security_guards.py, ensures robustness against varying job board formats without requiring strict schema adherence. The pattern list prioritizes common delimiters (em-dash, hyphen, en-dash) and handles both standard and reversed title formats.
Security Guards and Path Sanitization
Before writing to the filesystem, the command validates that the posting text does not contain relative path components (..) that could escape the intended directory structure. This enforces the trust-boundary rule specified in apply.md Step 0, preventing directory traversal attacks through malicious input.
Data Normalization Rules
Company names undergo aggressive normalization: legal suffixes (A/S, ApS, Inc., LLC) and extraneous whitespace are stripped to ensure consistent folder naming under documents/applications/. This guarantees that the same company maps to a single canonical path regardless of formatting variations in the source posting, preventing duplicate application folders for the same employer.
Complete Parsing Implementation
Below is a minimal recreation of the core parsing logic based on the actual implementation in tools/security_guards.py and the internal helper modules. This demonstrates how raw posting text converts to structured fields:
import re
from datetime import datetime
from pathlib import Path
def clean_posting(text: str) -> str:
"""Strip whitespace per apply.md Step 0 validation."""
return text.strip()
_TITLE_PATTERNS = [
re.compile(r"^(?P<company>.+?)\s*[–-]\s*(?P<role>.+)$"),
re.compile(r"^(?P<role>.+?)\s*[–-]\s*(?P<company>.+)$"),
]
def parse_title(line: str):
"""Extract company/role using regex fallbacks."""
for pat in _TITLE_PATTERNS:
m = pat.match(line)
if m:
return m.group("company").strip(), m.group("role").strip()
raise ValueError("Unable to parse company/role from title line")
_URL_RE = re.compile(r"https?://[^\s)]+")
def find_url(text: str) -> str | None:
"""Detect first URL in posting."""
m = _URL_RE.search(text)
return m.group(0) if m else None
_DEADLINE_RE = re.compile(
r"(?:application|close)\s*deadline[:\s]+(?P<date>[\w,\s]+)", re.I
)
def parse_deadline(text: str) -> str | None:
"""Normalize deadline to ISO-8601 or return None."""
m = _DEADLINE_RE.search(text)
if not m:
return None
raw = m.group("date").strip()
for fmt in ("%d %B %Y", "%Y-%m-%d", "%B %d, %Y"):
try:
return datetime.strptime(raw, fmt).date().isoformat()
except ValueError:
continue
return raw
def build_apply_record(posting: str) -> dict:
"""Complete pipeline from raw text to structured record."""
clean = clean_posting(posting)
lines = clean.splitlines()
# Extract metadata
company, role = parse_title(lines[0])
url = find_url(clean)
deadline = parse_deadline(clean)
# Archive raw posting
archive_dir = Path("documents/postings") / f"{company}_{role}"
archive_dir.mkdir(parents=True, exist_ok=True)
archive_path = archive_dir / "job_posting.md"
archive_path.write_text(clean, encoding="utf-8")
return {
"company": company,
"role": role,
"posting_url": url or "",
"deadline": deadline or "",
"archive_path": str(archive_path),
}
This implementation mirrors the actual parsing sequence validated in tests/test_apply_records_application.py, demonstrating the lightweight extraction strategy that preserves the original posting while enabling structured downstream processing.
Summary
- The
/applycommand implements a six-step parsing pipeline defined in.claude/commands/apply.mdthat converts raw job postings into structured CSV records. - Regex fallbacks in
parse_posting_title()handle multiple title formats, whiletools/security_guards.pyprovides shared validation utilities. - Raw postings are immediately archived to
documents/postings/before parsing, ensuring reproducibility for commands like/outcomeand/interview. - The parser normalizes company names and validates paths to prevent directory traversal, enforcing security boundaries specified in the command documentation.
- Parsed data propagates through the
apply_contextenvironment variable, enabling subsequent workflow steps to access structured fields without re-parsing.
Frequently Asked Questions
What happens if the job posting doesn't match the expected title format?
The parser uses a regex fallback strategy defined in parse_posting_title() that attempts multiple patterns sequentially. If standard Company – Role formats fail, it tries reversed ordering or HTML tag extraction. Only if all patterns fail does the command raise a validation error, ensuring robust handling of various job board layouts.
Where does the /apply command store the original job posting text?
The command archives the complete raw text to documents/postings/<company>_<role>/job_posting.md immediately after validation, as specified in .claude/commands/apply.md. This archived copy serves as the authoritative source for downstream commands and prevents data loss if the original URL becomes unavailable.
How does the parser handle different date formats for application deadlines?
The deadline detection uses case-insensitive regexes to locate dates, then attempts parsing against multiple common formats including %d %B %Y, %Y-%m-%d, and %B %d, %Y. Successfully parsed dates are normalized to ISO-8601 (YYYY-MM-DD). If parsing fails or no deadline is found, the field remains blank, which triggers different logic in subsequent salary negotiation steps.
What prevents malicious input from accessing files outside the project directory?
The command implements trust-boundary validation that scans input for relative path components (..). If detected, the command refuses to execute, preventing directory traversal attacks. This security guard is documented in apply.md Step 0 and implemented in the validation layer shared with tools/security_guards.py.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →