Job Shapes
Background, scheduled, and polling jobs all use durable runs.
Runlane has three product shapes, but only one execution record: the run.
application request -> trigger() -> run
schedule fire time -> tick() -> run
business wait point -> context.release() -> same run continues laterThe shape changes how a run is created or resumed. It does not change what operators inspect. Background, scheduled, and polling work all move through the same run statuses, event history, leases, queues, retry policy, release policy, cancellation, and operator APIs.
| Shape | What starts or resumes it | What executes user code | Operator view |
|---|---|---|---|
| Background job | Application code calls trigger() or runNow() | worker(), executeNext(), executeDelivery(), or the one inline runNow() attempt | One durable run created from a caller request |
| Scheduled job | tick() materializes a registered task schedule | The normal worker or delivery path | One durable run with source.type = "schedule" |
| Polling job | A task returns context.release(...); later tick() requests delivery when due | The normal worker or delivery path | The same durable run, with release events instead of failure noise |
Background jobs
A background job starts when application code asks Runlane to do work durably.
const { run } = await runlane.trigger(sendWelcomeEmail, { userId: 'user_123' })trigger() validates the payload, persists a run, records a delivery request, and returns a TriggerRunResult. With the default dispatch policy it also tries to publish the new wakeup immediately. If dispatch is deferred, or if a retryable transport publish fails, tick() can publish the pending outbox row later.
Workers then claim and execute the run. A process can do that by polling storage with executeNext() or worker(), or by handling a delivered transport message with executeDelivery().
Use background jobs for work that should survive process restarts, be retried on transient failure, and remain visible to operators. Common examples include sending email, syncing external systems, processing media, or updating search indexes.
When the application wants to start one durable attempt immediately in the current process, use runNow().
const run = await runlane.runNow(syncQuickBooksInvoices, { userId: 'user_123' })runNow() is still a background-job run with durable history and leases. It creates the run and claims the first attempt inline, then returns after that one attempt records an outcome. It does not loop through retries or releases inline. See Current-Process Execution.
Scheduled jobs
A scheduled job starts from a task({ schedule: ... }) entry instead of an application request.
const sendDigest = task({
id: 'digests.send',
schema: digestPayloadSchema,
schedule: {
id: 'digests.daily',
cron: '0 9 * * *',
timeZone: 'America/New_York',
payload: { userId: 'user_123' },
},
async run(payload) {
await sendDigestEmail(payload.userId)
},
})
await runlane.tick()tick() claims due schedule occurrences from the runtime's registered task set. For each due occurrence, it materializes an ordinary durable run and flushes the run's wakeup through the outbox. User task code does not run inside tick(). Workers or delivered transport messages execute the generated runs later.
Schedules are not a second execution engine. Operators should inspect generated runs the same way they inspect manually triggered runs.
Runlane supports one-time, interval, and cron schedules. Interval and cron schedules materialize the latest due fire time in a tick, not every missed boundary in one call. See Schedules for schedule options and occurrence behavior.
Polling jobs
A polling job is a durable run that reaches a business wait point and asks to continue later.
const pollReport = task({
id: 'reports.poll',
schema: reportPayloadSchema,
async run(payload, context) {
const report = await reports.get(payload.reportId)
if (report.status === 'processing') {
return context.release('5m', { reason: 'provider_not_ready' })
}
await storeReport(report)
},
})The run is released with a resume time. When that time is due, tick() records a fresh delivery request and publishes a wakeup through the outbox. The next worker attempt loads the same persisted payload and run history, then continues through the normal task lifecycle.
Polling jobs should use release semantics, not retry semantics. Waiting for a third-party report, payment, invoice, or export is not the same as failing to execute the task.
Use retry when the attempt failed and should count as failure pressure. Use release when the task made progress but must wait for external business state. See Retry Vs Release.