NotusBase

queue

Work queue: a limit on how many tasks run together, retries with growing backoff and a notification on completion. It combines what @convex-dev/workpool and @convex-dev/action-retrier do, on top of ctx.scheduler.

Enabling

// convex/schema.ts
import { queueTables } from "notusbase-modules/queue/schema";
export default defineSchema({ ...queueTables, /* yours */ });
// convex/queue.ts
export * from "notusbase-modules/queue";

Using

import { Queue, vOnCompleteArgs } from "notusbase-modules/queue";
import { internal } from "./_generated/api";

const emails = new Queue("emails", { maxParallelism: 5, retry: true });

export const send = mutation({
  args: { to: v.string() },
  handler: async (ctx, { to }): Promise<string> => {
    return emails.enqueueAction(ctx, internal.email.send, { to }, {
      onComplete: internal.email.sent,
      context: { to },
    });
  },
});

export const sent = internalMutation({
  args: vOnCompleteArgs(v.object({ to: v.string() })),
  handler: async (ctx, { workId, context, result }) => {
    if (result.kind === "failed") console.error(context.to, result.error);
  },
});
MethodRuns inWhat it does
enqueueAction(ctx, fn, args, options)mutation, actionEnqueues an action and returns the id
enqueueMutation(ctx, fn, args, options)mutation, actionEnqueues a mutation
status(ctx, id)query, mutation, actionpending, running, success, failed or canceled, with attempts
cancel(ctx, id)mutation, actionA pending task leaves right away. One that has already started finishes, with no retry
cancelAll(ctx)mutation, actionCancels the queue's pending tasks

Constructor options: maxParallelism (default 10), retry, sweepMs (default 30 s, sweep for lost tasks), retentionMs (default 1 day, how long a finished task stays in the table). Parallelism belongs to the queue: use the same value everywhere that names it, because the last call's value wins.

enqueue* options: retry, onComplete, context, runAfter, runAt.

retry is true ({ maxAttempts: 5, initialBackoffMs: 250, base: 2 }), false or an object with those fields, counting the first execution. The wait is initialBackoffMs * base^(failures - 1), multiplied by a number between 0.5 and 1.5. Without retry, it runs once.

onComplete receives { workId, context, result }, with result as { kind: "success", returnValue }, { kind: "failed", error } or { kind: "canceled" }.

Enqueueing is part of the caller's transaction: if the mutation rolls back, the task never existed.

Guarantees

Disabling

Remove convex/queue.ts, the Queue instances in the code and ...queueTables from the schema. The queue_tasks and queue_state tables stay. With the module gone, the queue's jobs fail because they cannot find the function, and pending tasks wait. When you enable it again, they run when the first new task is enqueued.

How it differs from the original

Depends on the scheduler

Everything here goes through jobs.go: