queue()
The queue builder, its options, and how a queue key becomes a route.
queue(ref, options) registers one BullMQ queue as a nav entry and a page in
the admin. FlowPanel does not implement a queue UI of its own — the page
embeds bull-board in an iframe. For
the security consequences of that split, read
Queues first; this page is the type surface.
Register a BullMQ queue in your FlowPanel admin.
export function queue(ref: unknown, options: QueueOptions): QueueConfig;import { defineAdmin, queue } from "@flowpanel/kit";
import { emailQueue } from "@/lib/queues";
export default defineAdmin({
// …adapter, auth…
queues: [
queue(emailQueue, {
label: "Email",
boardUrl: `http://localhost:3001/queue/email?token=${process.env.BOARD_TOKEN}`,
requireRole: "admin",
}),
],
});ref is typed unknown on purpose: FlowPanel never calls into BullMQ itself,
so it does not need the Queue type and does not force bullmq into your
type graph. The only thing it reads off the ref is .name, and only when
options.key is absent.
QueueOptions
Prop
Type
requireRole here is string | string[] — no predicate, unlike a
resource's or a dashboard's. That is the same restriction actions carry.
QueueConfig
What queue() returns. You pass it to defineAdmin; you rarely read it.
Prop
Type
The key, and the URL it produces
key defaults to ref.name — the name you gave the BullMQ Queue. It is
what the admin's own route uses:
<basePath>/queues/<key>basePath already carries its leading slash, so at the default the URL is
/admin/queues/email, and under basePath: "/internal/admin" it is
/internal/admin/queues/email.
Three ways defineAdmin rejects a queue:
| Condition | Error |
|---|---|
No .name on the ref and no options.key | queue() requires options.key when the queue has no .name |
| Two queues resolve to the same key | Duplicate queue key: "<key>". Each queue key must be unique. |
The key is dashboards, drawer or queues | queue "<key>" uses a name FlowPanel's routing reserves… |
The third is the same reserved-segment rule resources are held to — those
three segments are matched by the admin's own routing before it ever looks at
your names. Set options.key to something else.
Reading queue state from your own code
queue() only mounts the page. To count jobs for a dashboard widget, wrap the
same Queue instances with bullmqAdapter and read through the returned map:
import { bullmqAdapter } from "@flowpanel/kit/bullmq";
import { emailQueue, scrapeQueue } from "@/lib/queues";
export const jobs = bullmqAdapter({ email: emailQueue, scrape: scrapeQueue });import { metric } from "@flowpanel/kit";
import { jobs } from "@/lib/queue-adapter";
metric("Failed jobs", async () => jobs.queues.email.getJobCountByTypes("failed"), {
tone: "err",
drilldown: "/admin/queues/email",
});BullMQAdapter is { kind: "bullmq"; queues: Record<string, Queue> } — a
named map and nothing more. BullMQAdapterOptions is exported for symmetry
with the other adapters; bullmqAdapter takes the map directly, so nothing
consumes it.
Peer dependencies
bullmq and ioredis are optional peers, as are @bull-board/api,
@bull-board/express and express for @flowpanel/kit/bullmq/board. Install
them only if you use queues:
pnpm add bullmq ioredis
pnpm add @bull-board/api @bull-board/express express