Spec #9 — Follow-up Cadence Engine
Status: Done (shipped 2026-06-15) · Owner: Hust · Effort: M · Phase 2 · Depends on: #2 (stage source), #6 (caps)
1. Problem & user value
Timely, polite follow-ups materially improve response rates — but seekers forget, and over-nudging is spammy. A cadence engine computes per-application urgency from stage + elapsed time and surfaces nudges (in-app badges + optional email), capped to stay tasteful.
2. Scope
In: pure urgency functions over applications stage/timestamps; badges (urgent / overdue /
waiting / cold) on the Kanban; optional email nudges via the existing Resend + Trigger.dev
cron; caps from #6 (e.g. max 2 follow-ups per application).
Out: drafting the follow-up message body (reuses the generation tooling); outreach to new contacts (#17).
3. Design
- Cadence policy (config): e.g.
applied_first: 7d,interview_thankyou: 1d,max: 2, cold-after: 21d.computeUrgency(application, now) → urgent | overdue | waiting | cold | done. Pure + unit-tested;nowinjected (noDate.now()in the pure layer). - Surfacing: badges on Kanban cards + an "action queue" on the dashboard. Email nudges via a
Trigger.dev scheduled task (reuses
send-alertsinfra) honoring caps + quiet preferences. - Caps + opt-out enforced via #6's
followUpPolicy; never exceed the cap, always user-dismissable.
4. Data / API
applications: optionallastFollowUpAt,followUpCount. Afollow_upslog table (optional) or derive from stage history. New Trigger.dev taskfollow-up-nudges.
5. Plan & tasks
- Pure
computeUrgency+ cadence policy (unit-tested,nowinjected). - Badges + dashboard action queue (reads urgency).
follow-up-nudgesTrigger.dev task (Resend), capped + preference-aware.applications.lastFollowUpAt/followUpCount(migration) or follow_ups log.- Tests: urgency transitions, cap enforcement, E2E badge appears for an overdue fixture.
6. Acceptance
- An application past
applied_firstshows "overdue"; nudges respect the cap + opt-out; urgency is pure + unit-tested; CI green; zero competitor references.
Implementation (shipped)
Shipped 2026-06-15. The cadence engine + caps + AI surfacing landed; the email/Trigger.dev nudge task and visual Kanban badges are intentionally deferred (see "Deferred" below).
- Pure cadence engine —
packages/ai/src/cadence/follow-ups.ts:computeFollowUpSuggestions(apps, now, policy)withFOLLOWABLE_STAGES(applied/screening/interviewing);nowis injected (noDate.now()in the pure layer). Anchors onlastFollowUpAt ?? stageChangedAtand sorts by staleness. - Cap policy (from #6) —
packages/ai/src/policy/follow-up-policy.ts:canSendFollowUp()+DEFAULT_FOLLOW_UP_POLICY(maxFollowUps: 3,minIntervalDays: 3); reasonsok/max_reached/too_soon. - AI tool — suggestions —
packages/ai/src/tools/follow-up-suggestions.ts(followUpSuggestionsTool, registered as thefollowUpSuggestionstool): reads the signed-in user's active applications (userIdinjected server-side) and returns the ones due, capped. - AI tool — record —
packages/ai/src/tools/record-follow-up.ts(recordFollowUpTool, registered asrecordFollowUp): incrementsfollowUpCount+ setslastFollowUpAton the user's own application so future nudges respect the cap. No approval gate (sending stays manual / HITL). - Orchestrator wiring —
packages/ai/src/agents/orchestrator.tsregisters both tools (withuserIdinjected); exported viapackages/ai/src/index.tsandpackages/ai/src/tools/index.ts; documented in the system prompt (packages/ai/src/prompts.ts). - DB columns —
packages/db/src/schema/applications.ts:followUpCount(follow_up_count,integer NOT NULL DEFAULT 0) andlastFollowUpAt(last_follow_up_at,timestamp). Migration:packages/db/drizzle/0001_magical_meggan.sql. - Tests —
packages/ai/src/cadence/follow-ups.test.ts(urgency/anchor/cap/stage-skip transitions,nowinjected) andpackages/ai/src/tools/follow-up-tools.test.ts(auth gating +applicationIdvalidation).
Shipped follow-on (visual surface)
- Per-row follow-up badge —
followUpUrgency(app, now, policy)(packages/ai/src/cadence/follow-ups.ts, exported via the@ever-hust/ai/cadence/follow-upssubpath) classifies each application asoverdue/due/waiting/capped/none(anchored onlastFollowUpAt ?? stageChangedAt, honouring the #6 cap)./api/user/applicationscomputes it server-side per row, andapps/web/app/(dashboard)/applications/page.tsxrenders a "Follow up" / "Due" badge for the actionable states. Unit-tested infollow-ups.test.ts; covered bytests/e2e/authed/account.authed.spec.ts.
Shipped follow-on (email nudge)
follow-up-nudgesTrigger.dev task (shipped) —packages/triggers/src/follow-up-nudges.ts(processFollowUpNudges, daily 09:00 UTC) finds each user's due follow-ups (the pure cadence engine + #6 caps) and emails a capped, polite digest via Resend (FollowUpNudgeEmailtemplate +sendFollowUpNudgeEmail). Opt-out viapreferences.followUpNudges === false; a 3-day cooldown uses the newusers.last_follow_up_nudge_atcolumn (migration0003). The nudge is a reminder — it never incrementsapplications.followUpCount. (Trigger.dev project must be deployed to schedule it.)
Deferred (not in this ship)
- Dashboard action queue — the dedicated cross-application "what needs a nudge today" queue view is still deferred; the per-row badge + the email digest cover the taxonomy instead.
follow_upslog table — not added; follow-up state is derived from theapplications.followUpCount/lastFollowUpAtcolumns as the spec allowed.