# How CareerOps Manages the Job Application Tracker Using `data/applications.md`

> Learn how CareerOps uses data/applications.md as a single source of truth for job applications. Discover its CLI API for status management and data integrity.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: how-to-guide
- Published: 2026-08-22

---

**CareerOps treats [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) as the single source of truth for your job search, exposing a CLI-driven API that enforces canonical status values, atomic row updates, and file-level locking to prevent manual edits and data corruption.**

The CareerOps repository implements a disciplined, script-mediated workflow to maintain its **application tracker**. Rather than editing [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) directly, you interact with the file through specialized Node.js modules that guarantee consistency, traceability, and safe concurrent access across all write operations.

## Single Source of Truth Architecture

### The Canonical Schema in [`templates/states.yml`](https://github.com/santifer/career-ops/blob/main/templates/states.yml)

All status values in the application tracker are strictly defined in [`templates/states.yml`](https://github.com/santifer/career-ops/blob/main/templates/states.yml). This file acts as the schema authority, ensuring that every status cell—whether "Applied," "Evaluated," or "Offer Received"—uses a normalized string. When scripts like `set-status.mjs` process updates, they validate incoming values against this canonical list before writing to disk, preventing typographical drift and maintaining filterability.

### The Master File at [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md)

The markdown file at [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) serves as the atomic database for your entire job search history. It stores data in a header-aware table format that `tracker-parse.mjs` parses into structured rows. The system maintains **original line numbers** during rewrites to preserve `git blame` history, ensuring you can trace any status change back to the exact commit and CLI command that triggered it.

## The Write API: CLI Tools for Atomic Updates

### Updating Row Status with `set-status.mjs`

When you need to progress a job application through your pipeline, use `set-status.mjs` to modify a specific row. This tool loads [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md), locates the target row by ID, normalizes the status against [`templates/states.yml`](https://github.com/santifer/career-ops/blob/main/templates/states.yml), and executes an atomic rewrite.

```bash
node set-status.mjs 42 Applied --note "Sent application"

```

Behind the scenes, `set-status.mjs` imports `tracker-utils.mjs` and invokes `updateRow()`, which handles CSV-style quoting, column alignment, and cross-platform line endings before acquiring the write lock.

### Batch Importing with `merge-tracker.mjs` and `add-entry.mjs`

Adding new applications never requires touching [`applications.md`](https://github.com/santifer/career-ops/blob/main/applications.md) directly. Instead, generate a TSV file in `batch/tracker-additions/` and run the batch importer:

```bash
node add-entry.mjs \
  --company "Acme Corp" \
  --role "Senior ML Engineer" \
  --date "2024-07-15" \
  --status "Evaluated" \
  --score "—"

```

The `add-entry.mjs` wrapper generates the TSV and delegates to `merge-tracker.mjs`, which validates column order (ensuring **score appears before status**), checks for duplicate rows, and injects the new entry while maintaining the table's structural integrity.

```bash

# Process all pending TSV additions

node merge-tracker.mjs

```

## Safety Mechanisms and File Locking

### Atomic Writes via `tracker-utils.mjs`

The `tracker-utils.mjs` module provides the low-level `updateRow()` and `insertRow()` functions that mediate all disk access. These utilities handle the complexity of CSV-style quoting within markdown tables, ensure column alignment across rows, and manage temporary file creation for atomic write operations. By buffering changes to a temporary file before renaming, the system prevents half-written states that could corrupt the tracker during a process crash.

### Concurrent Write Protection with `tracker-writer-lock.mjs`

To prevent race conditions when multiple CLI commands execute simultaneously, CareerOps implements `tracker-writer-lock.mjs`. This lock file mechanism ensures that only one process can write to [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) at any given moment. If you trigger `set-status.mjs` while `merge-tracker.mjs` is already running, the second command waits for the lock to release, guaranteeing that row IDs and line numbers remain consistent.

## Read Operations and Data Integrity

### Querying with `tracker.mjs`

For UI components and reporting scripts that need to inspect the tracker without modifying it, `tracker.mjs` provides a read-only interface. This module exposes the parsed table structure to the rest of the application while enforcing the **write-only via CLI helpers** architectural rule, ensuring that no UI bug can accidentally alter historical application data.

### Cross-Validation via `tracker-sync-check.mjs`

The `tracker-sync-check.mjs` script enforces consistency between [`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md) and [`data/active-interviews.md`](https://github.com/santifer/career-ops/blob/main/data/active-interviews.md). It cross-references the *Status* column against the interview log, flags drift (such as an application marked "Interviewing" in the tracker but missing from the active interviews file), and suggests corrective actions. Crucially, this tool operates in a **read-only** mode; it never writes to the tracker file, preserving the separation between validation and modification concerns.

## Practical Workflow Examples

Combine these tools into a reproducible, version-controlled pipeline:

```bash

# 1. Create a new tracker row via the batch helper

node add-entry.mjs \
  --company "Acme Corp" \
  --role "Senior ML Engineer" \
  --date "2024-07-15" \
  --status "Evaluated" \
  --score "—"

# 2. Change the status of row #42

node set-status.mjs 42 Applied --note "Application sent on 2024-07-16"

# 3. Merge a batch of TSV additions (e.g., after scanning resumes)

node merge-tracker.mjs

# 4. Verify consistency with the interview log

node tracker-sync-check.mjs

```

Because every write flows through `tracker-utils.mjs` and respects the locking protocol, the application tracker remains tidy, sortable (by date, company, or status), and safely merge-able via Git without conflict.

## Summary

- **[`data/applications.md`](https://github.com/santifer/career-ops/blob/main/data/applications.md)** serves as the immutable source of truth, parsed by `tracker-parse.mjs` and never edited by hand.
- **[`templates/states.yml`](https://github.com/santifer/career-ops/blob/main/templates/states.yml)** defines canonical status strings to prevent data drift and ensure consistent filtering.
- **`set-status.mjs`** and **`merge-tracker.mjs`** provide the sole write pathways, both utilizing `tracker-utils.mjs` for atomic, locked updates.
- **`tracker-writer-lock.mjs`** prevents concurrent write conflicts, maintaining row ID stability and `git blame` traceability.
- **`tracker-sync-check.mjs`** validates consistency with [`data/active-interviews.md`](https://github.com/santifer/career-ops/blob/main/data/active-interviews.md) without modifying the tracker.

## Frequently Asked Questions

### Can I edit [`applications.md`](https://github.com/santifer/career-ops/blob/main/applications.md) manually in a text editor?

No. The system explicitly prohibits manual edits to prevent CSV-style quoting errors, column misalignment, and status typos. Always use `set-status.mjs` for updates or `add-entry.mjs` for new rows. The `tracker-writer-lock.mjs` mechanism assumes exclusive script access; manual edits could cause data corruption or merge conflicts.

### What happens if two CLI commands run simultaneously?

The second command blocks until `tracker-writer-lock.mjs` releases the exclusive lock. This sequential execution ensures that `updateRow()` operations in `tracker-utils.mjs` do not overwrite each other, preventing duplicate row IDs or truncated file writes.

### How does the system prevent invalid status values?

All write operations validate the status parameter against the canonical list defined in [`templates/states.yml`](https://github.com/santifer/career-ops/blob/main/templates/states.yml) before persisting changes. If you attempt to set a status like "In-Progress" when the canonical value is "Applied," the script exits with an error, enforcing schema consistency across the application tracker.

### How do I add multiple applications at once?

Generate TSV files in the `batch/tracker-additions/` directory, then run `node merge-tracker.mjs`. The script validates the TSV structure (ensuring score precedes status), checks for duplicates, and atomically inserts all rows while maintaining the original line numbering of existing entries.