Skip to main content

Defining jobs

Jobs are defined with the defineJob factory — no classes, no decorators, and nothing reflection-based (so the runtime survives minification). Each call returns an opaque job definition you pass to a FlareWorker.

import { defineJob } from "@zeridion/flare/worker";

const sendWelcome = defineJob<NewUserEvent>({
jobType: "SendWelcomeEmail",
queue: "emails",
maxAttempts: 5,
timeoutSeconds: 60,
async handle(payload, ctx) {
await mailer.send(payload.email, { signal: ctx.signal });
ctx.reportProgress(1);
},
});

Options

OptionTypeRequiredDefaultDescription
jobTypestringyesThe routing key the server uses to match an enqueued job to this handler. Must be unique within the worker.
queuestringno"default"The queue this job runs on.
maxAttemptsnumberno3How many times the server will attempt the job before giving up.
timeoutSecondsnumberno1800Per-execution timeout. The job's signal aborts when it elapses.
parse(raw: unknown) => TnoidentityValidate / transform the raw JSON payload before it reaches handle.
handle(payload: T, ctx) => Promise<void> | voidyesYour job logic.

jobType is explicit and required

jobType is the cross-language routing key. It must be set explicitly — there is no name derivation — so a job enqueued by one service is matched by a worker written in any language as long as the strings agree. Two definitions sharing a jobType throw DuplicateJobTypeError at construction.

Payload typing and validation

The type parameter on defineJob<T> types payload and the context at compile time. By default the raw decoded JSON is passed through unchanged.

For runtime safety, supply parse — any validator that returns the typed value on success and throws on failure works (for example a schema library's parse). A thrown parse fails the job before handle runs, and the job is acknowledged as failed:

import { z } from "zod";

const Schema = z.object({ email: z.string().email(), name: z.string() });

const sendWelcome = defineJob<z.infer<typeof Schema>>({
jobType: "SendWelcomeEmail",
parse: (raw) => Schema.parse(raw), // throws → job acked failed
async handle(payload, ctx) {
// payload is fully validated here
},
});

The handler contract

  • Return normally and the job is acknowledged succeeded.
  • Throw and the job is acknowledged failed; the thrown error's type name, message, and stack are reported with the acknowledgement. The server decides whether to retry based on the attempt count — the worker never retries the job itself.
  • Honour ctx.signal. Cancellation (timeout, shutdown, or a server cancel) takes effect only when your handler observes the signal. Pass it to every async call and check ctx.signal.aborted in long loops.
  • Be idempotent. Delivery is at-least-once. Guard side effects with a dedup check keyed on ctx.jobId so a re-delivery doesn't double-apply.
const chargeCard = defineJob<ChargeRequest>({
jobType: "ChargeCard",
maxAttempts: 5,
timeoutSeconds: 30,
async handle(payload, ctx) {
if (await chargeAlreadyRecorded(ctx.jobId)) return; // at-least-once guard

const result = await gateway.charge(payload, { signal: ctx.signal });
await recordCharge(ctx.jobId, result);
},
});

See also