---
title: "Jobs Queue — Schedules"
aliases: [payload-schedules, jobs-schedule, cron-jobs-payload]
tags: [payloadcms, jobs-queue, cron, scheduling, background-jobs]
sources: [raw/jobs-queue__schedules.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

Payload's `schedule` property on tasks/workflows automatically enqueues jobs on a cron cadence. Scheduling ≠ running — two separate mechanisms must both be configured.

- **Schedule** → adds jobs to DB queue on cron
- **Runner** → picks up queued jobs and executes handlers

## When to Use What

| Approach | Use Case | Example |
|---|---|---|
| `schedule` | Recurring automatic tasks | Daily reports, weekly emails, hourly syncs |
| `waitUntil` | One-time future job | Publish post at 3pm, trial expiry email in 7 days |
| Collection Hook | Triggered by document change | Email on publish, PDF on order create |
| Manual queue | Triggered by user/API action | "Generate Report" button |

## Critical: Two Configs Required

Both must be present or scheduled jobs will not work:

### 1. Schedule Config (on task/workflow)

```ts
export const SendDigestEmail: TaskConfig<'SendDigestEmail'> = {
  slug: 'SendDigestEmail',
  schedule: [
    {
      cron: '0 0 * * *', // Every day at midnight
      queue: 'nightly',
    },
  ],
  handler: async () => {
    await sendDigestToAllUsers()
  },
}
```

### 2. Runner Config (must match same queue name)

```sh
# Recommended: bin script (separate process from Next.js)
pnpm payload jobs:run --cron "* * * * *" --queue nightly --handle-schedules
```

Or via `autoRun`:

```ts
jobs: {
  autoRun: [{ cron: '* * * * *', queue: 'nightly' }],
}
```

**Checklist before debugging:**
- [ ] Task has `schedule` property?
- [ ] Config has `autoRun` or bin script runner?
- [ ] Both use the **same `queue` name**?
- [ ] Not on serverless? (`autoRun` needs long-running server)

## Schedule Handling Methods

### Bin Scripts (Recommended for Dedicated Servers)

```sh
# Combined: schedule + run
pnpm payload jobs:run --cron "*/5 * * * *" --queue myQueue --handle-schedules

# Schedule-only (separate processes)
pnpm payload jobs:handle-schedules --cron "*/5 * * * *" --queue myQueue
pnpm payload jobs:handle-schedules --cron "*/5 * * * *" --all-queues
```

Benefits: separate process, independent scaling, zero impact on Next.js server.

### autoRun (Dedicated Server Alternative)

```ts
jobs: {
  autoRun: [
    {
      cron: '*/5 * * * *',
      queue: 'default',
      // disableScheduling: false  ← default, set true to decouple
    },
  ],
}
```

### API Endpoint (Serverless)

```ts
// Handles both scheduling and running
await fetch('/api/payload-jobs/run?queue=myQueue')

// Schedule only
await fetch('/api/payload-jobs/handle-schedules?queue=myQueue')

// Disable scheduling in run endpoint
await fetch('/api/payload-jobs/run?queue=myQueue&disableScheduling=true')
```

Configure Vercel Cron to call `GET /api/payload-jobs/run` on your desired interval.

### Local API (Programmatic)

```ts
await payload.jobs.handleSchedules()
```

## ScheduleConfig Type

```ts
export type ScheduleConfig = {
  cron: string        // required; supports 5-field and 6-field (seconds) format
  queue: string       // required; must match runner queue name
  hooks?: {
    beforeSchedule?: BeforeScheduleFn
    afterSchedule?: AfterScheduleFn
  }
}
```

## Scheduling Lifecycle

1. **Cron evaluation** — reads `payload-jobs-stats` global to find which schedules are due
2. **beforeSchedule hook** — default: skip if active/runnable job of same type already exists; customizable
3. **Enqueue Job** — creates job with `waitUntil` set to next scheduled time
4. **afterSchedule hook** — default: updates `payload-jobs-stats` with last scheduled time; customizable

## Custom Concurrency & Dynamic Input

```ts
import { countRunnableOrActiveJobsForQueue } from 'payload'

schedule: [
  {
    cron: '* * * * *',
    queue: 'reports',
    hooks: {
      beforeSchedule: async ({ queueable, req }) => {
        const count = await countRunnableOrActiveJobsForQueue({
          queue: queueable.scheduleConfig.queue,
          req,
          taskSlug: queueable.taskConfig?.slug,
          onlyScheduled: true,
        })

        return {
          shouldSchedule: count < 3,  // allow up to 3 simultaneous
          input: { text: 'Hi there' }, // dynamic input at schedule time
        }
      },
    },
  },
]
```

## Common Cron Patterns

```ts
schedule: [{ cron: '* * * * *',    queue: 'frequent' }]  // every minute
schedule: [{ cron: '*/5 * * * *',  queue: 'default'  }]  // every 5 min
schedule: [{ cron: '0 * * * *',    queue: 'hourly'   }]  // every hour
schedule: [{ cron: '0 0 * * *',    queue: 'nightly'  }]  // midnight daily
schedule: [{ cron: '30 2 * * *',   queue: 'nightly'  }]  // 2:30 AM daily
schedule: [{ cron: '0 9 * * 1',    queue: 'weekly'   }]  // Mon 9:00 AM
schedule: [{ cron: '0 0 1 * *',    queue: 'monthly'  }]  // 1st of month
schedule: [{ cron: '0 8 * * 1-5',  queue: 'weekdays' }]  // weekdays 8AM
schedule: [{ cron: '*/30 * * * * *', queue: 'frequent' }] // every 30s (6-field)
```

**Cron field order:**

```
[second] minute  hour  day-of-month  month  day-of-week
           0-59  0-23  1-31          1-12   0-7 (0,7=Sun)
```

## Serverless Platforms

`autoRun` does NOT work on serverless (no long-running process). Instead:

1. Define `schedule` on tasks as normal
2. Configure Vercel Cron (or similar) to hit `GET /api/payload-jobs/run?queue=<name>` on your desired interval
3. Queued jobs execute during that same request (or separately via another runner)

## Troubleshooting

**Jobs not being queued:**
- Check `disableScheduling` is not `true` on `autoRun`
- Validate cron expression at crontab.guru (5-field) or test with seconds (6-field)
- Inspect `payload-jobs-stats` global: `await payload.findGlobal({ slug: 'payload-jobs-stats' })`

**Jobs queued but not running:**
- See [[wiki/payloadcms/jobs-queue-queues|Jobs Queue — Queues & Execution]] troubleshooting

**Jobs run at wrong times:**
- Ensure `autoRun` checks queue frequently enough (`* * * * *` = every minute)

**Duplicate jobs (multiple servers):**

```ts
// Server 1 — handles schedules
jobs: { shouldAutoRun: () => process.env.HANDLE_SCHEDULES === 'true', autoRun: [/*...*/] }

// Server 2 — only runs jobs, no scheduling
jobs: { shouldAutoRun: () => true, autoRun: [{ disableScheduling: true }] }
```

## Key Takeaways

- **Scheduling and running are separate concerns** — `schedule` enqueues, runner executes; both must be configured
- **Queue names must match** between `schedule` config and runner (`autoRun`/bin script)
- **Bin scripts are preferred** for dedicated servers — isolated process, no Next.js impact
- **Serverless = no autoRun** — use external cron (Vercel Cron) hitting the API endpoint
- **Default deduplication** — `beforeSchedule` hook skips new job if one already exists; override with custom hook for concurrency
- **Dynamic input** is set at schedule time via `beforeSchedule` return value, not at handler runtime
- Use `waitUntil` for one-time future jobs, not `schedule` (which recurs indefinitely)

## Sources

- `raw/jobs-queue__schedules.md`
- https://payloadcms.com/docs/jobs-queue/schedules

## Related

- [[wiki/payloadcms/jobs-queue|Jobs Queue — Overview]]
- [[wiki/payloadcms/jobs-queue-queues|Jobs Queue — Queues & Execution]]
- [[wiki/payloadcms/jobs-queue-jobs|Jobs Queue — Jobs Detail]]
- [[wiki/payloadcms/jobs-queue-quick-start|Jobs Queue — Quick Start Examples]]
