Wait without failing
Pause for time or an external event without recording a failure.
Use a release when the task is waiting for expected state. A release ends the attempt without recording a failure. Each resumed attempt still increases the attempt count used by retry.maxAttempts if a later attempt fails.
Wait for a time
Put the timed wait in the task handler. This example belongs in src/tasks/collect-report.ts:
import { task } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ REPORTS_API_URL: z.url() }).parse(process.env)
const reportSchema = z.discriminatedUnion('status', [
z.object({ status: z.literal('processing') }),
z.object({ downloadUrl: z.url(), status: z.literal('ready') }),
])
export const collectReport = task({
id: 'report.collect',
output: z.object({ downloadUrl: z.url() }),
schema: z.object({ reportId: z.string() }),
async run({ reportId }, context) {
const response = await fetch(new URL(`/reports/${encodeURIComponent(reportId)}`, env.REPORTS_API_URL), {
signal: context.signal,
})
if (!response.ok) throw new Error(`Reports API returned ${response.status}`)
const report = reportSchema.parse(await response.json())
if (report.status === 'processing') {
return context.release('30s', { reason: 'provider_still_processing' })
}
return { downloadUrl: report.downloadUrl }
},
})The next attempt starts this handler from the beginning. Keep maintenance running so transport-backed lanes publish a new wakeup when the delay is due.
Wait for a signal
A signal wakes runs that are already waiting on the same key. It does not store the approval itself. This example assumes your application has an approval API that stores the decision and returns { approved: boolean } from its read endpoint.
Read that saved decision on every attempt. Return when it is approved; wait only while it is pending:
import { task } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ APPROVAL_API_URL: z.url() }).parse(process.env)
const approvalSchema = z.object({ approved: z.boolean() })
export const waitForApproval = task({
id: 'approval.wait',
schema: z.object({ accountId: z.string() }),
async run({ accountId }, context) {
const response = await fetch(new URL(`/accounts/${encodeURIComponent(accountId)}/approval`, env.APPROVAL_API_URL), {
signal: context.signal,
})
if (!response.ok) throw new Error(`Approval API returned ${response.status}`)
const approval = approvalSchema.parse(await response.json())
if (approval.approved) return
return context.waitForSignal(`approval.${accountId}`, {
reason: 'approval_required',
timeout: '1m',
})
},
})Save the approval before sending its signal. Call this service from an authenticated route that checks the caller's access to the account. The approval API must make repeated writes safe:
import { type RunlaneRuntime } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ APPROVAL_API_URL: z.url() }).parse(process.env)
export async function approveAccount(runlane: RunlaneRuntime, accountId: string) {
const response = await fetch(new URL(`/accounts/${encodeURIComponent(accountId)}/approval`, env.APPROVAL_API_URL), {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ approved: true }),
})
if (!response.ok) throw new Error(`Approval API returned ${response.status}`)
return runlane.signals.send(`approval.${accountId}`)
}The resumed handler starts at the top, reads the saved approval, and returns successfully. Calling waitForSignal() on every attempt without checking saved state would keep the run waiting.
A signal sent before the waiter exists is not saved for later. A signal can also arrive between the approval read and the wait being stored. The one-minute timeout makes the run check again if it misses that signal. This timeout is a next-check time, not an approval deadline. Workers and maintenance must be running to make that check happen.
The send result contains only runs that this call successfully resumed. For a human decision with a durable result and a fixed deadline, use a wait token.
Use limit to bound a high-fanout send. Call it again when more matching waiters may remain.
Wait for another run
Use context.waitForRun(childRunId, { timeout }) when this task depends on the terminal state of a known run.
The wakeup does not hand the child output directly to the parent. On the next attempt, read the child run and decide how to handle its terminal state.
Wait for a stored result
Use a wait token when the external event must carry one durable result, such as an approval. Tokens remain readable before and after a run resumes.
| Need | Use |
|---|---|
| Retry a failure | Task retry policy |
| Poll again later | Timed release |
| Wake current matching waiters | Signal |
| Store one external decision | Wait token |
| Follow one known run | Run wait |
Verify the wait
- Execute the first attempt.
- Confirm the run is
releasedwith the expected wait condition. - Advance a controlled clock, send the signal, complete the token, or finish the child.
- Run maintenance when the lane requires it.
- Execute the next attempt and check the final state.
If a run does not resume, check the key or due time, maintenance, environment, queue, and delivery path. Do not throw an error to wake it sooner; that changes failure history and retry accounting.
Code before the release can run again. Save repeat-sensitive work as a durable step.