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:
- Explicit
--dateflag — highest priority, user-supplied value - "Applied YYYY-MM-DD" note — parsed from the row's notes column
- Tracker
datecolumn — the original entry date - 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_HEADERfor 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.mjsautomates first follow-up creation by generating pins indata/follow-ups.mdwhen tracker rows reach Applied status.- Date resolution follows a strict priority: explicit flag → "Applied YYYY-MM-DD" note → tracker
datecolumn → today. - Cadence configuration from
config/profile.ymldetermines theapplied_firstoffset added to the resolved date. - Idempotency via
isAlreadySeededprevents duplicate pins without--force. - Concurrency safety through
withFollowupsLockand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →