Some work takes minutes. Transcribing an hour of audio, say. That is too long to
keep a caller on the line, so it goes in a workflow: a function whose
progress is recorded step by step, so the work survives the process it started
in. Restart a sixty-segment job at segment 41 and the first forty are not
re-done.
const text = await ctx.step("transcribe",() => transcribe(audio));
return { text };
}
ctx.step(name, fn) runs fn once and records what it returned.
The body is re-run from the top on every resume. Each step that already finished
returns its recorded value instead of running again. That replay is what makes a
run survive a restart, and it is why the real work goes inside a step: the whole
Node runtime is available in there — fetch, a model call, a database — and
none of it is available in the body.
A step is recorded under its name plus the number of times this run has already
reached that name: transcribe#0, transcribe#1, and so on, counted per name.
Two rules follow from that.
mapConcurrent(items, limit, fn) from @alexkroman1/aai/step runs a fan-out
with a ceiling on how many are in flight at once. A fan-out of any size stays
one literal name: sixty segments are sixty records under transcribeSegment,
which is exactly what the occurrence counter is for.
So do not build a name per item. ctx.step(`segment-${index}`, …) looks like
the careful version and is the bug.