Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scheduler

The Scheduler runs registered tasks on a fixed 1-second tick. Each task is configured with a timing rule, optional conditions, and a handler. Tasks are one-shot by default and are automatically removed after their handler runs.

SchedulerProvider is opt-in — it is not included in defaultProviders. Add it explicitly:

import { Application, defaultProviders, SchedulerProvider } from "@artisansdk/architect"

Application.configure()
  .withProviders([...defaultProviders, new SchedulerProvider()])
  .run()

Basic usage

Register tasks from a ServiceProvider’s boot() hook:

import { Scheduler, type ContainerContract as Container } from "@artisansdk/architect"

boot(container: Container) {
  const scheduler = container.make(Scheduler)

  // Run once after 1 minute
  scheduler.do(() => showModal()).in(1, "minutes")

  // Run every hour, starting after a 5-minute delay
  scheduler.do(() => syncData()).in(5, "minutes").every(1, "hours")
}

Timing

Delay before first run — .in()

.in() sets when the task first becomes eligible to run. It accepts a numeric offset with a unit, a Date, or any object with an epochMilliseconds property (e.g. Temporal.Instant or Temporal.ZonedDateTime).

scheduler.do(fn).in(30, "seconds")
scheduler.do(fn).in(new Date("2026-07-01T09:00:00"))
scheduler.do(fn).in(Temporal.Now.instant().add({ hours: 1 }))

Supported units: "milliseconds", "seconds", "minutes", "hours".

Recurring tasks — .every()

.every() makes a task recurring. The schedule advances on a fixed cadence — the next tick is always computed from the last scheduled fire, not from the last successful run. A task whose conditions fail at a given tick will be offered again at the next interval.

scheduler.do(() => pollApi()).every(30, "seconds")

Tasks without .every() are one-shot by default and are removed after their first run.

Conditions

.when()

The handler only runs when the condition is truthy:

scheduler.do(fn).every(1, "hours").when(() => isOnline())

Supports an optional comparison operand and value:

scheduler.do(fn).every(1, "hours").when(() => retryCount, "<", 5)

Supported operands: =, ==, ===, !=, !==, <>, >, <, >=, <=.

.unless()

The handler only runs when the condition is falsy — the inverse of .when():

scheduler.do(fn).in(1, "minutes").unless(() => alreadyShownToday())

Conditions do not affect the schedule. If a condition fails at a scheduled tick, the interval still advances and the task is offered again at the next fire time.

Named tasks

Register a task by name to cancel it later without holding a reference. If a name is already in use, the existing task is removed and a warning is logged before the new one is registered.

scheduler.task("review-prompt", () => showModal()).in(1, "minutes")

// Later:
scheduler.cancel("review-prompt")

Tags

Tag tasks to cancel them as a group:

scheduler.do(() => showModal()).tag("popups").in(1, "minutes")
scheduler.do(() => showBanner()).tag("popups").every(1, "hours")

// Drop all popup tasks at once:
scheduler.cancelTag("popups")

Names and tags are separate namespaces. cancel() matches by name only; cancelTag() matches by tag only.

Cancellation

// By reference
const task = scheduler.do(fn).every(5, "minutes")
scheduler.cancel(task)

// By name
scheduler.cancel("review-prompt")

// By tag
scheduler.cancelTag("popups")

One-shot vs recurring

ConfigurationBehaviour
No .every()One-shot — runs once when conditions pass, then auto-removed.
.every(n, unit)Recurring — stays registered, fires on a fixed cadence.
.once()Explicit one-shot (same as default, useful for clarity).

Error handling

If a task handler throws, the error is caught and logged to console.warn. The task is still removed if it was one-shot, and the rest of the tasks in the tick are unaffected.

Using Scheduler directly

import { Scheduler, type ContainerContract as Container } from "@artisansdk/architect"

boot(container: Container) {
  const scheduler = container.make(Scheduler)
  scheduler.task("alert", () => showAlert())
    .when(() => !alreadySeenToday())
    .in(0, "seconds")
}