Run.timeTravel()
The .timeTravel() method re-executes a workflow starting from any specific step, using either stored snapshot data or custom context you provide. Use it to debug failed workflows and test individual steps with different inputs. It can also recover from errors without re-running the entire workflow.
Usage exampleDirect link to Usage example
const run = await workflow.createRun()
const result = await run.timeTravel({
step: 'step2',
inputData: { value: 10 },
})
ParametersDirect link to Parameters
step:
inputData?:
resumeData?:
initialState?:
context?:
nestedStepsContext?:
requestContext?:
outputWriter?:
tracingContext?:
currentSpan?:
tracingOptions?:
rootSpanName?:
workflow run: '<id>' or agent run: '<id>' name. Use it to tell runs apart in trace lists.metadata?:
requestContextKeys?:
traceId?:
parentSpanId?:
nestUnderParent?:
true together with parentSpanId to attach this run as a child of that span inside the existing trace, for example an LLM judge under the run it scores. Exporters then keep the trace's own name, input, output, and tags. Leave unset when parentSpanId comes from your own tracing system. Ignored without a valid traceId and parentSpanId.tags?:
outputOptions?:
includeState?:
includeResumeLabels?:
ReturnsDirect link to Returns
result:
traceId?:
spanId?:
Extended usage examplesDirect link to Extended usage examples
Time travel with custom contextDirect link to Time travel with custom context
const result = await run.timeTravel({
step: 'step2',
context: {
step1: {
status: 'success',
payload: { value: 0 },
output: { step1Result: 2 },
startedAt: Date.now(),
endedAt: Date.now(),
},
},
})
Time travel to nested workflow stepDirect link to Time travel to nested workflow step
// Using dot notation
const result = await run.timeTravel({
step: 'nestedWorkflow.step3',
inputData: { value: 10 },
})
// Using array of step IDs
const result = await run.timeTravel({
step: ['nestedWorkflow', 'step3'],
inputData: { value: 10 },
})
Time travel with initial stateDirect link to Time travel with initial state
const result = await run.timeTravel({
step: 'step2',
inputData: { value: 10 },
initialState: {
counter: 5,
metadata: { source: 'time-travel' },
},
})
Time travel with nested workflows contextDirect link to Time travel with nested workflows context
const result = await run.timeTravel({
step: 'nestedWorkflow.step3',
context: {
step1: {
status: 'success',
payload: { value: 0 },
output: { step1Result: 2 },
startedAt: Date.now(),
endedAt: Date.now(),
},
nestedWorkflow: {
status: 'running',
payload: { step1Result: 2 },
startedAt: Date.now(),
},
},
nestedStepsContext: {
nestedWorkflow: {
step2: {
status: 'success',
payload: { step1Result: 2 },
output: { step2Result: 3 },
startedAt: Date.now(),
endedAt: Date.now(),
},
},
},
})
Concurrent time travel callsDirect link to Concurrent time travel calls
Only one timeTravel() call can run for a workflow run at a time. Before it runs anything, timeTravel() atomically claims the run by moving its stored status from the status it loaded (for example suspended or success) to running. If another caller already claimed it, this call throws WORKFLOW_TIME_TRAVEL_ALREADY_CLAIMED and executes no steps. A call that loads the run while it's already running throws WORKFLOW_TIME_TRAVEL_RUN_RUNNING.
This matters whenever time travel can be triggered more than once for the same run, such as a "retry from step" button pressed twice or a retried API request.
try {
await run.timeTravel({ step: 'step2' })
} catch (error) {
if (
error.id === 'WORKFLOW_TIME_TRAVEL_ALREADY_CLAIMED' ||
error.id === 'WORKFLOW_TIME_TRAVEL_RUN_RUNNING'
) {
// Another caller is already re-running this workflow. Re-read the run
// state instead of time travelling again.
}
}
Over HTTP, POST /api/workflows/:workflowId/time-travel-async returns 409 Conflict for a losing call. The time-travel and time-travel-stream endpoints respond before the time travel finishes, so a losing call doesn't change their status code.
Calling timeTravel() again after the previous call finished is still allowed and re-runs the target step.
The claim is enforced atomically by storage adapters that report supportsConcurrentUpdates(). Adapters without atomic read-modify-write support (such as ClickHouse, Cloudflare D1, Cloudflare KV, Cloudflare Durable Objects, LanceDB, and Redis) can't enforce it, and the claim is also skipped when shouldPersistSnapshot excludes the running status. The Inngest workflow runner doesn't claim the run yet.
NotesDirect link to Notes
- Time travel requires storage to be configured since it relies on persisted workflow snapshots
- When re-executing a workflow, the workflow loads the existing snapshot from storage (if available)
- Step results before the target step are reconstructed from the snapshot or provided context
- Execution begins from the specified step with the provided or reconstructed input data
- If a step that precedes the target in the current workflow definition has no recorded entry in the snapshot (for example, the step was renamed since the run was recorded, or the run never reached it),
timeTravelrejects with a descriptive error before anything executes and the stored snapshot is left unchanged - The workflow continues to completion from that point forward
- Time travel can be used on workflows that haven't been run yet by providing custom context or input data for the step to start from.