Guide · 06

Run code later.

Schedule a mutation to run after a deadline. The schedule is saved in the same commit as the change that made it.

This app marks an invoice ready five seconds after its last update. Save it as examples/invoices.ts and deploy it.

examples/invoices.ts
import { collection, define, mutation, query } from "@flower-js/sdk";
import { scheduler } from "@flower-js/sdk/scheduler";

const invoices = collection<{ total: number; status: string }>("invoices");
const finalize = mutation("internal.invoice.finalize", (ctx, id: string) => {
  const invoice = ctx.get(invoices, id);
  if (invoice) ctx.set(invoices, id, { ...invoice, status: "ready" });
  return null;
});
const timers = scheduler("invoiceTimers", { finalize });

const update = mutation("internal.invoice.update", (ctx, args: { id: string; total: number }) => {
  if (!args || typeof args.id !== "string" || !args.id ||
      !Number.isSafeInteger(args.total) || args.total < 0) {
    throw new TypeError("An invoice ID and nonnegative integer total are required");
  }
  ctx.set(invoices, args.id, { total: args.total, status: "draft" });
  return timers.after(ctx, `finalize:${args.id}`, 5_000, "finalize", args.id);
});
const get = query("internal.invoice.get", (ctx, id: string) => ctx.get(invoices, id));

export default define({
  collections: [timers.records],
  maintenance: timers.maintenance,
  http: { "invoice.update": update, "invoice.get": get },
});
Try the delayed update
node sdk/cli.ts deploy examples/invoices.ts
node sdk/cli.ts call invoice.update '{"id":"inv-1","total":2400}' \
  --request-id invoice-edit-1
node sdk/cli.ts watch invoice.get '"inv-1"'
  • Add timers.records to your collections.
  • Reusing a timer ID replaces its deadline. Use a new ID per callback you want.
  • timers.at takes an epoch-millisecond deadline. cancel, get, scan and retry manage timers.

How timers run

  • A timer stores a handler name and JSON arguments, not a closure. When renaming a handler, keep the old name or migrate pending timers.
  • To repeat, have the callback reschedule itself under the same ID.
  • A failed callback’s changes are discarded, then it is retried: three attempts, 1,000 ms first backoff, 60,000 ms maximum.
  • The leader checks for due timers every 250 ms (FLOWER_MAINTENANCE_INTERVAL_MS).

A deadline means “not before.” Load, elections or lost quorum can delay it. Its changes commit once, but it may be evaluated more than once, so send external side effects through a leased worker.

Custom maintenance callbacks

Pass define({ maintenance }) and return { $flower: { continue: true } } to run again right away. A burst stops when nothing changes, at the transaction byte budget, or after FLOWER_MAINTENANCE_BURST_MS (50 ms). See examples/scheduling.ts for a fuller timer app.