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.

Jobs

Now that we have covered Tasks and Workflows, we can tie them together with a concept called a Job.

For example, let's say we have a Workflow or Task that describes the logic to sync information from Payload to a third-party system. This is how you'd declare how to sync that info, but it wouldn't do anything on its own. In order to run that task or workflow, you'd create a Job that references the corresponding Task or Workflow.

Jobs are stored in the Payload database in the payload-jobs collection, and you can decide to keep a running list of all jobs, or configure Payload to delete the job when it has been successfully executed.

Queuing a new job

In order to queue a job, you can use the payload.jobs.queue function.

Here's how you'd queue a new Job, which will run a createPostAndUpdate workflow:

1
const createdJob = await payload.jobs.queue({
2
// Pass the name of the workflow
3
workflow: 'createPostAndUpdate',
4
// The input type will be automatically typed
5
// according to the input you've defined for this workflow
6
input: {
7
title: 'my title',
8
},
9
})

In addition to being able to queue new Jobs based on Workflows, you can also queue a job for a single Task:

1
const createdJob = await payload.jobs.queue({
2
task: 'createPost',
3
input: {
4
title: 'my title',
5
},
6
})

Where to Queue Jobs

Jobs can be queued from anywhere in your application. Here are the most common scenarios:

From Collection Hooks

The most common place - queue jobs in response to document changes:

1
{
2
slug: 'posts',
3
hooks: {
4
afterChange: [
5
async ({ req, doc, operation }) => {
6
// Only send notification for published posts
7
if (operation === 'update' && doc.status === 'published') {
8
await req.payload.jobs.queue({
9
task: 'notifySubscribers',
10
input: {
11
postId: doc.id,
12
},
13
})
14
}
15
},
16
],
17
},
18
}

From Field Hooks

Queue jobs based on specific field changes:

1
{
2
name: 'featuredImage',
3
type: 'upload',
4
relationTo: 'media',
5
hooks: {
6
afterChange: [
7
async ({ req, value, previousValue }) => {
8
// Generate image variants when image changes
9
if (value !== previousValue) {
10
await req.payload.jobs.queue({
11
task: 'generateImageVariants',
12
input: {
13
imageId: value,
14
},
15
})
16
}
17
},
18
],
19
},
20
}

From Custom Endpoints

Queue jobs from your API routes:

1
export const POST = async (req: PayloadRequest) => {
2
const job = await req.payload.jobs.queue({
3
workflow: 'generateMonthlyReport',
4
input: {
5
month: new Date().getMonth(),
6
year: new Date().getFullYear(),
7
},
8
})
9
10
return Response.json({
11
message: 'Report generation queued',
12
jobId: job.id,
13
})
14
}

From Server Actions

Queue jobs from Next.js server actions:

1
'use server'
2
3
import { getPayload } from 'payload'
4
import config from '@payload-config'
5
6
export async function scheduleEmail(userId: string) {
7
const payload = await getPayload({ config })
8
9
await payload.jobs.queue({
10
task: 'sendEmail',
11
input: { userId },
12
})
13
}

Job Options

When queuing a job, you can pass additional options:

1
await payload.jobs.queue({
2
task: 'sendEmail',
3
input: { userId: '123' },
4
5
// Schedule the job to run in the future
6
waitUntil: new Date('2024-12-25T00:00:00Z'),
7
8
// Assign to a specific queue
9
queue: 'high-priority',
10
11
// Add custom metadata for tracking
12
log: [
13
{
14
message: 'Email queued by admin',
15
createdAt: new Date().toISOString(),
16
},
17
],
18
})

Common options

  • waitUntil - Schedule the job to run at a specific date/time in the future
  • queue - Assign the job to a specific queue (defaults to 'default')
  • log - Add custom log entries for debugging or tracking
  • req - Pass the request context for access control

Check Job Status

After queuing a job, you can check its status:

1
const job = await payload.jobs.queue({
2
task: 'processPayment',
3
input: { orderId: '123' },
4
})
5
6
// Later, check the job status
7
const updatedJob = await payload.findByID({
8
collection: 'payload-jobs',
9
id: job.id,
10
})
11
12
console.log(updatedJob.completedAt) // When it finished
13
console.log(updatedJob.hasError) // If it failed
14
console.log(updatedJob.taskStatus) // Details of each task

Job Status Fields

Each job document contains:

1
{
2
id: 'job_123',
3
taskSlug: 'sendEmail', // Or workflowSlug for workflows
4
input: { userId: '123' }, // The input you provided
5
completedAt: '2024-01-15...', // When job completed (null if pending)
6
hasError: false, // True if job failed
7
totalTried: 1, // Number of attempts
8
processing: false, // True if currently running
9
taskStatus: { // Status of each task (for workflows)
10
sendEmail: {
11
'1': {
12
complete: true,
13
output: { emailSent: true }
14
}
15
}
16
},
17
log: [ // Execution log
18
{
19
message: 'Job started',
20
createdAt: '...'
21
}
22
]
23
}

Access Control

By default, Payload's job operations bypass access control when used from the Local API. You can enable access control by passing overrideAccess: false to any job operation.

To define custom access control for jobs, add an access property to your Jobs Config:

1
import type { SanitizedConfig } from 'payload'
2
3
const config: SanitizedConfig = {
4
// ...
5
jobs: {
6
access: {
7
// Control who can queue new jobs
8
queue: ({ req }) => {
9
return req.user?.roles?.includes('admin')
10
},
11
// Control who can run jobs
12
run: ({ req }) => {
13
return req.user?.roles?.includes('admin')
14
},
15
// Control who can cancel jobs
16
cancel: ({ req }) => {
17
return req.user?.roles?.includes('admin')
18
},
19
},
20
},
21
}

Each access control function receives the current req object and should return a boolean. If no access control is defined, the default behavior allows any authenticated user to perform the operation.

To use access control in the Local API:

1
const req = await createLocalReq({ user }, payload)
2
3
await payload.jobs.queue({
4
workflow: 'createPost',
5
input: { title: 'My Post' },
6
overrideAccess: false, // Enable access control
7
req, // Pass the request with user context
8
})

Cancelling Jobs

Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.

To cancel a specific job, use the payload.jobs.cancelByID method with the job's ID:

1
await payload.jobs.cancelByID({
2
id: createdJob.id,
3
})

To cancel multiple jobs at once, use the payload.jobs.cancel method with a Where query:

1
await payload.jobs.cancel({
2
where: {
3
workflowSlug: {
4
equals: 'createPost',
5
},
6
},
7
})
Next

Queues