Structured Notes
Attach a bounded, validated object of annotations to a job — capped at 15 fields and 4 KiB, rendered by the dashboard.
Attach a bounded, validated object of annotations to a job — capped at 15 fields and 4 KiB, rendered by the dashboard.
notes is a structured annotation field on every job: a small object (at most
15 top-level keys, 4096 bytes encoded) you attach at enqueue time and
read back from the job. Unlike metadata — a free-form JSON string you encode
yourself — notes is validated before the job is written and rendered by the
dashboard as a key/value table.
notes vs metadatanotes | metadata | |
|---|---|---|
| Type at enqueue | Record<string, unknown> | string (pre-encoded JSON) |
| Top-level fields | ≤ 15 | unbounded |
| Encoded size | ≤ 4 KiB | unbounded |
| Validation | Before enqueue | None |
| Dashboard render | Key/value table | Raw JSON |
| Survives DLQ + replay | Yes | Yes |
| Best for | Short, human-readable annotations | Opaque operational context |
Reach for notes when a person (or the dashboard) will read the value:
customer ids, tenant names, a short reason. Reach for metadata when you
need to carry an opaque blob with no size ceiling — trace envelopes, request
context.
Pass an object as the notes enqueue option:
const id = queue.enqueue("processOrder", [order], {
notes: {
customerId: "cus_abc",
tier: "gold",
priorityReason: "VIP onboarding",
},
});enqueueMany accepts it per entry, and publish accepts it too — there, the
notes are stamped on every delivery of the message:
queue.enqueueMany("resize", [
{ args: ["a.png"], options: { notes: { album: "spring" } } },
{ args: ["b.png"], options: { notes: { album: "summer" } } },
]);
queue.publish("orders.created", [order], { notes: { region: "eu-west" } });Validation runs synchronously, before anything reaches the core — an invalid object throws rather than producing a job you have to clean up later.
A Job carries the notes as the raw canonical JSON string the core stored, so
parse it when you need the object:
const job = queue.getJob(id);
const notes = job?.notes ? (JSON.parse(job.notes) as Record<string, unknown>) : undefined;
notes?.customerId; // "cus_abc"Pub/sub deliveries are the exception: TopicMessage.notes is already parsed
into an object for you.
The contract lives in encodeNotes (src/notes.ts) and is enforced at enqueue:
| Constraint | Limit |
|---|---|
| Max top-level fields | 15 |
| Max encoded size | 4096 bytes (UTF-8) |
| Value types | Anything JSON.stringify accepts |
Violations throw NotesValidationError, and the message names the constraint
that failed:
import { NotesValidationError } from "@byteveda/flexiq";
try {
queue.enqueue("processOrder", [order], { notes: tooManyFields });
} catch (error) {
if (error instanceof NotesValidationError) {
// "notes: at most 15 top-level fields (got 20)"
}
}Values that JSON.stringify cannot represent — circular references, BigInt —
also throw NotesValidationError rather than a raw TypeError, so one catch
covers every notes failure. Nested objects and arrays are allowed; only the
top-level key count and the encoded byte size are bounded.
NotesValidationError extends
FlexiQError, so an enclosing
catch (e) { if (e instanceof FlexiQError) … } catches it too.
The dashboard surfaces notes on the job detail view as a key/value table beside the metadata card. The 15-field cap keeps that table scannable without scrolling.
notes is a first-class column on the jobs, dead_letter, and
archived_jobs tables on SQLite and PostgreSQL, and rides along with the job's
JSON representation on Redis. Notes survive dead-letter moves and retries — the
original notes are restored on the re-enqueued job — plus replay and archival.