tasks
Background work: scheduled jobs and an on-demand queue. For when tasks are the right tool (and how the engine behaves), see the Tasks service.
Tasks have two sides: you declare them in src/tasks.ts with task(), and
you drive them with the tasks handle — both imported from the SDK:
import { task, tasks } from "@lodekit/sdk";The handle works in the browser and in server functions.
The src/tasks.ts contract
Section titled “The src/tasks.ts contract”Declare each task as a named export of src/tasks.ts built with task():
task<P = unknown>(config: TaskConfig<P>): DefinedTask<P>interface TaskConfig<P = unknown> { cron?: string; every?: string; catchUp?: string | boolean; retries?: number; timeout?: string; run: (payload: P) => void | Promise<void>;}cron— a 5-field cron expression, machine-local timezone. Mutually exclusive withevery.every— interval sugar ("30m","1h"), minimum"1m". Mutually exclusive withcron. Omit both for an on-demand task you run withenqueue().catchUp— catch-up window for occurrences missed while the engine was off ("6h");true= always catch up (still coalesced to one run);false= on-time only. Default"1h".retries— retries after the first attempt, 0–20. Default3.timeout— per-attempt timeout. Default"5m".run— the function that does the work. Required. Receives the payload passed toenqueue()(undefinedfor scheduled runs).
The task’s name is the kebab-case of its export identifier — the export
care_reminder is the task care-reminder everywhere else (in enqueue(),
runs(), the dashboard). Names match /^[a-z0-9-]+$/, max 128 characters.
Example
Section titled “Example”Greenhouse declares all three forms — on-demand, cron, and interval:
import { task } from "@lodekit/sdk";
export const care_reminder = task({ run: async (p: { plantId: number; kind: CareKind }) => { /* ... */ },});
export const morning_rounds = task({ cron: "0 9 * * *", catchUp: true, run: async () => { /* join across careState/plants/careRules, break streaks */ },});
export const compost_sweep = task({ every: "12h", run: async () => { /* prune archived photos + trim journals */ },});tasks.enqueue()
Section titled “tasks.enqueue()”enqueue(name: string, payload?: unknown, opts?: { delay?: string; at?: number; dedupeKey?: string }): Promise<number>name— the task’s name (kebab-case, as above).payload— passed to the task’srunfunction.opts.delay— run no earlier than this duration from now ("30m"). Mutually exclusive withat.opts.at— run no earlier than this ms-epoch time. Mutually exclusive withdelay.opts.dedupeKey— dedupe handle, 1–256 characters.
Queues a run and resolves to its run id. With a dedupeKey, if a queued
run of the same task already holds that key, no new run is created — you get
the existing run’s id back. Throws for an unknown or retired task.
Example
Section titled “Example”Greenhouse schedules a reminder at the exact due time, deduped per plant and care kind:
const runId = await tasks.enqueue( "care-reminder", { plantId, kind }, { at: dueAt, dedupeKey: `${plantId}:${kind}` });“Remind me later” re-queues it an hour out, under its own dedupe key:
const runId = await tasks.enqueue("care-reminder", { plantId, kind }, { delay: "1h", dedupeKey: `${plantId}:${kind}:later`,});tasks.cancel()
Section titled “tasks.cancel()”cancel(runId: number): Promise<boolean>runId— the id returned byenqueue().
Cancels a run that is still queued. Returns true if it was canceled,
false when the run already started or finished, or doesn’t exist.
Example
Section titled “Example”if (prev?.reminderRunId != null) await tasks.cancel(prev.reminderRunId).catch(() => false);tasks.runs()
Section titled “tasks.runs()”runs(opts?: { task?: string; state?: TaskRunState; limit?: number }): Promise<TaskRun[]>opts.task— only runs of this task.opts.state— only runs in this state.opts.limit— max results. Default 50 (thetasks.list_limitplatform setting), clamped to 1–200.
Returns run history, newest first.
Example
Section titled “Example”const failures = await tasks.runs({ task: "morning-rounds", state: "failed" });tasks.list()
Section titled “tasks.list()”list(): Promise<TaskInfo[]>Returns every declared task with its schedule, retired flag, config error (if its declaration failed validation), next scheduled run, and a summary of its last run.
Example
Section titled “Example”const infos = await tasks.list();const rounds = infos.find((t) => t.name === "morning-rounds");// rounds.nextRun, rounds.lastRun?.stateHow the engine runs your tasks
Section titled “How the engine runs your tasks”TaskRunState
Section titled “TaskRunState”type TaskRunState = "queued" | "running" | "succeeded" | "failed" | "canceled";TaskRun
Section titled “TaskRun”interface TaskRun { id: number; task: string; state: TaskRunState; source: "enqueue" | "schedule" | "manual"; attempt: number; payload: unknown; notBefore: number; createdAt: number; startedAt: number | null; finishedAt: number | null; durationMs: number | null; error: string | null;}id— the run id.task— the task’s name.state— where the run is in its lifecycle.source— how it was created:enqueue(), the schedule, or a manual trigger.attempt— which attempt this is (1 = first).payload— the payload it was enqueued with.notBefore— ms-epoch earliest start time.createdAt/startedAt/finishedAt— ms-epoch timestamps (nulluntil reached).durationMs— wall-clock duration of the finished attempt, ornull.error— the failure message, ornull.
TaskInfo
Section titled “TaskInfo”interface TaskInfo { name: string; schedule: { kind: "cron" | "every"; expr: string } | null; retired: boolean; error: string | null; nextRun: number | null; lastRun: { runId: number; state: TaskRunState; finishedAt: number | null; error: string | null } | null;}name— the task’s name.schedule— its schedule, ornullfor on-demand tasks.retired—truewhen the task was removed fromsrc/tasks.ts; its run history sticks around, but it no longer runs and can’t be enqueued.error— the declaration’s validation error, ornull.nextRun— ms-epoch time of the next scheduled run, ornull.lastRun— a summary of the most recent run, ornullif it never ran.