---
title: "Jobs Queue — Jobs: Queuing, Status, Access Control, Cancellation"
aliases: [payload-jobs-detail, jobs-queue-detail]
tags: [payloadcms, jobs-queue, typescript]
sources: [raw/jobs-queue__jobs.md]
created: 2026-05-15
updated: 2026-05-15
---

# Jobs Queue — Jobs (Detail)

A **Job** is a runtime instance of a Task or Workflow stored in `payload-jobs`.

See the full overview at [[wiki/payloadcms/jobs-queue|Jobs Queue Overview]].

---

## Queuing a Job

```ts
// Queue a Workflow
await payload.jobs.queue({
  workflow: 'createPostAndUpdate',
  input: { title: 'my title' },
})

// Queue a single Task
await payload.jobs.queue({
  task: 'createPost',
  input: { title: 'my title' },
})
```

### Where to Queue

| Location | Pattern |
|----------|---------|
| Collection `afterChange` hook | `req.payload.jobs.queue(...)` — most common |
| Field `afterChange` hook | Trigger on specific field change (e.g. image variants on upload change) |
| Custom REST endpoint | `req.payload.jobs.queue(...)`, return `job.id` to caller |
| Next.js Server Action | `getPayload({ config })` → `payload.jobs.queue(...)` |

#### Collection hook example

```ts
afterChange: [async ({ req, doc, operation }) => {
  if (operation === 'update' && doc.status === 'published') {
    await req.payload.jobs.queue({
      task: 'notifySubscribers',
      input: { postId: doc.id },
    })
  }
}]
```

#### Server Action example

```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'

export async function scheduleEmail(userId: string) {
  const payload = await getPayload({ config })
  await payload.jobs.queue({ task: 'sendEmail', input: { userId } })
}
```

---

## Job Options

```ts
await payload.jobs.queue({
  task: 'sendEmail',
  input: { userId: '123' },
  waitUntil: new Date('2024-12-25T00:00:00Z'), // schedule for future
  queue: 'high-priority',                       // assign to named queue
  log: [{ message: 'Queued by admin', createdAt: new Date().toISOString() }],
  req,  // pass request context for access control
})
```

| Option | Description |
|--------|-------------|
| `waitUntil` | Run at specific future date/time |
| `queue` | Named queue (default: `'default'`) |
| `log` | Custom log entries for debugging/tracking |
| `req` | Request context (required for access control) |

---

## Job Status

### Check after queuing

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

const updatedJob = await payload.findByID({
  collection: 'payload-jobs',
  id: job.id,
})

updatedJob.completedAt  // ISO string when finished (null if pending)
updatedJob.hasError     // true if failed
updatedJob.taskStatus   // per-task details (for workflows)
```

### Full job document schema

```ts
{
  id: 'job_123',
  taskSlug: 'sendEmail',         // or workflowSlug for workflows
  input: { userId: '123' },
  completedAt: '2024-01-15...',  // null if still pending
  hasError: false,
  totalTried: 1,                 // number of execution attempts
  processing: false,             // true if currently running
  taskStatus: {                  // per-task (workflows only)
    sendEmail: {
      '1': { complete: true, output: { emailSent: true } }
    }
  },
  log: [{ message: 'Job started', createdAt: '...' }],
}
```

---

## Access Control

By default, `payload.jobs.*` Local API calls **bypass** access control.

### Define access rules in Jobs Config

```ts
jobs: {
  access: {
    queue:  ({ req }) => req.user?.roles?.includes('admin'),
    run:    ({ req }) => req.user?.roles?.includes('admin'),
    cancel: ({ req }) => req.user?.roles?.includes('admin'),
  },
}
```

- Each function receives `req` and returns `boolean`
- If undefined, authenticated users can perform the operation
- **Do NOT** set access control on the `payload-jobs` collection directly — may be deprecated

### Enable access control in Local API

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

await payload.jobs.queue({
  workflow: 'createPost',
  input: { title: 'My Post' },
  overrideAccess: false,  // enforce access control
  req,                    // must include user context
})
```

---

## Cancelling Jobs

```ts
// Cancel single job
await payload.jobs.cancelByID({ id: createdJob.id })

// Cancel multiple jobs by query
await payload.jobs.cancel({
  where: { workflowSlug: { equals: 'createPost' } },
})
```

- Cancelling a **running** job: current task finishes, subsequent tasks are skipped
- Cancellation is checked **between tasks**, not mid-task
- From inside a handler: `throw new JobCancelledError('reason')`

---

## Key Takeaways

- Jobs are instances of Tasks or Workflows stored in `payload-jobs`
- Queue from anywhere: collection hooks (most common), field hooks, endpoints, server actions
- `waitUntil` for one-time future scheduling; `queue` to segment execution pools
- Full job status is in `payload-jobs` collection — `completedAt`, `hasError`, `taskStatus`
- Access control lives in `jobs.access` config, **not** on the collection; use `overrideAccess: false` + `req` to enforce
- Cancellation is inter-task (current task finishes); use `JobCancelledError` from within handlers

---

## Related

- [[wiki/payloadcms/jobs-queue|Jobs Queue — Full Reference]] (tasks, workflows, queues, schedules, gotchas)
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] (most common place to queue jobs)
- [[wiki/payloadcms/authentication-overview|Authentication Overview]] (access control patterns)
- [[wiki/payloadcms/local-api|Local API]] (`payload.jobs.*`, `overrideAccess`)
