Workbench

The dashboard — served by your worker, or mounted in your own app.

Workbench is OpenQueue's dashboard: queue overview with live counters and latency percentiles, a virtualized runs table with payloads and logs, flow graphs, schedule management, error triage grouped by class, and a test console for enqueueing jobs by hand.

Served by the worker

The zero-effort path — enable it in your config and the worker hosts it:

worker.config.ts
export default defineConfig({
  // …
  workbench: {
    enabled: true,
    title: 'Jobs',
    basePath: '/workbench',
    auth: {
      username: process.env.WORKBENCH_USER!,
      password: process.env.WORKBENCH_PASSWORD!,
    },
  },
});

Mounted in Next.js

Serve the dashboard from the app you already deploy, behind your existing auth. Create a catch-all route:

app/admin/jobs/[[...workbench]]/route.ts
import { workbench } from '@openqueue/workbench/next';

export const { GET, POST, PUT, PATCH, DELETE } = workbench({
  redis: { url: process.env.REDIS_URL! },
  basePath: '/admin/jobs',
});

Leave basePath unset when the route lives at the app root.

Mounted in h3

import { createWorkbenchApp } from '@openqueue/workbench/h3';

const dashboard = await createWorkbenchApp({
  redis: { url: process.env.REDIS_URL! },
});

app.mount('/workbench', dashboard);

When redis is provided without an explicit queue list, Workbench discovers queues automatically from the published queue catalog.

Mounted in other hosts

The dashboard is a plain fetch handler, so it mounts into any fetch-native host — no shipped adapter needed. createFetchHandler returns a .fetch(request); createWorkbenchApp returns an H3 app you can hand to h3's Node adapter.

// Hono — mount the fetch handler under a prefix
import { createFetchHandler } from '@openqueue/workbench';

honoApp.mount(
  '/admin/jobs',
  createFetchHandler({ redis: { url: process.env.REDIS_URL! }, basePath: '/admin/jobs' })
    .fetch,
);
// Express — h3's Node adapter bridges the app onto a middleware
import { toNodeHandler } from 'h3';
import { createWorkbenchApp } from '@openqueue/workbench/h3';

const app = await createWorkbenchApp({ redis: { url: process.env.REDIS_URL! } });
expressApp.use('/admin/jobs', toNodeHandler(app));

Read-only mode

Set readonly: true to expose the dashboard to a wider audience without retry/enqueue/pause actions — useful for support teams and stakeholders.

On non-BullMQ worlds

Workbench can mount on a worker backed by a non-BullMQ world (a world in the config instead of redis). Much of it still works; the parts that read BullMQ directly degrade gracefully rather than erroring. This matrix is the code truth:

  • Fully works: the shell, config, and auth; the test console (enqueues through the registry → runtime.trigger); dynamic schedule CRUD; alert contact-point and rule CRUD (persisted in the world's own store).
  • Inert: alert firing — it evaluates BullMQ queue metrics, which a non-BullMQ world doesn't expose, so rules never trigger.
  • Empty: the Runs page reads BullMQ, not the core run store, so run history for these worlds lives on the control API (/openqueue/v1) and the client, not the dashboard. Overview, counts, metrics, errors, activity, search, and tags render as zeros; queue pages, job detail/logs/spans, the retry/remove/promote/clean/bulk/pause actions, repeatable schedulers, and the Prometheus endpoint all read raw BullMQ and stay empty.

When the worker mounts Workbench on a world with no BullMQ queues it logs a one-line reminder pointing at /openqueue/v1 for run history.

Prior art

Hat tip to pontusab/workbench, the BullMQ dashboard that set the bar for what queue tooling should feel like. OpenQueue ships that caliber of dashboard as one piece of a larger machine — tasks, retries, flows, schedules, and persistence, designed together with the UI rather than bolted on next to it.

On this page