docs/solutions/integration-issues/merged-is-not-ran-long-cron-seeders.md
defensePatents sat at EMPTY (crit) on /api/health with records: 0, and
production overall was DEGRADED — after the USPTO ODP migration (#5284) had
merged and deployed. The seeder code was correct. It had simply never run.
The same shape appeared twice in one session, so it is a pattern, not an incident.
/api/health?compact=1 shows the domain as EMPTY, records: 0, and no
seedAgeMin — the seed-meta key is absent entirely, not merely stale.scripts/_defense-patents-source.mjs,
never the seeder entry point) returned 90 valid records and passed the
seeder's own validateDefensePatents. The code was fine the whole time.SUCCESS deployments. Nearly all of them were buildOnly: true —
image builds triggered by pushes to main, which run nothing. In the last
100 deployments there was exactly one real run against 82 builds. A busy
deployment list is not evidence that a cron ever fired.Check the seeder's cron cadence against the fix's merge time before concluding anything:
# The cron schedule lives in Railway service config, not the repo.
# seed-bundle-static-ref: "0 3 * * 0" => Sunday 03:00 UTC, WEEKLY.
railway status --json # then query the deployment's serviceManifest.deploy.cronSchedule
The timeline that explained everything:
| when | what |
|---|---|
| Sun 2026-07-12 03:00 UTC | last cron tick — before the fix existed |
| Mon 2026-07-13 18:07 UTC | #5284 merged and the image rebuilt |
| Sun 2026-07-19 03:00 UTC | next cron tick — 5 days away |
The fix was baked into the image and had never executed. The remedy is a Railway-side manual backfill, which also verifies the fix on the production network (publish, envelope dual-write, Redis verification) rather than only in a local probe.
Do not use railway run for this acceptance step. Railway documents that
command as fetching service variables and executing the command locally.
It can therefore pass or fail on the operator's network while proving nothing
about the seeder's production egress.
Until the project has a dedicated one-off runner, use a controlled temporary cron execution:
deploy.startCommand and
deploy.cronSchedule.Active; Railway skips overlapping cron runs.node scripts/audit-railway-watch-paths.mjs to prove registry convergence.See Railway's official cron job and
railway run documentation for the network
and scheduling semantics.
/api/health cleared on the next poll.
A Railway cron seeder has two independent lifecycles that are easy to conflate:
main (buildOnly: true deployments).
This is what makes the service look active.Merging a fix advances (1) immediately and (2) not at all. For an hourly seeder the gap is invisible. For a weekly seeder it is up to 7 days — long enough that the fix looks broken.
This is compounded by the fact that a seed-meta key does not resurrect itself:
nothing re-seeds an absent key except a run of the seeder that owns it.
Merged ≠ deployed ≠ ran. The only ground truth for seeder data is the
seed-meta key in Redis, not the PR state, not CI, and not the Railway badge.
A corollary worth internalizing: shipping a health-key registration in the same PR as its seeder makes health go crit for exactly one cron period. The health endpoint starts grading a key before the seeder that populates it has ever run. Observed twice on the same day:
defensePatents — weekly cron → would have stayed crit for ~5 days.chinaMacro / chinaReleaseCalendar (#5294) — daily cron (0 8 * * *) →
crit for ~10 hours, then self-healed with no intervention.The registration is at api/health.js:211 (data key) and api/health.js:403
(seed-meta key); the weekly section is scripts/seed-bundle-static-ref.mjs:6
(intervalMs: WEEK).
scripts/seed-freshness-baseline.json with an
entry-level expiresAt bounded to the first scheduled cron window. The latter
is only a temporary rollout bridge: at or after its expiry the still-live
problem blocks again even if the baseline's root expiry is later. Never use an
unbounded acknowledgement to suppress a cutover fault.railway run as production-network evidence. It injects
Railway variables into a local process. Use an actual scheduled execution or
a dedicated Railway-side one-off runner.railway status --json → serviceManifest.deploy.cronSchedule.SUCCESS deployments as runs. Filter on meta.buildOnly —
buildOnly: true deployments build the image and execute nothing.scripts/_defense-patents-source.mjs, not
scripts/seed-defense-patents.mjs). This particular seeder happens to guard
its runSeed behind an isMain check, but that is not a repo-wide guarantee —
an unguarded seeder executes on import and writes production Redis.seed-bundle-macro tick.