Retry failed work
Retry temporary failures with a bounded attempt budget and backoff.
Use automatic retries for transient failures where running the same task again is safe. Use a release instead when the task is waiting for an expected business condition.
Set the retry policy on the task
Put retry policy on the task definition, for example in src/tasks/sync-account.ts:
import { ErrorCode, RetryBackoffType, RunlaneError, task } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ ACCOUNTS_API_URL: z.url() }).parse(process.env)
export const syncAccount = task({
id: 'account.sync',
schema: z.object({ accountId: z.string() }),
retry: {
maxAttempts: 4,
backoff: {
type: RetryBackoffType.Exponential,
delay: '30s',
maxDelay: '5m',
},
},
async run({ accountId }, context) {
const response = await fetch(new URL(`/accounts/${encodeURIComponent(accountId)}/sync`, env.ACCOUNTS_API_URL), {
method: 'POST',
signal: context.signal,
})
if (response.status === 404) {
throw new RunlaneError({
code: ErrorCode.TaskFailed,
message: 'The account no longer exists.',
retryable: false,
})
}
if (!response.ok) throw new Error(`Accounts API returned ${response.status}`)
},
})maxAttempts includes the first attempt and any attempts that ended in a release. A release records no failure, but its resumed attempt still increases the count used for a later retry decision.
If you omit retry, a failed task does not retry automatically. When you set retry, you must supply maxAttempts. See the task reference for backoff options and defaults.
With a retry policy, an ordinary thrown error becomes a retryable task failure. If attempts remain, Runlane stores the run as retrying with its next due time. The fourth failed attempt in this example exhausts the budget and stores a terminal failed run.
Keep the handler safe to repeat. An external effect can succeed before Runlane stores the attempt result.
Use provider idempotency or a durable step around work that must not be submitted twice.
Stop retrying a permanent failure
Use RunlaneError with retryable: false when another attempt cannot help. In the example, a missing account ends the run as failed even if attempts remain. Other HTTP failures use an ordinary Error, so the task's retry policy applies.
Choose which responses are permanent based on the API you call. A validation error, revoked access, or deleted record may need an application change before the work can succeed.
Catch public API errors with error instanceof RunlaneError, then inspect error.code and error.retryable. Use the error reference for the full constructor, stored failure fields, and error codes.
Make sure due retries can run
Polling workers can acquire due retrying runs from storage. Transport-delivery deployments also need maintenance to append and publish a fresh delivery request when a retry becomes due. Keep that code in the deployment's maintenance entry point; do not put it in the task module.
To verify the policy, make the dependency fail in a controlled test, execute one attempt, and inspect runlane.runs.attempts(runId). The run should be retrying before the budget is exhausted and failed afterward.
If a run fails without retrying, check:
maxAttemptsis greater than the current attempt;- the error is retryable;
- a structured error was not marked non-retryable.
If a retry is past due, inspect workers, maintenance, and delivery. Raising the retry count will not move stuck work.
Use wait without failing when the provider is healthy but has not finished the requested work.