better-effect
better-effect-mq

Defining Jobs

Declare immutable queue identities, codecs and payload versions.

A queue is an inert namespace. A Job identity is the literal queue + name + version tuple; JavaScript function names are never used for persistence or routing. Defining a Job does not resolve a Service, open a connection or register a Worker.

import * as z from 'zod'
import { Codec, JobEncodeFailure, Queue, Retry } from 'better-effect-mq'
import { Schema as CoreSchema } from 'better-effect-schema'
import { Schema } from 'better-effect-schema/zod'

const SendEmailPayloadSchema = z.object({
  to: z.email(),
  messageId: z.string().min(1)
})
class SendEmailPayload extends Schema.Class<SendEmailPayload>('app/SendEmailPayload')(
  SendEmailPayloadSchema
) {}
const SendEmailPayloadCodec = Codec.standardSchema({
  schema: SendEmailPayload,
  encode: (value) =>
    CoreSchema.encode(SendEmailPayload, value).mapError(
      (error) => new JobEncodeFailure({ message: error.message, code: 'schema-encode' })
    )
})
const SendEmailFailure = z.object({ code: z.string().min(1) })

const Emails = Queue.define('emails')
const SendEmail = Emails.job('send-email', {
  version: 1,
  payload: SendEmailPayloadCodec,
  result: Codec.void,
  failure: Codec.standardSchema({ schema: SendEmailFailure }),
  defaults: {
    attempts: 5,
    backoff: Retry.exponential({
      initialDelayMs: 1_000,
      maxDelayMs: 60_000,
      maxAttempts: 5
    })
  },
  idempotencyKey: ({ messageId }) => messageId
})

Codec.standardSchema is the recommended persistence-boundary codec for a provider-backed schema. The Zod 4 schema in this example is adapted through better-effect-schema; its class value is encoded back to JSON before persistence. Primitive codecs remain convenient for primitive values. Job.PayloadInput describes the codec input; Job.Payload is the decoded value received by a handler.

Trusted JSON escape hatch

Codec.json<T>() is an intentional simple JSON escape hatch, not the default for boundary examples. Use it only when the value is already trusted and JSON-safe and there is no meaningful runtime validation to perform.

Keep a new Job version when a persisted payload or result contract changes:

The following continuation uses the provider-backed setup from Defining Jobs to encode a changed payload contract.

const SendEmailV2PayloadSchema = z.object({
  recipient: z.email(),
  body: z.string().min(1)
})
class SendEmailV2Payload extends Schema.Class<SendEmailV2Payload>('app/SendEmailV2Payload')(
  SendEmailV2PayloadSchema
) {}
const SendEmailV2PayloadCodec = Codec.standardSchema({
  schema: SendEmailV2Payload,
  encode: (value) =>
    CoreSchema.encode(SendEmailV2Payload, value).mapError(
      (error) => new JobEncodeFailure({ message: error.message, code: 'schema-encode' })
    )
})
const SendEmailV2 = Emails.job('send-email', {
  version: 2,
  payload: SendEmailV2PayloadCodec
})

A local JobRegistry can accept multiple versions for a Worker claim. It is not a global handler registry. Version changes and upcasting are application policy: keep old codecs available for records that are still in flight, and remove a version only after its persisted records are retired.

Metadata is bounded string data. Do not put connections, requests, live Error objects, lease tokens or arbitrary causes in a Job record.

On this page