step boundaries and names
why
retry repeats whatever sits inside the retried unit, so the boundary decides what gets charged, written, and sent twice. a step wrapping a read, a provider charge, and a notification turns a late failure into a duplicated customer payment; the same boundary, split, repeats only what is safe. the exact stable name is the identity the durable runtime uses to decide which attempts re-run and which results are reused — renaming it silently invalidates in-flight progress.
rule
- a step contains work that may safely repeat together. split before an external side effect, an expensive provider call, a long wait, or a result a later step needs independently.
- name every step with an exact, stable identity, unique within its workflow instance. the name is an execution identity, not display copy; changing it changes the durable execution contract and needs a documented migration decision.
- branch before the step call, not inside it, when the branch selects a different durable operation. each operation gets its own exact step name, so the runtime step list shows which path executed instead of hiding the fork inside one generic union result.
boundary test
ask: "if the next instruction fails, do I want this preceding work to run again?" if not, make the preceding work its own durable step and persist its result.
- group short, atomic work that shares one retry and one outcome.
- split storage reads, provider calls, writes, and publishing when they have different failure modes or reuse value.
- put every awaited external side effect inside the durable step that owns its retry behavior.
- return a typed bounded result, usually a stable reference.
names
- use a direct, exact name such as
"crawl: read source", or a named step-name builder with explicit typed inputs. - never derive a step name with replacement, casing conversion, timestamps, random values, or a model response.
- different operations must have different names even when they call the same provider.
do not
- wrap an entire workflow in one retryable step.
- reuse a step name for two different operations.
- hide a category fork inside one step that returns a union result.
- let a notification call sit outside the durable decision it announces when duplication would create a false record.
- use a generic
"process","run", or"step"name.
good and bad
bad
step("process", async () => {const source = await readSource();const result = await expensiveProviderCall(source);await persistResult(result);await notifyCompletion(result);});
one generic name wraps storage read, provider call, write, and notification in a single retry unit: a late failure replays the charge, the customer pays twice, no intermediate result is reusable, and casually renaming the name loses in-flight progress.
good
step("crawl: read source", readSource);step("crawl: fetch provider result", fetchProviderResult);step("crawl: persist result", persistResult);step("crawl: notify completion", notifyCompletion);
each side effect with its own failure mode and reuse value gets an exact, stable step name: a transient notification failure retries one cheap step instead of a charge, a resume reuses completed results by name, and the durable history reads as an auditable sequence of named business actions rather than an opaque "process".