Every company that has been around a while runs a batch layer: nightly syncs between systems, month-end runs that decide what people get paid or billed, exports a regulator expects on schedule. In older applications it is usually held together by cron, shell scripts, and whoever reads the failure emails.
When it comes up for modernization, the first question is usually which language to rewrite the jobs in. That is the smaller question. The pattern we reach for answers the bigger one: replace cron with durable workflow orchestration in Temporal, keep the business rules in Rails, and retire each old job only after a side-by-side run proves the new one produces the same output.
What cron batch jobs are missing
Cron batch jobs are missing durability: nothing outside the running process records how far a job got.
Half-finished runs. A job dies at 2 AM partway through. Did the first half commit? Will a rerun double-process four thousand records? The person who knows is asleep, and the answer lives in their head, not in the system.
Silent non-runs. A job stops running and nobody notices for weeks, because success and "never started" produce the same signal: silence.
State everywhere and nowhere. Progress is implied by rows in tables, files in directories, and flags named processed_v2. Reconstructing a run takes archaeology.
Retry folklore. "If the export fails, delete the temp file, reset the flag, and run it again, but never during the billing window." Every older shop has rules like this. They are institutional risk dressed up as institutional knowledge.
A nicer language fixes none of these. Moving execution state out of the worker process fixes all of them.
How does Temporal replace cron for batch jobs?
Temporal replaces cron's fire-and-forget model with durable execution. Durable execution means the workflow's state is saved step by step outside the process that runs it, so a crash at any step resumes from the last completed step with its state intact. Temporal's documentation describes the Event History behind this as "an append-only log" that is "durably persisted by the Temporal Service, so your application state survives crashes or failures."
A workflow declares the steps (fetch, transform, write, notify), and activities do the work. Temporal records each activity's result before the workflow moves on, which maps one to one onto the failure list above.
A worker dies mid-run, and the workflow resumes on another worker from the last completed step, without redoing committed work. Retries become a policy declared per activity instead of folklore. The 2 AM question ("what state is it in?") has an answer you can look up, because the Temporal Web UI shows each workflow's history, inputs, and results. A job that never started becomes a visible, alertable condition.
For teams coming from cron, the real change is that idempotency and retry rules move out of tribal memory and into the code. The platform enforces what the folklore only recommended.
Not every batch layer needs this much machinery. When the work is mostly moving records between systems and calling an API along the way, workflow automation with n8n covers it with far less to run. Temporal earns its weight when runs are long, stateful, and financially consequential.
Where Rails fits
Rails holds the domain logic, and Temporal only orchestrates. Rails suits batch work: heavy database access, and models whose validations and associations encode the business rules. Temporal ships an official Ruby SDK, so activities call into the same models the rest of the application uses. The business rules live once and get tested once, instead of being rewritten inside each job script.
The pairing is deliberately unexciting: Rails for the domain, Temporal for durability. Batch stops being a separate world with its own failure culture and becomes ordinary application code with strong execution guarantees.
How to migrate a legacy batch job without breaking it
Migrating a legacy batch job is riskier than building a new one, because the old job's real specification is its behavior, not its documentation. Two practices carry the weight in our delivery work.
Extract the behavior contract first. A behavior contract is a written record of what the job actually does: inputs, outputs, edge cases, side effects, and the quirks downstream systems depend on. We read the old code as a specification to recover, not as source to translate. AI-assisted analysis has made this step practical on code nobody fully remembers. Pinning that behavior down with characterization tests before an agent edits the code follows the same logic.
Validate by running both. The new workflow runs alongside the old job against the same data. We compare outputs until parity is shown, not asserted. Only then does the old job retire. "It passed the tests we thought to write" and "it produces what the old system produces" are different levels of confidence, and month-end financial jobs deserve the second one.
Done this way, the batch layer tends to become the most observable, most recoverable software in the company: a strange reversal for the code that used to be the scariest. If yours is due, our Ruby on Rails practice does this work, behavior contracts and parity runs included. It starts with our framework for deciding whether a legacy app is worth modernizing.

