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
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
jobType | string | yes | — | The routing key the server uses to match an enqueued job to this handler. Must be unique within the worker. |
queue | string | no | "default" | The queue this job runs on. |
maxAttempts | number | no | 3 | How many times the server will attempt the job before giving up. |
timeoutSeconds | number | no | 1800 | Per-execution timeout. The job's signal aborts when it elapses. |
parse | (raw: unknown) => T | no | identity | Validate / transform the raw JSON payload before it reaches handle. |
handle | (payload: T, ctx) => Promise<void> | void | yes | — | Your 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 checkctx.signal.abortedin long loops. - Be idempotent. Delivery is at-least-once. Guard side effects with a dedup
check keyed on
ctx.jobIdso 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
- Recurring jobs — payloadless jobs on a cron schedule.
- Job context — everything available inside a handler.
- Configuration — wiring jobs into a
FlareWorker.