Guide · 07

Hand out work. Let records expire.

Give a job to one worker at a time, and treat old records as gone. Both are plain TypeScript helpers.

A work queue gives each job to one worker at a time. An expiring collection hides records after a set time. For worker design, see External workers.

Claim, work, complete

Inside your application module
import { expiringCollection, workQueue } from "@flower-js/sdk/temporal";

const jobs = workQueue("jobs", { maxLeaseMs: 30_000 });
const sessions = expiringCollection("sessions", {
  expiration: { afterUpdateMs: 60_000 },
});
const tokens = expiringCollection("tokens", {
  expiration: { afterCreationMs: 10_000 },
});
  • Add each helper’s records to define({collections}). Jobs are claimed oldest first.
  • Separate tenants with workQueue("jobs", {maxLeaseMs: 30_000, scope: tenant}); inspect with queue.scan(ctx). Scopes are not access control.

examples/workers.ts wraps enqueue, claim, complete, fail and retry in mutations. Deploy it to a fresh app:

Enqueue work using the supplied example
node sdk/cli.ts deploy examples/workers.ts
node sdk/cli.ts call jobs.enqueue \
  '{"id":"receipt-1","payload":{"invoice":"inv-1"}}' \
  --request-id enqueue-receipt-1
worker.ts · checkout root
import { FlowerClient } from "@flower-js/sdk";
import type { Claim } from "@flower-js/sdk/temporal";

const client = new FlowerClient("http://127.0.0.1:7101");
const args = { owner: "worker-1", leaseMs: 10_000 };
const claimId = crypto.randomUUID(); // Persist this for retries of this call.
const { value: lease } = await client.call<typeof args, Claim | null>(
  "jobs.claim", args, { requestId: claimId },
);

if (lease) {
  // Perform external work here, using idempotency or downstream fencing.
  // Preserve this completion ID and its exact arguments on uncertain replies.
  const completionId = crypto.randomUUID();
  await client.call("jobs.complete", {
    id: lease.id, owner: lease.owner, token: lease.token,
    ...(lease.history ? { history: lease.history } : {}),
    result: { processed: true },
  }, { requestId: completionId });
}
  • claim returns {id, payload, owner, token, expiresAt, attempt, history?}, or null when there is no work.
  • complete and fail need the current owner and token before expiry, or fail with LEASE_LOST. Expired work is claimable again at once.
  • Failed jobs stay until retried. Leases can’t be renewed.
  • Retrying a claim with the same request ID returns the original, possibly expired, lease. Use a new ID for new work.

Expiry does not stop the old worker. The receiving service should take idempotency keys or check fencing tokens.

Let records expire

A record is hidden once ctx.now() >= expiresAt. sweep, called from a mutation or maintenance, deletes it. entry returns the value and its timestamps.

Expiration policies
Policy passed to setMeaning
{ afterCreationMs: 10_000 }Expires from its original live creation time.
{ afterUpdateMs: 60_000 }Restarts the lifetime on each update.
{ at: epochMilliseconds }Uses an absolute deadline chosen by your code.
nullDoes not expire.
  • Updates keep the creation time; writing over an expired record starts fresh.
  • Reads never extend a lifetime; rewrite in a mutation to refresh. For logic on expiry, use a timer.
  • Keep node clocks in sync. ctx.now() is fixed per evaluation, and commit time counts against leases and lifetimes.