Skip to content

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.

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 with every.
  • every — interval sugar ("30m", "1h"), minimum "1m". Mutually exclusive with cron. Omit both for an on-demand task you run with enqueue().
  • 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. Default 3.
  • timeout — per-attempt timeout. Default "5m".
  • run — the function that does the work. Required. Receives the payload passed to enqueue() (undefined for 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.

Greenhouse declares all three forms — on-demand, cron, and interval:

src/tasks.ts
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 */ },
});
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’s run function.
  • opts.delay — run no earlier than this duration from now ("30m"). Mutually exclusive with at.
  • opts.at — run no earlier than this ms-epoch time. Mutually exclusive with delay.
  • 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.

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`,
});
cancel(runId: number): Promise<boolean>
  • runId — the id returned by enqueue().

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.

if (prev?.reminderRunId != null) await tasks.cancel(prev.reminderRunId).catch(() => false);
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 (the tasks.list_limit platform setting), clamped to 1–200.

Returns run history, newest first.

const failures = await tasks.runs({ task: "morning-rounds", state: "failed" });
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.

const infos = await tasks.list();
const rounds = infos.find((t) => t.name === "morning-rounds");
// rounds.nextRun, rounds.lastRun?.state
type TaskRunState = "queued" | "running" | "succeeded" | "failed" | "canceled";
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 (null until reached).
  • durationMs — wall-clock duration of the finished attempt, or null.
  • error — the failure message, or null.
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, or null for on-demand tasks.
  • retired — true when the task was removed from src/tasks.ts; its run history sticks around, but it no longer runs and can’t be enqueued.
  • error — the declaration’s validation error, or null.
  • nextRun — ms-epoch time of the next scheduled run, or null.
  • lastRun — a summary of the most recent run, or null if it never ran.