Retries and waits
Step failure and time-based waiting both pause progress, but they preserve different state. A retry belongs to a step attempt. A wait is an explicit workflow command with a UTC deadline. Both can release the current worker and resume through the scheduler.
Default retry policy
ctx.schedule_step() uses RetryPolicy::default(). It permits three attempts with no delay and fails the run when the final attempt fails.
Production workflow code should usually state the policy explicitly. Readers can see the intent without looking up defaults, and history records that exact intent.
max_attempts includes the first execution. This policy runs at most four attempts and waits five seconds after each failure before the next one.
Three practical policies
An exponential policy selects a delay from 1..=current_cap milliseconds. Immutable run, step, and failed-attempt identity drive the selection. A restart therefore reproduces the same delay without preserving random-generator state.
The initial delay is at least one millisecond, the maximum cannot be lower than the initial delay, and constructors clamp max_attempts to at least one.
Continue after retry exhaustion
The default failure action is StepFailureAction::FailRun. A workflow that needs a manual-review, fallback, or compensation branch can use continue_workflow_on_failure().
ctx.step_failed() reads a durable final failure. Do not infer exhaustion from a transient error string inside workflow code.
Timer waits
wait_until() records a stable wait_id and an absolute UTC time. Before the deadline, the run is Suspended and no asynchronous call stack needs to stay resident.
The Utc::now() value in this sketch must be calculated once outside the replay decision and then pinned. A safer design places the deadline in run input or lets the host calculate it before run creation.
Reusing the same wait_id with another deadline returns NonDeterministic.
How scheduling resumes work
FlowScheduler queries FlowEventStore::list_due_wakeups() for expired waits and delayed retries. The memory and JSONL stores project histories. SQLite and PostgreSQL use indexed projections created by their migrations.
next_scheduled_wakeup() returns the earliest deadline in the store so a host can plan its next bounded sleep. A scheduler loop still needs a shutdown signal and bounded waits.
Retry policy or workflow loop
Use step retry for a brief technical failure. Every attempt keeps the same step identity, and policy owns the count and delay.
Use a workflow loop for business polling. Each round gets a new stable step ID, the workflow can inspect the result and record progress, and then it decides whether another wait is necessary. The polling_loop example demonstrates that pattern.
External steps remain at-least-once under either design. Retry count never replaces idempotency.
