Jobs
Background tasks with typed payloads — send emails, process data, schedule work.
Jobs are background tasks that run outside the request lifecycle. They're ideal for sending emails, processing data, generating reports, or any work that shouldn't block an API response.
Defining a Job
import { job } from "questpie/services";
import z from "zod";
export default job({
name: "sendAppointmentConfirmation",
schema: z.object({
appointmentId: z.string(),
customerId: z.string(),
}),
handler: async ({ payload, email, collections }) => {
const customer = await collections.user.findOne({
where: { id: payload.customerId },
});
if (!customer) return;
const appointment = await collections.appointments.findOne({
where: { id: payload.appointmentId },
with: { barber: true, service: true },
});
if (!appointment) return;
await email.sendTemplate({
template: "appointmentConfirmation",
input: {
customerName: customer.name,
appointmentId: appointment.id,
barberName: appointment.barber.name,
serviceName: appointment.service.name,
scheduledAt: appointment.scheduledAt.toISOString(),
},
to: customer.email,
});
},
});Place job files in jobs/. The filename becomes the job key: send-appointment-confirmation.ts → sendAppointmentConfirmation.
Publishing Jobs
Publish jobs from hooks, routes, or other jobs via the queue context:
.hooks({
afterChange: async ({ data, operation, queue }) => {
if (operation === "create") {
await queue.sendAppointmentConfirmation.publish({
appointmentId: data.id,
customerId: data.customer,
});
}
},
})The queue object is fully typed — autocompletion shows all available jobs and their expected payloads.
Running Workers
Publishing a job only writes it to the configured queue adapter. It does not execute the handler inside the current request. Start a separate worker process that imports the generated app and listens for queued jobs:
import { app } from "#questpie";
await app.queue.listen({
teamSize: 5,
batchSize: 3,
});Add a script next to your web app scripts:
{
"scripts": {
"dev": "bun --bun vite dev",
"dev:worker": "bun --bun run --watch ./src/worker.ts",
"worker": "bun --bun run ./src/worker.ts"
}
}Run the app and the worker as two processes:
bun run dev
bun run dev:workerIn production, deploy the HTTP app and the worker separately with the same environment variables and database connection. The HTTP app publishes jobs. The worker process consumes them and runs the job handlers. You can run multiple worker processes for horizontal scaling.
listen() registers long-running consumers and installs graceful shutdown handlers for SIGINT and SIGTERM by default. Use teamSize for per-process concurrency and batchSize for how many jobs the adapter fetches at once.
One-Off Processing
For serverless cron, tests, or maintenance scripts, process a bounded batch with runOnce() instead of starting a long-running worker:
import { app } from "#questpie";
const result = await app.queue.runOnce({
batchSize: 10,
jobs: ["sendAppointmentConfirmation"],
});
await app.queue.stop();
console.log(`Processed ${result.processed} jobs`);The jobs filter accepts either the generated registration key (sendAppointmentConfirmation) or the job definition name (sendAppointmentConfirmation, send-appointment-confirmation, etc.).
Handler Context
Job handlers receive the same AppContext as routes:
| Property | Description |
|---|---|
payload | Validated data matching the Zod schema |
collections | Typed collection API |
email | Email service |
queue | Publish other jobs |
services | User-defined services |
db | Database instance |
logger | Logger service |
Scheduled Jobs
Use startAfter when publishing a single delayed job:
await queue.sendAppointmentReminder.publish(
{ appointmentId: "appt_123" },
{ startAfter: new Date("2026-05-01T08:00:00Z") },
);Use options.cron on the job definition for recurring work:
import { job } from "questpie/services";
import z from "zod";
export default job({
name: "syncInventory",
schema: z.object({}),
options: {
cron: "*/15 * * * *",
retryLimit: 3,
},
handler: async ({ services, logger }) => {
const result = await services.inventory.sync();
logger.info({ result }, "Inventory synced");
},
});app.queue.listen() registers cron schedules automatically before starting workers. If you need a schedule-only setup step, call await app.queue.registerSchedules() and then await app.queue.stop(). Cron jobs must accept an empty payload because schedule registration validates schema.parse({}).
Email Templates
Jobs often send emails using templates defined in emails/:
import { email } from "questpie/services";
import z from "zod";
export default email({
name: "appointmentConfirmation",
schema: z.object({
customerName: z.string(),
appointmentId: z.string(),
barberName: z.string(),
serviceName: z.string(),
scheduledAt: z.string(),
}),
handler: ({ input }) => ({
subject: "Appointment Confirmed",
html: `
<h1>Hi ${input.customerName}!</h1>
<p>Your appointment has been confirmed.</p>
<table>
<tr><td>Barber</td><td>${input.barberName}</td></tr>
<tr><td>Service</td><td>${input.serviceName}</td></tr>
<tr><td>When</td><td>${input.scheduledAt}</td></tr>
</table>
`,
}),
});Queue Adapter
Configure the queue adapter in your runtime config. QUESTPIE uses pg-boss by default:
import { runtimeConfig } from "questpie/app";
import { pgBossAdapter } from "questpie/adapters/pg-boss";
export default runtimeConfig({
queue: {
adapter: pgBossAdapter({
connectionString: process.env.DATABASE_URL,
}),
},
// ...
});Real-World Example
Notifying subscribers when a blog post is published:
import { job } from "questpie/services";
import z from "zod";
export default job({
name: "notifyBlogSubscribers",
schema: z.object({
postId: z.string(),
title: z.string(),
excerpt: z.string(),
slug: z.string(),
}),
handler: async ({ payload, email, collections }) => {
const users = await collections.user.find({ limit: 1000 });
await Promise.allSettled(
users.docs.map((user) =>
email.sendTemplate({
template: "newBlogPost",
input: {
title: payload.title,
excerpt: payload.excerpt,
slug: payload.slug,
},
to: user.email,
}),
),
);
},
});Related Pages
- Hooks — Trigger jobs from collection hooks
- Typed Routes — Server-side logic
- Queue — Queue infrastructure setup
- Email — Email adapter configuration