How `followup-seed.mjs` Pins the First Follow-Up Date When a Row Becomes Applied in `career-ops`

followup-seed.mjs automatically creates a pin directive in data/follow-ups.md the moment a tracker entry's status changes to Applied, recording the next follow-up date and the date the pin was written.

When a job application in career-ops transitions to Applied, the follow-up system needs to know when to remind you next. Rather than requiring manual entry, followup-seed.mjs handles this automatically by generating a pin—a specially formatted line that the cadence engine later reads to determine reminder timing. This article explains the exact mechanism, from path resolution to atomic file writing, as implemented in the santifer/career-ops repository.

Locating Tracker and Follow-Up Files

The script begins by resolving two critical paths. resolveTrackerPath and resolveFollowupsPath check for environment overrides first, then fall back to sensible defaults (data/applications.md and data/follow-ups.md respectively).

Source: resolveTrackerPath, resolveFollowupsPath.

Reading and Validating the Target Row

Once paths are established, readTrackerRows parses the entire tracker file into structured objects. The script selects the row matching the provided appNum and immediately validates its status.

Source: readTrackerRows.

Status Validation

The normalizeStatus function standardizes status strings for comparison. Unless --force is passed, the script aborts if the row isn't Applied—preventing accidental pins for Wishlist or Interviewing entries.

Source: normalizeStatus check.

Determining the Applied Date

The applied date anchors all follow-up calculations. resolveAppliedDate implements a three-tier fallback:

  1. Explicit --date flag — highest priority, user-supplied value
  2. "Applied YYYY-MM-DD" note — parsed from the row's notes column
  3. Tracker date column — the original entry date
  4. Today — final fallback via localToday

Source: resolveAppliedDate.

Loading Cadence Configuration and Computing Next Date

The resolveCadenceConfig function (imported from followup-cadence.mjs) reads config/profile.yml and extracts the applied_first offset—the number of days after application before the first follow-up.

The calculation is straightforward:

const nextDate = addDays(parseDate(appliedDate), cadence.applied_first);

Source: addDays usage, resolveCadenceConfig.

Creating the Pin Line

The formatPinLine helper renders a machine-readable directive that the cadence parser recognizes:

- next #<appNum> <nextDate> (set <setDate>)

This format encodes: the follow-up type (next), application number, scheduled follow-up date, and when the pin was created.

Source: formatPinLine.

Idempotency and Concurrency Protection

Preventing Duplicate Pins

Before writing, isAlreadySeeded checks follow-ups.md for existing pins or table rows matching appNum. If found and --force isn't set, the script exits with seeded: false, avoiding duplicate entries.

Source: isAlreadySeeded.

Cross-Process Locking

withFollowupsLock (using acquireFollowupsLock from pipeline-lock.mjs) ensures only one process can modify follow-ups.md at a time. This prevents race conditions when multiple CLI invocations or automation scripts run concurrently.

Source: withFollowupsLock.

Atomic File Writing

The pin is appended using writeFileAtomic, which:

  • Creates a temporary file adjacent to the target
  • Writes the complete updated content (including FOLLOWUPS_HEADER for new files)
  • Renames the temp file over the original

This guarantees an all-or-nothing update—even if the process crashes, follow-ups.md never contains partial data.

Source: writeFileAtomic, appendPins.

Usage Examples

Programmatic API

import { seedFollowup } from './followup-seed.mjs';

(async () => {
  const result = await seedFollowup(42, {
    // date: '2024-09-01',  // optional explicit date
    force: false,           // abort if not Applied
    dryRun: false,          // actually write the pin
  });

  console.log(result);
  // {
  //   seeded: true,
  //   appNum: 42,
  //   pin: '- next #42 2024-09-15 (set 2024-09-02)',
  //   nextDate: '2024-09-15',
  //   appliedDate: '2024-09-01',
  //   appDateSource: 'explicit',
  //   setDate: '2024-09-02'
  // }
})();

CLI Invocation


# Pin with explicit apply date

node followup-seed.mjs 42 --date 2024-09-01

# Output:

# ✅ Seeded #42: next follow-up 2024-09-15 (applied 2024-09-01, set 2024-09-02)

Bulk Backfill

import { seedBackfill } from './followup-seed.mjs';

(async () => {
  const { seeded, skipped } = await seedBackfill({ dryRun: true });
  console.log(`Would seed ${seeded.length}, skip ${skipped.length} already pinned`);
})();

Key Files in the Follow-Up System

File Purpose
followup-seed.mjs Core script for creating pin directives when rows become Applied
followup-cadence.mjs Cadence configuration, date arithmetic (addDays), and scheduling logic
tracker-parse.mjs Parses data/applications.md into structured row objects
lib/local-today.mjs Provides current date for set timestamps
pipeline-lock.mjs Filesystem locking primitives for safe concurrent access

Summary

  • followup-seed.mjs automates first follow-up creation by generating pins in data/follow-ups.md when tracker rows reach Applied status.
  • Date resolution follows a strict priority: explicit flag → "Applied YYYY-MM-DD" note → tracker date column → today.
  • Cadence configuration from config/profile.yml determines the applied_first offset added to the resolved date.
  • Idempotency via isAlreadySeeded prevents duplicate pins without --force.
  • Concurrency safety through withFollowupsLock and atomic writes protects file integrity under parallel execution.

Frequently Asked Questions

What happens if I run followup-seed.mjs on a non-Applied row?

Without --force, the script aborts after normalizeStatus detects the mismatch. With --force, it proceeds regardless of status—useful for edge cases or testing.

Can I override the automatically detected applied date?

Yes. Pass --date YYYY-MM-DD to set it explicitly. Otherwise, the script searches your notes for "Applied YYYY-MM-DD", falls back to the tracker's date column, and finally uses today's date.

How does the script prevent duplicate pins?

isAlreadySeeded scans follow-ups.md for existing pins matching the appNum. It also checks for table rows (legacy format). If either exists, the script exits with seeded: false unless you pass --force.

What file locking mechanism does career-ops use?

withFollowupsLock leverages acquireFollowupsLock from pipeline-lock.mjs, which implements robust filesystem-based locking. This ensures only one process modifies follow-ups.md at a time, preventing corruption during concurrent writes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →