How many times the stage function may run in total, INCLUDING the first
run. Must be an integer >= 1; 1 means "no retry" (a declared no-op,
accepted so a policy can be switched off without deleting it — such a
stage runs once and records no attempts, unless it declares timeoutMs
or the run has a deadline: then it records its one attempt).
OptionalbackoffHow long to wait before the NEXT attempt. Either a fixed number of
milliseconds, or a function of the attempt that just FAILED (1-based) and
its error, returning milliseconds — (n) => 100 * 2 ** (n - 1) is
exponential backoff; (n, error) => statedWaitMs(error) ?? 100 * n honours
a wait the outside system stated (a Retry-After) without the engine
parsing anything. A one-argument function is unchanged. Omitted (or a
non-finite / negative result, or a throw) means no wait.
The wait is a plain awaited timer that is CANCELLED by the run's
AbortSignal. It is not a pause: a chart cannot be checkpointed in the
middle of a backoff. Under a run deadline, a wait that would outlast it is
never started: the series stops no-time-left.
OptionalmaxThe most one wait may last, in milliseconds — a finite number >= 0. The
runner ENFORCES it: every wait is min(backoffMs's answer, maxBackoffMs),
whatever the error stated, so the declaration cannot lie.
It is what makes a FUNCTION backoff countable before the run. A function
may read the error (a stated Retry-After), and no error exists at build,
so the static budget check never asks it: with maxBackoffMs it counts
each wait at the ceiling; without, the stage is unbounded ('backoff').
OptionalretryIs THIS error worth another attempt? Defaults to retrying every error.
Returning false ends the stage immediately on the normal error path, with
no retry event; the attempt's record says why it stopped (reason: 'declined').
A throw from this predicate is treated as false — a broken predicate
must not turn a failing stage into an infinite retry loop. The record
tells the two apart: a throw stops the series declined with
retryOnThrew: true.
OptionaltimeoutThe cap on ONE attempt, in milliseconds — a finite number > 0. The attempt
runs under min(timeoutMs, time left before the run's deadline); when the
cap runs out, the attempt's signal (scope.$attempt().signal) aborts with
an AttemptTimeoutError, the engine stops waiting for it, and the attempt
is recorded timeout (then retried like any failure, if the policy allows).
What the engine stops is the WAITING. The function's own work stops only if
it passes the signal on (fetch(url, { signal: scope.$attempt().signal }));
whatever it writes after its attempt ended is dropped and recorded on the
stage (StageSnapshot.lateWrites).
Declared with attempts: 1, it is a timeout with no retry — the stage
still records its one attempt.
Declarative per-stage retry policy — the policy the attempt runner runs (
AttemptPolicyin the design is this one type, under this one name).WHY this exists: a retry hand-rolled inside a stage function is invisible to the trace. The stage shows one execution, the commit log shows one bundle, and the two failed calls that preceded the successful one left no mark anywhere — an unexplained behaviour in a library whose whole thesis is that nothing invisible happens. Declared here instead, every attempt is part of the record: each attempt fires
FlowRecorder.onAttemptwith its AttemptRecord, each failed attempt that is retried also firesFlowRecorder.onStageRetry, and the stage's execution-tree node carriesattemptsandattemptSummary.A POLICY, not a stage kind —
retrysits on a normal stage the wayisPausable/isStreamingdo. The serialized nodetypeis unchanged and every existing consumer keeps rendering the stage exactly as before.Example