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";
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.
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 |
db | Database instance |
Email Templates
Jobs often send emails using templates defined in emails/:
import { email } from "questpie";
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 { pgBossAdapter, runtimeConfig } from "questpie";
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";
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,
}),
),
);
},
});