| name | wasp-jobs |
| description | Background jobs with PgBoss for Wasp applications. Use when implementing async tasks, scheduled jobs, email queues, or background processing. Requires PostgreSQL database. |
| triggers | ["background job","scheduled task","cron","job","email queue","async task","PgBoss"] |
| version | 1 |
| last_updated | "2025-10-18T00:00:00.000Z" |
| allowed_tools | ["Edit","Bash","Read"] |
Wasp Background Jobs Skill
Quick Reference
When to use this skill:
- Sending emails asynchronously
- Processing data in background
- Scheduled/recurring tasks
- Long-running operations
- Queue-based processing
Critical Requirements
MUST use PostgreSQL - PgBoss requires PostgreSQL (SQLite not supported)
// schema.prisma
datasource db {
provider = "postgresql" // ✅ Required for jobs
url = env("DATABASE_URL")
}
Complete Job Setup Workflow
1. Define Job in main.wasp
job emailSender {
executor: PgBoss, // Requires PostgreSQL
perform: {
fn: import { sendEmail } from "@src/jobs/emailSender.js"
},
entities: [User, EmailQueue] // Entities needed in job
}
Job options:
job myJob {
executor: PgBoss,
perform: {
fn: import { myJobFunction } from "@src/jobs/myJob.js"
},
entities: [User, Task],
schedule: {
cron: "0 0 * * *", // Daily at midnight
args: {=json { "foo": "bar" } json=} // Optional default args
}
}
2. Implement Job Function
File: src/jobs/emailSender.js
import type { EmailSender } from "wasp/server/jobs";
type EmailArgs = {
to: string;
subject: string;
body: string;
};
export const sendEmail: EmailSender<EmailArgs> = async (args, context) => {
const user = await context.entities.User.findUnique({
where: { email: args.to },
});
if (!user) {
console.error("User not found:", args.to);
return { success: false, error: "User not found" };
}
try {
console.log(`Sending email to ${args.to}`);
console.log(`Subject: ${args.subject}`);
console.log(`Body: ${args.body}`);
return { success: true };
} catch (error) {
console.error("Email send failed:", error);
throw error;
}
};
3. Trigger Job Programmatically
From an action:
import { emailSender } from "wasp/server/jobs";
import type { SendWelcomeEmail } from "wasp/server/operations";
export const sendWelcomeEmail: SendWelcomeEmail = async (args, context) => {
if (!context.user) throw new HttpError(401);
await emailSender.submit({
to: context.user.email,
subject: "Welcome!",
body: "Thanks for signing up!",
});
return { message: "Email queued" };
};
With delay:
await emailSender.submit(
{ to: "user@example.com", subject: "Reminder", body: "Don't forget!" },
{ startAfter: new Date(Date.now() + 3600000) },
);
With retry configuration:
await emailSender.submit(emailArgs, {
retryLimit: 5,
retryDelay: 60,
retryBackoff: true,
});
Scheduling Patterns
Cron-Based Scheduling
job dailyReport {
executor: PgBoss,
perform: {
fn: import { generateDailyReport } from "@src/jobs/reports.js"
},
schedule: {
cron: "0 9 * * 1-5", // 9 AM on weekdays
args: {=json { "reportType": "daily" } json=}
}
}
Common cron patterns:
"0 * * * *" - Every hour
"0 0 * * *" - Daily at midnight
"0 9 * * 1-5" - 9 AM on weekdays
"0 0 1 * *" - First day of month
"*/15 * * * *" - Every 15 minutes
Programmatic Scheduling
import { myJob } from "wasp/server/jobs";
await myJob.submit(args, {
startAfter: new Date("2025-12-01T10:00:00Z"),
});
await myJob.submit(args, {
retryLimit: 3,
retryDelay: 3600,
expireInHours: 24,
});
Job Patterns
Email Queue Pattern
export const queueEmail = async (args, context) => {
if (!context.user) throw new HttpError(401);
const emailRecord = await context.entities.EmailQueue.create({
data: {
to: args.to,
subject: args.subject,
body: args.body,
status: "PENDING",
},
});
await emailSender.submit({
emailId: emailRecord.id,
});
return emailRecord;
};
export const emailSender = async (args, context) => {
const email = await context.entities.EmailQueue.findUnique({
where: { id: args.emailId },
});
if (!email) return { success: false };
try {
await sendEmailViaService(email);
await context.entities.EmailQueue.update({
where: { id: args.emailId },
data: { status: "SENT", sentAt: new Date() },
});
return { success: true };
} catch (error) {
await context.entities.EmailQueue.update({
where: { id: args.emailId },
data: { status: "FAILED", error: error.message },
});
throw error;
}
};
Batch Processing Pattern
job processBatchUsers {
executor: PgBoss,
perform: {
fn: import { processBatch } from "@src/jobs/batchProcessor.js"
},
entities: [User],
schedule: {
cron: "0 2 * * *"
}
}
export const processBatch = async (args, context) => {
const batchSize = 100
let offset = 0
let processedCount = 0
while (true) {
const users = await context.entities.User.findMany({
skip: offset,
take: batchSize,
where: { needsProcessing: true }
})
if (users.length === 0) break
for (const user of users) {
await processUser(user, context)
processedCount++
}
offset += batchSize
console.log(`Processed ${processedCount} users`)
}
return { processedCount }
}
Error Handling
Job-Level Error Handling
export const myJob = async (args, context) => {
try {
await doWork(args, context);
return { success: true };
} catch (error) {
console.error("Job failed:", error);
await context.entities.JobLog.create({
data: {
jobName: "myJob",
args: JSON.stringify(args),
error: error.message,
stack: error.stack,
},
});
throw error;
}
};
Dead Letter Queue
export const processDeadLetterQueue = async (args, context) => {
const failedJobs = await context.entities.JobLog.findMany({
where: {
status: "FAILED",
retries: { gte: 5 },
},
});
for (const job of failedJobs) {
await notifyAdmin({
subject: "Job permanently failed",
job: job.jobName,
args: job.args,
error: job.error,
});
}
};
PostgreSQL Setup
Local Development
brew install postgresql
brew services start postgresql
createdb myapp_dev
sudo apt-get install postgresql
sudo systemctl start postgresql
sudo -u postgres createdb myapp_dev
.env.server
DATABASE_URL="postgresql://username:password@localhost:5432/myapp_dev"
schema.prisma
datasource db {
provider = "postgresql" // Required for PgBoss
url = env("DATABASE_URL")
}
Common Job Errors
Error: PgBoss requires PostgreSQL
Cause: Using SQLite as database provider
Fix:
// Change in schema.prisma
datasource db {
provider = "postgresql" // Not "sqlite"
url = env("DATABASE_URL")
}
Error: Job not defined
Cause: Forgot to restart wasp after adding job to main.wasp
Fix:
../scripts/safe-start.sh
Error: Cannot submit job
Cause: Job not imported correctly
Fix:
import { myJob } from "wasp/server/jobs";
import { myJob } from "@wasp/jobs";
Best Practices
✅ DO:
- Use jobs for long-running operations
- Handle errors and log failures
- Set appropriate retry limits
- Use PostgreSQL (required)
- Test jobs locally before scheduling
- Monitor job execution
- Implement dead letter queue
❌ NEVER:
- Use jobs for real-time operations
- Forget error handling
- Use SQLite (PgBoss requires PostgreSQL)
- Set infinite retries
- Skip job logging
Quick Setup Checklist
Critical Rules
Database: MUST use PostgreSQL (PgBoss requirement)
Restart: ALWAYS restart wasp after adding jobs to main.wasp
Error handling: ALWAYS handle errors in job functions
Monitoring: LOG job execution and failures
References