Skip to content

Scheduling

Jobs can be scheduled to start at a future time using the schedule option. The job is created transactionally but won’t be processed until the specified time.

await withTransactionHooks(async (transactionHooks) =>
client.startJobChain({
transactionHooks,
typeName: "send-reminder",
input: { userId: "123" },
schedule: { afterMs: 5 * 60 * 1000 }, // 5 minutes from now
}),
);

The same schedule option works with continueWith for deferred continuations:

return complete(async ({ continueWith }) =>
continueWith({
typeName: "follow-up",
input: { userId: job.input.userId },
schedule: { afterMs: 24 * 60 * 60 * 1000 }, // 24 hours later
}),
);

A scheduled job is just a pending job with a future scheduledAt. Use triggerJob (or triggerJobs for a batch) to override the schedule and run it immediately — useful for admin “run now” actions or manually advancing a queued reminder.

await withTransactionHooks(async (transactionHooks) =>
client.triggerJob({
transactionHooks,
id: jobId,
}),
);

triggerJob throws JobNotFoundError if the job does not exist and JobNotTriggerableError if it is not pending (e.g. already running, completed, or blocked).

triggerJobs validates atomically — if any id in the batch is missing or not pending, the entire call fails and no job is triggered. Both methods are mutating and require transactionHooks + a transaction context.

For periodic tasks like daily digests, health checks, or billing cycles, start a new independent job chain from within the handler instead of using continueWith. This keeps each execution as its own short-lived chain rather than building an ever-growing chain history.

const jobTypeRegistry = defineJobTypeRegistry<{
'daily-digest': {
entry: true;
input: { userId: string };
output: { sentAt: string };
};
}>();
// In processor — start a new chain with a scheduled delay
'daily-digest': {
attemptHandler: async ({ job, complete }) => {
await sendDigestEmail(job.input.userId);
return complete(async ({ sql, transactionHooks }) => {
if (userStillSubscribed) {
await client.startJobChain({
sql,
transactionHooks,
typeName: 'daily-digest',
input: { userId: job.input.userId },
schedule: { afterMs: 24 * 60 * 60 * 1000 }, // Run again tomorrow
deduplication: {
key: `digest:${job.input.userId}`,
excludeJobChainIds: [job.chainId], // Skip the completing chain
},
});
}
return { sentAt: new Date().toISOString() };
});
},
}

See examples/showcase-scheduling for a complete working example demonstrating recurring jobs with scheduling and deduplication. See also Deduplication and Transaction Hooks.