Runlane
Concepts

Schedules

Schedules materialize durable runs over time.

A schedule is deployment-time task configuration that creates runs automatically at a specific time or cadence. A schedule does not execute user code directly. It materializes durable runs, and workers execute those runs through the normal task lifecycle.

Runlane supports three schedule shapes:

  • once: materialize a run at one specific time
  • interval: materialize the latest due run on a fixed duration cadence
  • cron: materialize the latest due run from a cron expression, optionally interpreted in a time zone

Schedules are colocated on the task they create. The authoring shape does not include a type or taskId field; core infers the schedule kind from exactly one of runAt, every, or cron and attaches the task id during registration.

import { createRunlane, queue, task, WorkerMode } from '@runlane/core'

const digestQueue = queue({ name: 'digests', default: true })

const sendDigest = task({
  id: 'digests.send',
  queue: digestQueue,
  schema: digestPayloadSchema,
  schedule: [
    {
      id: 'digests.daily',
      cron: '0 9 * * *',
      timeZone: 'America/New_York',
      payload: { userId: 'user_123' },
    },
  ],
  async run(payload) {
    await sendDigestEmail(payload.userId)
  },
})

const runlane = createRunlane({
  lane,
  queues: [digestQueue],
  tasks: { sendDigest },
})

await runlane.tick()
await runlane.worker({ mode: WorkerMode.Drain }).closed

Schedule options:

OptionApplies toRequiredDefaultWhat it controls
idall schedulesYesNoneStable schedule id. It is part of deterministic occurrence identity, must be unique across the runtime's task catalog, and cannot contain :.
queueall schedulesNoTask queue or runtime default queueQueue definition used by materialized runs. The queue must be registered on the runtime.
enabledall schedulesNotrueWhether the runtime materializer should consider this schedule.
payloadall schedulesRequired when task schema requires payloadundefined only when the task schema accepts itStatic JSON payload validated synchronously when the task is registered, then validated again before materialization. There is no payload callback.
runAtonceYesNoneExact Date when one run should be materialized.
everyintervalYesNoneRunlane duration string cadence, such as 15m or 1h.
startsAtintervalNoUnix epoch anchorAlignment point for interval cadence.
endsAtintervalNoNo end boundStops materializing interval occurrences after this time.
croncronYesNoneCron expression used to find the latest due fire time.
timeZonecronNoCron parser defaultIANA time zone used to interpret the cron expression.

Call runlane.tick() from maintenance infrastructure. It claims due occurrences from the runtime's registered task schedules and writes ordinary durable runs. It is safe to call from a cron process, a serverless scheduled function, or a maintenance worker. It does not execute task code; workers still claim and execute the materialized runs.

One tick is bounded maintenance. By default, contractDefaults.maintenance allows one pass to:

  • materialize up to 100 due schedules
  • terminally cancel up to 100 abandoned cancellation requests
  • request delivery for up to 100 due waiting or expired-lease runs
  • claim up to 100 outbox rows

Call tick() repeatedly when draining a large backlog. Each call uses the runtime clock once, then runs schedule materialization, cancellation finalization, delivery recovery, and outbox publishing in that order. Keep maintenance separate from transport delivery handlers; a delivery handler should process the wakeups it received, while tick() is a bounded global pass.

Materialization is durable per occurrence, not transactional across the whole registered schedule set. If one schedule fails, earlier schedules in the same tick remain materialized. The failing tick rejects and later maintenance phases do not run in that call; rerun tick() after fixing the failure to advance the rest and flush pending outbox rows.

Occurrences

A schedule occurrence is the durable claim for one scheduled fire time. Occurrences prevent duplicate materialization when more than one scheduler process is running.

Storage must support schedule occurrence claims for due registered schedules. The scheduler claims an occurrence, creates or recovers the generated run, and marks the occurrence complete with the run ids it produced. If another process already owns or completed the occurrence, storage returns no claim and the materializer skips it.

Occurrence ids are deterministic for environment + scheduleId + fireAt. The generated id is an opaque bounded hash, not a parseable key. The scheduled run id is derived from that occurrence id, so all materializers race on the same durable identities.

If a process creates the scheduled run and crashes before completing the occurrence, the next materializer can reclaim the same occurrence after claim expiry, see the already-created scheduled run, and complete the occurrence with that run id instead of creating a duplicate.

Interval and cron materialization intentionally emits at most one occurrence per schedule per tick: the latest fire time due at or before the maintenance time. Run tick() repeatedly from maintenance infrastructure to advance recurring schedules over time. Missed intermediate interval or cron boundaries are not backfilled in the same call.

Schedule timing rules:

ShapeTiming behavior
onceMaterializes at the original runAt once it is due, even if the materializer runs late.
intervalMaterializes the latest interval boundary at or before the maintenance time. The cadence anchors to Unix epoch unless startsAt is set, and endsAt caps the latest eligible boundary.
cronMaterializes the most recent matching fire time at or before the maintenance time, using timeZone when supplied. Create or enable cron schedules only when that catch-up behavior is desired.

Run Creation Semantics

Scheduled runs are ordinary durable runs with source.type = "schedule" and source.scheduleId set. They use the occurrence id as their dedupe identity. Queue capacity does not block materialization: due runs are created first, then dispatch and lease acquisition wait behind bounded queue capacity.

Schedule queues override the task queue for materialized runs. If a schedule does not specify queue, runtime resolution falls back to the task queue, then the runtime default queue. Missing or unregistered queues fail runtime registration or dynamic task registration before a scheduled run is created.

Task-level singleton keys still apply, so recurring schedules cannot overlap work for the same resource when the lane enforces singleton keys. Task-level idempotency keys do not apply to scheduled runs; reusing producer idempotency across recurring occurrences would suppress legitimate future runs.

Task-level concurrency keys also apply to scheduled runs and require bounded-queue support just like triggered runs. Core resolves singleton and concurrency keys before claiming the occurrence, so capability or key-validation failures do not consume an occurrence claim.

Each schedule entry carries static payload data when the task schema requires payload. Omitted payload is allowed only when the task schema accepts undefined; omission passes undefined through the same schema validation before registration and materialization. Because schedules are deployment-time configuration, schedule payload schemas must validate synchronously during registration.

Runlane does not support payload builder callbacks or implicit trigger-time payload inheritance in the task-colocated schedule surface. Model dynamic multi-tenant recurring work through a separate application API rather than hiding it inside static schedule definitions.

Invariants

Schedules are definitions. Occurrences are durable claims. Runs are executable work.

This separation keeps scheduled jobs observable. Operators inspect the generated runs, not a separate schedule execution system.

On this page