⚠️ AMENDMENT — the
step.runENUMERATION in this card is WRONG (motir run MOTIR-3414, 2026-08-23)Re-measured before the first line of work, on the ref rather than on a working tree, per the run-time rule that a card counting a population is re-measured against the ref it names (or
origin/mainwhen it names none). Branch base:origin/main@165f1485.$ git grep -c 'step\.run(' origin/main -- '*.ts' '*.tsx' 60 occurrences across 27 files $ git grep -c 'step\.run(' origin/main -- '*.ts' '*.tsx' ':!tests/*' 58 occurrences across 26 files"84 call sites across 37 files" is in fact 58 across 26 (60 across 27 if the one test file is counted). Read every "84" below as 58, and every "37 files" as 26.
The second limb is falsified outright, and it is the one that was load-bearing. The story's
explanationMdargued the shim earns its cost because the call sites live "including services well outsidelib/jobs/". They do not:$ git grep -l 'step\.run(' origin/main -- '*.ts' '*.tsx' ':!tests/*' 25 of 26 files are under lib/jobs/ ; the 26th is scripts/plan-seed/data/story-1.6.ts — seed FIXTURE DATA, not a serviceNo service file outside
lib/jobs/callsstep.run. The blast radius of rewriting the call sites islib/jobs/and nothing else — which is where the seam already is.What changes and what does not. The DELIVERABLE is untouched and the CONCLUSION stands: 58 call sites are still 58 that should not be rewritten, and a shim that keeps the existing job definitions compiling unchanged is still the right shape. What weakens is the COST argument — the shim is bought for compatibility, risk and reversibility, not for a blast radius that reaches outside the jobs layer. Recorded rather than quietly deleted, because an amendment with evidence and a silently-dropped clause look identical in the diff and are opposite in kind.
Where
37probably came from:lib/jobs/registry.tsexports 37 job functions across 24 definition files. Two real populations, correctly observed, fused into a sentence about a third.Planning bug: MOTIR-3428. Nothing else in this card is changed by the amendment — no deliverable, edge, repo pin, estimate or acceptance criterion.
The foundation every other story in this epic fans off. It builds the Postgres-backed job engine behind the existing defineJob / sendEvent seam and proves it on ONE pilot job in production — it does not migrate the other 23.
job_event (the log a dispatcher fans out from), job_queue (one row per run, with run_at / attempts / state), job_step (memoized step results keyed by (run_id, step_id)).step shim — the piece that makes this migration affordable. step.run(id, fn) becomes look up (run_id, id); if present return it, else execute, persist, return, which is the whole of the Inngest step contract our code actually uses. All 84 step.run call sites across 37 files change by zero characters.sendEvent routes each job id to either engine and the two run side by side for the length of the migration.onFailure and DLQ parity — the job_run ledger and /settings/workspace/jobs keep working with no change visible to an operator.This story ENDS at: one pilot job running on the new engine in production, with the ledger, the DLQ and the operator dashboard behaving exactly as they do today.
It HANDS OFF: the event-triggered jobs to the event cutover story, the 13 scheduled jobs to the scheduled story, the container supervisors and the collapse of their stepped shape to the supervisor story, and every deletion to the retirement story. Nothing in this story removes any Inngest code — both engines are live when it lands, which is exactly what makes the later cutovers reversible one job at a time.
Open /settings/workspace/jobs. Trigger the pilot job. See its run row appear, succeed, and carry its output — indistinguishable from an Inngest-backed run. Force it to fail; see it retry, exhaust, and land in the DLQ tab. Press Replay; see it succeed.
It is not a general-purpose durable-execution engine. It implements exactly the contract our 24 jobs use — memoized steps, durable sleep, retries, cron, one debounce, one idempotency key — and deliberately nothing else. waitForEvent is not implemented, because it has zero real call sites: the single grep hit in the tree is a comment quoting Inngest's documentation.
lib/jobs/defineJob.ts — the wrapper whose signature must not changelib/jobs/sendEvent.ts · lib/jobs/registry.ts — the emit side and the function listlib/jobs/dlq.ts · lib/services/jobRunsService.ts · app/(authed)/settings/workspace/jobs/page.tsx — the ledger, the DLQ and the operator surface that must keep workingfly.toml — where the worker process group is declareddocs/jobs.md — the substrate documentation this story amends