Save step results
Save a completed step so retries can reuse its validated result.
Use a durable step when a task may restart after a retry or release but one completed side effect should not be submitted again.
import { task } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ REPORTS_API_URL: z.url() }).parse(process.env)
const jobSchema = z.object({ jobId: z.string() })
const jobStatusSchema = z.object({ pending: z.boolean() })
export const processReport = task({
id: 'report.process',
output: z.object({ jobId: z.string() }),
schema: z.object({ reportId: z.string() }),
async run({ reportId }, context) {
const submission = await context.step.run('submit-provider-job', { output: jobSchema }, async ({ token }) => {
const response = await fetch(new URL('/jobs', env.REPORTS_API_URL), {
body: JSON.stringify({ reportId }),
headers: { 'content-type': 'application/json', 'idempotency-key': token },
method: 'POST',
signal: context.signal,
})
if (!response.ok) throw new Error(`Reports API returned ${response.status}`)
return jobSchema.parse(await response.json())
})
const response = await fetch(new URL(`/jobs/${encodeURIComponent(submission.jobId)}`, env.REPORTS_API_URL), {
signal: context.signal,
})
if (!response.ok) throw new Error(`Reports API returned ${response.status}`)
const status = jobStatusSchema.parse(await response.json())
if (status.pending) return context.release('30s', { reason: 'provider_job_pending' })
return { jobId: submission.jobId }
},
})Wrap the side effect
Give the step a stable key and validate the output that Runlane will store.
On the first success, Runlane validates and transforms the callback output once, stores the schema output, and returns it. A later attempt with the same run and step key returns that saved output without calling the callback or applying the schema again. Other runs have their own checkpoints, even when they use the same step key.
Keep a step key's saved output shape compatible across deployments. Original callback outputs are not retained separately; changing the schema does not transform existing checkpoints. Storage still validates the persisted record format on reads.
The task can now release and poll again without intentionally submitting a second provider job.
Use the step token as the provider's idempotency key when possible. A remote call can succeed before its checkpoint is stored.
The token lets a repeated submission resolve to the same remote operation. A durable step cannot make a non-idempotent API exactly once.
A callback that throws is not checkpointed. Output that fails the step schema is also not checkpointed and fails the attempt with a structured task-output error. Fix the callback or schema mismatch before retrying.
Test a failure or release after the step. Execute another attempt and confirm the callback ran once while both attempts saw the same output.
Durable steps are part of the storage contract, so every conforming lane keeps these checkpoint rules.
The same rule applies to human review. Create the wait token in a durable step and use the step token as its idempotency key.
After resume, the step returns the original token id instead of creating another approval. See build a human-in-the-loop flow.