Errors and retries
This page shows what IdentityFlow does when a step
fails and how to change it. Retries are on by default: every flow.do step
makes up to five attempts, one minute apart. After the last one, or after an
error that stops retries, your workflow decides whether the instance ends.
First step: check whether the default fits each step that calls another system. Then go where your case is:
- a different number of attempts or delay: Retry boundary;
- an error that another attempt cannot fix: Errors that stop retries;
- a call that must not take effect twice: External effects;
- an instance that already ended as
ERRORED: Errored instances.
Every flow.do step is retried by default: up to five attempts in total,
one minute apart. Change this with the step’s retries option:
retries value | Meaning |
|---|---|
false | Run once; no retry. |
a number, such as 3 | The maximum number of attempts, including the first. 3 means at most three runs. 0 and 1 both mean a single run. |
a duration string, such as '10 seconds' | The delay between attempts. The number of attempts and the backoff keep their defaults. |
{ limit, delay, backoff } | All three settings. Each field is optional. limit: false retries without limit. |
backoff decides how the delay grows:
'constant'(the default) waitsdelaybefore every attempt.'linear'waitsdelaytimes the number of attempts made so far.'exponential'waitsdelaytimes two to the power of the attempts made so far. A delay of two seconds waits four seconds after the first failed attempt and eight after the second.
Linear and exponential delays are capped at one day.
Write durations as strings, such as '2 seconds' or '1 minute'. A number in
delay is read as milliseconds; 0 fails in 0.3.0 and errors the instance.
This step from Retry a step and stop on a final error makes at most three attempts, waits four and then eight seconds between them, and stops at once on an error that another attempt cannot fix:
return await flow.do( 'book vacation days', { retries: { limit: 3, delay: '2 seconds', backoff: 'exponential' } }, async () => { try { return await payroll.book(flow.params.employeeId, flow.params.days); } catch (error) { if (isInsufficientBalance(error)) { throw new NonRetryableError('Vacation balance is exhausted', { cause: error }); } throw error; } }, );{ retries: { limit: 3, delay: '2 seconds', backoff: 'exponential' } },- three attempts in total
- base delay
- waits 4 s, then 8 s
A step also accepts a timeout option. 0.3.0 records it with the step but does
not enforce it: an attempt runs until your callback returns or throws. To bound
a call, give your client its own timeout, for example by passing
AbortSignal.any([signal, AbortSignal.timeout(30_000)]), where signal is the
step’s signal.
Retry defaults for all steps can also be set for a workflow or for the engine; see Retry and error contracts.
Two conditions make a retry safe. The step must have a stable name, so IdentityFlow recognizes the second attempt as the same step. And everything the step calls must tolerate receiving the same request again; see External effects.
Some errors end the step without another attempt:
NonRetryableErrorsays that another attempt cannot succeed. Throw it for a permanent refusal, as the example does. Pass the original error ascauseto keep it.ValidationErrorandLockedErrorare not retried unless you create them withretryable: true. Bad input usually stays bad.TimeoutError,AssertionError,AuthorizationError,NotFoundError,ForbiddenErrorandConflictErrorare not retried either.
Any WorkflowError created with retryable: false also stops retries. Any
other error, including a plain Error and a WorkflowError without
retryable: false, is retried according to retries.
All error classes are exported from @identity-flow/sdk. The full list is under
Errors you throw or handle.
When the last attempt fails, or an error stops retries, the step ends as
ERRORED and await flow.do(…) throws that error in your workflow function.
- If your workflow does not catch the error, the
instance ends as
ERRORED. Steps of that instance that were still running or waiting end asERROREDas well. - If your workflow catches the error, it can continue, for example with a
different step or a result that records the failure. Rethrow errors for
which
isAbortError(error)is true; see Cancellation boundary.
Between two attempts, the failed attempt is recorded as a STEP_ERRORED event
with the time of the next attempt, and the instance stays ACTIVE.
Retry a step and stop on a final error shows both outcomes, a retry that succeeds and a refusal that ends the instance, with a test.
When a step calls another system, IdentityFlow delivers that call at least once. It can arrive twice. A process can stop after the other system accepted the request but before IdentityFlow recorded the step’s result; the next attempt then sends the request again. No retry setting prevents this, and neither does cancellation.
So every system that a step changes needs to recognize a repeated request:
- Send an idempotency key that the receiving system understands, and let it
treat a second request with the same key as already done. A stable key is
the step’s ID: the step callback receives the step as
step, andstep.idstays the same for every attempt of that step. - Or make the change itself repeatable, such as “set the role to approver” instead of “add a role”.
IdentityFlow records whatever your step callback returns. Return the receiving system’s confirmation, such as its booking ID, so the record shows that the other side accepted the work and not only that a request was sent.
An ERRORED instance is finished. 0.3.0 has no command that resumes or
retries it. Find the cause, fix it, and start a new instance.
To find the cause:
- An account with the
vieweraccess function sees the instance’s business steps and their status on the instance page in the IdentityFlow UI. An instance that failed with anError, such as mostERROREDinstances, shows no business data there; an operator reads its error message in the Operations view. A dialog answer that fails its schema is refused when it is submitted. A recorded answer can still error an instance when the workflow continues: if it was recorded without that check, by 0.3.0 or because the check could not decide, or if the validation rules changed after it passed. - An account with the
operatoraccess function works with instances in the Operations view. While a step is still waiting for its next attempt, the instance isACTIVE, and an operator can pause or terminate it. Operate running workflows describes the views and actions. - Reproduce the failure in a Workflow Test.
flow.observe(instance)returns the instance, its steps and its events. Each failed attempt is aSTEP_ERROREDevent whosemessagereadsAttempt #<n> errored for step "<name>" with "<error message>".
Do not repair a run by editing or deleting events. Events are the audit trail.
When you share an error report, name the workflow, the version, the step, who was acting and the error code, and leave out parameters, answers and credentials. The diagnostic of a Workflow Test has exactly this shape. For how an instance continues after a restart of the host, see Replay.