Skip to content

Scheduling — api.scheduleNextStep()

api.scheduleNextStep() queues the next step to execute after a delay. Calling it multiple times in the same step creates a fan-out — parallel branches that each run independently.

await api.scheduleNextStep({ delay, action, payload });
OptionTypeDescription
delaynumber | stringSeconds until the next step runs. Accepts a number or a duration string. Minimum: 10 seconds. Maximum: 400 days
actionstringThe method name on your Workflow class to call
payloadanyData passed as the data argument to the next step

Instead of raw seconds, you can pass a human-readable string:

StringDuration
"30 sec" / "30 seconds"30 seconds
"5 min" / "5 minutes"5 minutes
"2 hr" / "2 hours"2 hours
"1 day" / "3 days"1 or 3 days
"1 week" / "2 weeks"1 or 2 weeks
class Workflow {
async start(data, headers, api) {
// Schedule the next step to run in 10 minutes
await api.scheduleNextStep({
delay: '10 min',
action: 'sendReminder',
payload: { orderId: data.id, email: data.email },
});
}
async sendReminder({ orderId, email }, headers, api) {
api.log('Sending reminder for order', orderId, 'to', email);
// ... send email via fetch()
}
}

Call scheduleNextStep() multiple times in one step to launch parallel branches. Each call is an independent branch. The run is not marked complete until all branches finish.

class Workflow {
async start(data, headers, api) {
// Process each line item in parallel
for (const item of data.line_items) {
await api.scheduleNextStep({
delay: 10,
action: 'processItem',
payload: { itemId: item.id, title: item.title },
});
}
// start() completes here — all N branches are now running concurrently
}
async processItem({ itemId, title }, headers, api) {
api.log('Processing item:', title);
// Each branch runs independently
}
}

Each call to scheduleNextStep() increments the chain depth. Fan-out branches at the same level all share the same depth counter — a fan-out of 100 branches from start has depth 1, not 100.

There is no per-plan chain depth limit. A platform-wide circuit breaker prevents runaway infinite loops.

api.dedupe() prevents a step from running more than once for the same logical event within a time window. It is useful for idempotent guards at the start of your start step.

const { locked } = await api.dedupe(unique_id, delay);
ArgumentTypeDefaultDescription
unique_idstringrequiredAn identifier that should be unique per logical event
delaynumber120How long (in seconds) to hold the lock before it expires

Returns { locked: boolean, key: string }. If locked is true, another invocation already acquired the lock — skip processing.

class Workflow {
async start(data, headers, api) {
const { locked } = await api.dedupe(`order-${data.id}`, 300);
if (locked) {
api.log('Duplicate event for order', data.id, '— skipping');
return;
}
// Safe to process — this invocation holds the lock
await api.scheduleNextStep({ delay: 1, action: 'processOrder', payload: data });
}
}