Agent skill

wasp-jobs

Background jobs with PgBoss for Wasp applications. Use when implementing async tasks, scheduled jobs, email queues, or background processing. Requires PostgreSQL database.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/wasp-jobs

SKILL.md

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)

prisma
// schema.prisma
datasource db {
  provider = "postgresql"  // ✅ Required for jobs
  url      = env("DATABASE_URL")
}

Complete Job Setup Workflow

1. Define Job in main.wasp

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:

wasp
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

typescript
import type { EmailSender } from "wasp/server/jobs";

type EmailArgs = {
  to: string;
  subject: string;
  body: string;
};

export const sendEmail: EmailSender<EmailArgs> = async (args, context) => {
  // Access entities via 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 {
    // Send email logic here
    console.log(`Sending email to ${args.to}`);
    console.log(`Subject: ${args.subject}`);
    console.log(`Body: ${args.body}`);

    // Actual email sending would go here
    // await emailService.send(args)

    return { success: true };
  } catch (error) {
    console.error("Email send failed:", error);
    throw error; // PgBoss will retry
  }
};

3. Trigger Job Programmatically

From an action:

typescript
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);

  // Trigger job
  await emailSender.submit({
    to: context.user.email,
    subject: "Welcome!",
    body: "Thanks for signing up!",
  });

  return { message: "Email queued" };
};

With delay:

typescript
// Send email in 1 hour
await emailSender.submit(
  { to: "user@example.com", subject: "Reminder", body: "Don't forget!" },
  { startAfter: new Date(Date.now() + 3600000) }, // 1 hour delay
);

With retry configuration:

typescript
await emailSender.submit(emailArgs, {
  retryLimit: 5, // Retry up to 5 times
  retryDelay: 60, // 60 seconds between retries
  retryBackoff: true, // Exponential backoff
});

Scheduling Patterns

Cron-Based Scheduling

wasp
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

typescript
import { myJob } from "wasp/server/jobs";

// Schedule one-time job
await myJob.submit(args, {
  startAfter: new Date("2025-12-01T10:00:00Z"),
});

// Schedule recurring job (every 6 hours)
await myJob.submit(args, {
  retryLimit: 3,
  retryDelay: 3600, // 1 hour retry delay
  expireInHours: 24, // Job expires after 24 hours
});

Job Patterns

Email Queue Pattern

typescript
// Queue email for sending
export const queueEmail = async (args, context) => {
  if (!context.user) throw new HttpError(401);

  // Create email queue record
  const emailRecord = await context.entities.EmailQueue.create({
    data: {
      to: args.to,
      subject: args.subject,
      body: args.body,
      status: "PENDING",
    },
  });

  // Trigger job
  await emailSender.submit({
    emailId: emailRecord.id,
  });

  return emailRecord;
};

// Job processes queued email
export const emailSender = async (args, context) => {
  const email = await context.entities.EmailQueue.findUnique({
    where: { id: args.emailId },
  });

  if (!email) return { success: false };

  try {
    // Send email
    await sendEmailViaService(email);

    // Update status
    await context.entities.EmailQueue.update({
      where: { id: args.emailId },
      data: { status: "SENT", sentAt: new Date() },
    });

    return { success: true };
  } catch (error) {
    // Update status to failed
    await context.entities.EmailQueue.update({
      where: { id: args.emailId },
      data: { status: "FAILED", error: error.message },
    });
    throw error;
  }
};

Batch Processing Pattern

typescript
job processBatchUsers {
  executor: PgBoss,
  perform: {
    fn: import { processBatch } from "@src/jobs/batchProcessor.js"
  },
  entities: [User],
  schedule: {
    cron: "0 2 * * *"  // 2 AM daily
  }
}

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

    // Log progress
    console.log(`Processed ${processedCount} users`)
  }

  return { processedCount }
}

Error Handling

Job-Level Error Handling

typescript
export const myJob = async (args, context) => {
  try {
    // Job logic
    await doWork(args, context);
    return { success: true };
  } catch (error) {
    console.error("Job failed:", error);

    // Log to database
    await context.entities.JobLog.create({
      data: {
        jobName: "myJob",
        args: JSON.stringify(args),
        error: error.message,
        stack: error.stack,
      },
    });

    // Rethrow to trigger PgBoss retry
    throw error;
  }
};

Dead Letter Queue

typescript
// Handle jobs that failed all retries
export const processDeadLetterQueue = async (args, context) => {
  // Find failed jobs
  const failedJobs = await context.entities.JobLog.findMany({
    where: {
      status: "FAILED",
      retries: { gte: 5 },
    },
  });

  // Alert admin or take remedial action
  for (const job of failedJobs) {
    await notifyAdmin({
      subject: "Job permanently failed",
      job: job.jobName,
      args: job.args,
      error: job.error,
    });
  }
};

PostgreSQL Setup

Local Development

bash
# macOS
brew install postgresql
brew services start postgresql
createdb myapp_dev

# Linux
sudo apt-get install postgresql
sudo systemctl start postgresql
sudo -u postgres createdb myapp_dev

.env.server

bash
DATABASE_URL="postgresql://username:password@localhost:5432/myapp_dev"

schema.prisma

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:

prisma
// 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:

bash
# Ctrl+C to stop, then safe-start (multi-worktree safe)
../scripts/safe-start.sh

Error: Cannot submit job

Cause: Job not imported correctly

Fix:

typescript
// ✅ CORRECT
import { myJob } from "wasp/server/jobs";

// ❌ WRONG
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

  • Switch to PostgreSQL (if using SQLite)
  • Define job in main.wasp
  • Implement job function
  • Restart ../scripts/safe-start.sh (multi-worktree safe)
  • Test job submission
  • Add error handling
  • Set up monitoring/logging

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

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results