# Jobs

Source: https://payloadcms.com/docs/beta/jobs-queue/jobs

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

> **Default**
>
> Whereas you define Workflows and Tasks, which control your business logic, a
> **Job** is an individual instance of either a Task or a Workflow which
> contains many tasks.

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:

```ts
const createdJob = await payload.jobs.queue({
  // Pass the name of the workflow
  workflow: 'createPostAndUpdate',
  // The input type will be automatically typed
  // according to the input you've defined for this workflow
  input: {
    title: 'my title',
  },
  overrideAccess: true,
})
```

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

```ts
const createdJob = await payload.jobs.queue({
  task: 'createPost',
  input: {
    title: 'my title',
  },
  overrideAccess: true,
})
```

### 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:

```ts
{
  slug: 'posts',
  hooks: {
    afterChange: [
      async ({ req, doc, operation }) => {
        // Only send notification for published posts
        if (operation === 'update' && doc.status === 'published') {
          await req.payload.jobs.queue({
            task: 'notifySubscribers',
            input: {
              postId: doc.id,
            },
            overrideAccess: true,
          })
        }
      },
    ],
  },
}
```

#### From Field Hooks

Queue jobs based on specific field changes:

```ts
{
  name: 'featuredImage',
  type: 'upload',
  relationTo: 'media',
  hooks: {
    afterChange: [
      async ({ req, value, previousValue }) => {
        // Generate image variants when image changes
        if (value !== previousValue) {
          await req.payload.jobs.queue({
            task: 'generateImageVariants',
            input: {
              imageId: value,
            },
            overrideAccess: true,
          })
        }
      },
    ],
  },
}
```

#### From Custom Endpoints

Queue jobs from your API routes:

```ts
export const POST = async (req: PayloadRequest) => {
  const job = await req.payload.jobs.queue({
    workflow: 'generateMonthlyReport',
    input: {
      month: new Date().getMonth(),
      year: new Date().getFullYear(),
    },
    overrideAccess: false,
    req,
  })

  return Response.json({
    message: 'Report generation queued',
    jobId: job.id,
  })
}
```

#### From Server Actions

Queue jobs from Next.js server actions:

```ts
'use server'

import { headers } from 'next/headers'
import { createLocalReq, getPayload, UnauthorizedError } from 'payload'
import config from '@payload-config'

export async function scheduleEmail() {
  const payload = await getPayload({ config })
  const { user } = await payload.auth({ headers: await headers() })

  if (!user) {
    throw new UnauthorizedError()
  }

  const req = await createLocalReq({ user }, payload)

  await payload.jobs.queue({
    task: 'sendEmail',
    input: { userId: user.id },
    overrideAccess: false,
    req,
  })
}
```

### Job Options

When queuing a job, you can pass additional options:

```ts
await payload.jobs.queue({
  task: 'sendEmail',
  input: { userId: '123' },
  overrideAccess: true,

  // Schedule the job to run in the future
  waitUntil: new Date('2024-12-25T00:00:00Z'),

  // Assign to a specific queue
  queue: 'high-priority',

  // Add custom metadata for tracking
  log: [
    {
      message: 'Email queued by admin',
      createdAt: new Date().toISOString(),
    },
  ],
})
```

#### 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
- `overrideAccess` - Skip Jobs access control when set to `true` (defaults to `false`)

#### Check Job Status

After queuing a job, you can check its status:

```ts
const job = await payload.jobs.queue({
  task: 'processPayment',
  input: { orderId: '123' },
  overrideAccess: true,
})

// Later, check the job status
const updatedJob = await payload.findByID({
  collection: 'payload-jobs',
  overrideAccess: true,
  id: job.id,
})

console.log(updatedJob.completedAt) // When it finished
console.log(updatedJob.hasError) // If it failed
console.log(updatedJob.taskStatus) // Details of each task
```

#### Job Status Fields

Each job document contains:

```ts
{
  id: 'job_123',
  taskSlug: 'sendEmail',        // Or workflowSlug for workflows
  input: { userId: '123' },     // The input you provided
  completedAt: '2024-01-15...',  // When job completed (null if pending)
  hasError: false,              // True if job failed
  totalTried: 1,                // Number of attempts
  processingUntil: null,        // Future date while a worker owns the job
  processingToken: null,        // Internal token for the current owner
  taskStatus: {                 // Status of each task (for workflows)
    sendEmail: {
      '1': {
        complete: true,
        output: { emailSent: true }
      }
    }
  },
  log: [                        // Execution log
    {
      message: 'Job started',
      createdAt: '...'
    }
  ]
}
```

Payload increments `totalTried` after each completed attempt. While the handler runs, Payload automatically renews `processingUntil`. The claim also gets a `processingToken`, and worker updates only start while that token still matches and enough lease time remains for the configured safety buffer. If the worker disappears, the lease expires and a later jobs runner call can claim the job again. A crashed worker does not increment `totalTried` or consume a handler retry.

#### Access Control

Use Payload's dedicated Jobs APIs and endpoints, such as `payload.jobs.*`, `/run`, and `/handle-schedules`. Calling generic Local API, REST, or GraphQL CRUD directly on `payload-jobs` is not recommended because it bypasses the job operations and does not run `jobs.access.*`.

Raw collection create, read, update, and delete access is denied by default. You can adjust these rules through `jobsCollectionOverrides`. REST, GraphQL, and Local API CRUD run these access rules by default. Set `overrideAccess: true` only for trusted server-side operations that should bypass them.

The dedicated job operations use `jobs.access.queue`, `run`, and `cancel`. `payload.jobs.queue()`, `run()`, `runByID()`, `cancel()`, and `cancelByID()` run these functions by default because their `overrideAccess` option defaults to `false`. Set `overrideAccess: true` to bypass Jobs access control for trusted server-side work. The `/run` and `/handle-schedules` endpoints always check `jobs.access.run`.

Configure access for the dedicated job operations in your Jobs Config:

```ts
import type { SanitizedConfig } from 'payload'

const config: SanitizedConfig = {
  // ...
  jobs: {
    access: {
      // Control who can queue new jobs
      queue: ({ req }) => {
        return req.user?.roles?.includes('admin')
      },
      // Control who can run jobs
      run: ({ req }) => {
        return req.user?.roles?.includes('admin')
      },
      // Control who can cancel jobs
      cancel: ({ req }) => {
        return req.user?.roles?.includes('admin')
      },
    },
  },
}
```

These functions receive the current `req` and return a boolean. By default, they allow authenticated users.

Omit `overrideAccess` or pass `overrideAccess: false` to run them from the Local API:

```ts
const req = await createPayloadRequest({ payload, user })

await payload.jobs.queue({
  workflow: 'createPost',
  input: { title: 'My Post' },
  overrideAccess: false, // Enable access control
  req, // Pass the request with user context
})
```

#### 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:

```ts
await payload.jobs.cancelByID({
  id: createdJob.id,
  overrideAccess: true,
})
```

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

```ts
await payload.jobs.cancel({
  overrideAccess: true,
  where: {
    workflowSlug: {
      equals: 'createPost',
    },
  },
})
```

From within a task or workflow handler, you can also cancel the current job by throwing a `JobCancelledError`:

```ts
throw new JobCancelledError('Job was cancelled')
```
