Workhorse
Operating

Queries and timelines

Inspect any job's state, list work with bounded payloads, replay lifecycle evidence, and ask the queue how it feels.

When something goes wrong at 2 a.m., you need answers from the queue itself: what state is this job in, what happened to it, and is the system healthy? Workhorse answers all three from PostgreSQL read models that stay separate from the dispatch path. You can page through millions of terminal jobs without slowing a single worker's claim.

Construct Admin from the same caller-owned pool or executor as Queue:

const admin = new Admin(pool);

Read one known job

admin.getJob(jobId) joins the job's immutable identity to its runtime or outcome row and returns a JobSnapshot — or null when the identity is unknown or retired.

const snapshot = await admin.getJob(jobId);

if (snapshot) {
  console.log(snapshot.state, snapshot.currentAttempt, snapshot.result);
} else {
  console.log("no such job, or its identity was retired");
}

That null matters. A history query alone cannot distinguish "never existed" from "existed and was cleaned up", so use getJob whenever existence is the question.

List jobs with bounded payloads

admin.listJobs(query) filters by queue, type, states, tags, and creation time. It orders pages newest-first by immutable creation keys and returns a signed cursor for the next call.

const page = await admin.listJobs({
  queue: "billing",
  states: ["active", "failed"],
  payload: {
    include: true,
    maxBytes: 4_096,
    redactKeys: ["cardNumber", "accessToken"],
  },
});

for (const job of page.items) {
  console.log(job.id, job.state, job.payloadStatus, job.payload);
}

if (page.nextCursor) {
  const next = await admin.listJobs({
    queue: "billing",
    states: ["active", "failed"],
    payload: { include: true, maxBytes: 4_096, redactKeys: ["cardNumber", "accessToken"] },
    cursor: page.nextCursor,
  });
}

Payloads are omitted by default because operator lists rarely need them. When you opt in, PostgreSQL redacts the top-level keys you name, enforces the size ceiling, and marks each row's payloadStatus as included, omitted, or too_large. Sensitive data never has to leave the database just to render a list.

Two rules keep paging honest:

  • A cursor is signed against the filters and payload projection that created it. Reusing it with a different query fails loudly instead of silently changing page membership.
  • Pages are weakly consistent across calls. Jobs enqueued or transitioned between calls can shift later pages, so confirm any individual job with getJob.

Reconstruct what happened

admin.getJobTimeline(jobId, query) merges retained job_event rows and closed attempt_history rows into one newest-first stream, with its own cursor for long histories.

const timeline = await admin.getJobTimeline(jobId, { limit: 50 });

for (const entry of timeline.items) {
  if (entry.kind === "event") {
    console.log(entry.occurredAt, entry.eventType, entry.details);
  } else {
    console.log(entry.occurredAt, `attempt ${entry.attempt}`, entry.outcome);
  }
}

This is the durable answer to "why did attempt 3 retry?" — each closed attempt carries its outcome, and each event carries its details. One caveat: history categories retire on independent retention windows, so a known job can have a partial or empty timeline. Absence of timeline is never proof of absence of the job.

For the other durable projections, admin.getCheckpoint, listCheckpoints, getWait, listWaits, and getProgress expose checkpoints, durable waits, and progress without interpreting them as lifecycle state.

Ask the queue how it feels

queue.health() reads every correctness-sensitive value in a single SQL statement, so the snapshot describes one instant — reported as capturedAt. It then evaluates the snapshot against database-owned budgets and returns a verdict.

const health = await queue.health();

if (health.status.level !== "healthy") {
  for (const reason of health.status.reasons) {
    console.warn(reason.code, reason.observed, reason.budget);
  }
}

status.level is healthy, degraded, or critical. Each exceeded budget produces a reason with a stable machine-readable code and the observed value, so automation branches on codes instead of parsing prose. Exact counts live at the top level of QueueHealth; values that come from PostgreSQL's own lagging statistics live under observations, where lag is expected. The applied policy lives under budgets, so callers can explain the threshold without carrying process-local configuration.

The workhorse health --json command runs the identical evaluation and exits non-zero on any exceeded budget. It is ready for probes and cron checks. For continuous export instead of polling, see WorkhorseMetricsObserver in Maintenance and retention.

Next


Exact query fields, cursor binding, payload controls, and health measures: architecture reference.