Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Quick Start Example

Let's walk through a practical example of setting up a simple job queue. We'll create a task that sends a welcome email when a user signs up.

You might wonder: "Why not just send the email directly in the afterChange hook?"

  • Non-blocking: If your email service takes 2-3 seconds to send, your API response would be delayed. With jobs, the API returns immediately.
  • Resilience: If the email service is temporarily down, the hook would fail and potentially block the user creation. Jobs can retry automatically.
  • Scalability: As your app grows, you can move job processing to dedicated servers, keeping your API fast.
  • Monitoring: All jobs are tracked in the database, so you can see if emails failed and why.

Now let's build this example step by step.

Step 1: Define a Task

First, create a task in your payload.config.ts:

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ... other config
5
jobs: {
6
tasks: [
7
{
8
slug: 'sendWelcomeEmail',
9
retries: 3,
10
inputSchema: [
11
{
12
name: 'userEmail',
13
type: 'email',
14
required: true,
15
},
16
{
17
name: 'userName',
18
type: 'text',
19
required: true,
20
},
21
],
22
handler: async ({ input, req }) => {
23
// Send email using your email service
24
await req.payload.sendEmail({
25
to: input.userEmail,
26
subject: 'Welcome!',
27
text: `Hi ${input.userName}, welcome to our platform!`,
28
})
29
30
return {
31
output: {
32
emailSent: true,
33
},
34
}
35
},
36
},
37
],
38
},
39
})

This defines a reusable task with a unique slug, an inputSchema that validates and types the input data, and a handler function containing the work to be performed. The retries option ensures the task will automatically retry up to 3 times if it fails. Learn more about Tasks.

Step 2: Queue the Job trigger

1
{
2
slug: 'users',
3
hooks: {
4
afterChange: [
5
async ({ req, doc, operation }) => {
6
// Only send welcome email for new users
7
if (operation === 'create') {
8
await req.payload.jobs.queue({
9
task: 'sendWelcomeEmail',
10
input: {
11
userEmail: doc.email,
12
userName: doc.name,
13
},
14
})
15
}
16
},
17
],
18
},
19
// ... fields
20
}

This uses payload.jobs.queue() to create a job instance from the task definition. The job is added to the queue immediately but runs asynchronously, so the API response returns right away without waiting for the email to send. Jobs are stored in the database as documents in the payload-jobs collection.

Step 3: Run the Jobs

1
export default buildConfig({
2
// ... other config
3
jobs: {
4
tasks: [
5
/* ... */
6
],
7
autoRun: [
8
{
9
cron: '*/5 * * * *', // Run every 5 minutes
10
},
11
],
12
},
13
})

The autoRun configuration automatically processes queued jobs on a schedule using cron syntax. In this example, Payload checks for pending jobs every 5 minutes and executes them. Alternatively, you can manually trigger job processing with payload.jobs.run() or use the API endpoint for serverless platforms.

That's it! Now when users sign up, a job is queued and will be processed within 5 minutes without blocking the API response.

Example 2: Recurring Scheduled Job

The previous example showed manual job queuing (jobs triggered by user actions). Now let's look at a job that runs automatically on a schedule without any user action.

We'll create a task that generates a daily analytics report every morning at 8 AM.

Why Use Scheduled Jobs?

  • Automated recurring tasks: No need to manually trigger them
  • Predictable timing: Reports, cleanups, syncs run at exact times
  • No manual intervention: Set it once and forget it

Step 1: Define a Task with Schedule

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ... other config
5
jobs: {
6
tasks: [
7
{
8
slug: 'generateDailyReport',
9
10
// This automatically queues a job every day at 8 AM
11
schedule: [
12
{
13
cron: '0 8 * * *', // 8:00 AM daily
14
queue: 'reports', // Put it in the 'reports' queue
15
},
16
],
17
18
inputSchema: [],
19
20
outputSchema: [
21
{
22
name: 'reportId',
23
type: 'text',
24
},
25
],
26
27
handler: async ({ req }) => {
28
// Generate the report
29
const yesterday = new Date()
30
yesterday.setDate(yesterday.getDate() - 1)
31
32
const analytics = await req.payload.find({
33
collection: 'analytics',
34
where: {
35
createdAt: {
36
greater_than_equal: yesterday.toISOString(),
37
},
38
},
39
})
40
41
// Save the report
42
const report = await req.payload.create({
43
collection: 'reports',
44
data: {
45
date: new Date().toISOString(),
46
totalEvents: analytics.totalDocs,
47
summary: `Generated report for ${yesterday.toDateString()}`,
48
},
49
})
50
51
return {
52
output: {
53
reportId: report.id,
54
},
55
}
56
},
57
},
58
],
59
},
60
})

The schedule property defines when this job should run (every day at 8 AM).

Step 2: Configure the Job Runner

To actually queue and execute scheduled jobs, you need to configure the autoRun property. This handles both queuing jobs based on their schedule and running them:

1
export default buildConfig({
2
// ... other config
3
jobs: {
4
tasks: [
5
/* task from step 1 */
6
],
7
8
// This processes jobs from the 'reports' queue
9
autoRun: [
10
{
11
cron: '* * * * *', // Check every minute
12
queue: 'reports', // Process 'reports' queue
13
limit: 10,
14
},
15
],
16
},
17
})

How It Works

Here's the complete flow:

  1. At 8:00 AM: The schedule configuration automatically queues a new job in the 'reports' queue
  2. Within 1 minute: The autoRun cron checks the 'reports' queue and finds the job
  3. Execution: The job runs and generates the report
  4. The next day: The process repeats automatically at 8:00 AM

Complete Configuration

Here's the full config with both the task and runner:

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ... other config
5
jobs: {
6
tasks: [
7
{
8
slug: 'generateDailyReport',
9
schedule: [
10
{
11
cron: '0 8 * * *',
12
queue: 'reports',
13
},
14
],
15
handler: async ({ req }) => {
16
// Report generation logic
17
const report = await generateReport()
18
return { output: { reportId: report.id } }
19
},
20
},
21
],
22
autoRun: [
23
{
24
cron: '* * * * *',
25
queue: 'reports',
26
limit: 10,
27
},
28
],
29
},
30
})

When to Use Each Approach

Approach

When to Use

Example

Manual Queuing

Jobs triggered by user actions or data changes

Welcome emails, payment processing, notifications

Scheduled Jobs

Jobs that run automatically at regular intervals

Daily reports, weekly cleanups, nightly syncs

Scheduled with Future

One-time job in the future

Publish post at 3pm tomorrow, trial expiry reminders

For scheduled jobs with waitUntil:

1
// Queue a one-time job for the future
2
await payload.jobs.queue({
3
task: 'publishPost',
4
input: { postId: '123' },
5
waitUntil: new Date('2024-12-25T15:00:00Z'), // Runs once at this time
6
})

This is different from the schedule property, which repeats automatically.

See Job Schedules for more details on scheduling options and advanced patterns.

Was this page helpful?

Next

Tasks