---
title: "Jobs Queue — Tasks"
aliases: [payload-tasks, jobs-queue-tasks]
tags: [payloadcms, jobs-queue, tasks, typescript, scheduling]
sources: [raw/jobs-queue__tasks.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

A **Task** is a strongly-typed function declaration registered in the Payload config that performs isolated business logic. Tasks are the atomic unit of the [[wiki/payloadcms/jobs-queue|Jobs Queue]] — Jobs and Workflows call Tasks.

Key trait: Tasks support **automatic retries**, making them ideal for non-deterministic work (AI/LLM calls, external API calls, payment processing).

## Defining Tasks

Tasks live in `jobs.tasks[]` in `buildConfig`:

```ts
jobs: {
  tasks: [
    {
      slug: 'createPost',          // unique across tasks AND workflows
      retries: 2,
      inputSchema: [
        { name: 'title', type: 'text', required: true },
      ],
      outputSchema: [
        { name: 'postID', type: 'text', required: true },
      ],
      handler: async ({ input, job, req }) => {
        const newPost = await req.payload.create({
          collection: 'post',
          data: { title: input.title },
        })
        return { output: { postID: newPost.id } }
      },
    } as TaskConfig<'createPost'>,
  ],
}
```

## Task Config Options

| Option | Description |
|--------|-------------|
| `slug` | Unique identifier — must be unique across tasks and workflows |
| `handler` | Function `async ({ input, job, req, tasks, inlineTask }) => { output: {...} }` or absolute file path |
| `inputSchema` | Payload field definitions — Payload generates TypeScript types from these |
| `outputSchema` | Same — generated output types |
| `retries` | Number of retries on failure; `0` = no retry; `undefined` = inherit from workflow |
| `retries.shouldRestore` | Boolean or function — whether to skip re-running a previously-passed task |
| `onFail` | Callback executed after all retries are exhausted |
| `onSuccess` | Callback executed on successful completion |
| `label` | Human-readable display name |
| `interfaceName` | Override generated TS interface name (default: `Task` + capitalized slug) |
| `concurrency` | Exclusive execution key — requires `jobs.enableConcurrencyControl: true` |
| `schedule` | Auto-queue on cron schedule — see [[wiki/payloadcms/jobs-queue-schedules|Job Schedules]] |

## Scheduling Tasks

Add `schedule` to auto-queue the task without calling `payload.jobs.queue()` manually:

```ts
{
  slug: 'dailyDigest',
  schedule: [
    { cron: '0 8 * * *', queue: 'daily' },
  ],
  handler: async ({ req }) => {
    // ... send emails
    return { output: { emailsSent: 42 } }
  },
}
```

**Critical:** `schedule` only *queues* jobs. You must also configure a runner with the same queue name:

```ts
autoRun: [
  { cron: '* * * * *', queue: 'daily', limit: 10 },
]
```

Common cron patterns:
```ts
{ cron: '0 * * * *',    queue: 'hourly'    }  // every hour
{ cron: '0 0 * * *',    queue: 'nightly'   }  // midnight
{ cron: '0 9 * * 1',    queue: 'weekly'    }  // Monday 9AM
{ cron: '*/5 * * * *',  queue: 'frequent'  }  // every 5 min
{ cron: '*/3 * * * * *',queue: 'realtime'  }  // every 3 sec (6-field syntax)
```

## Common Patterns

### Database Operations
```ts
{ slug: 'updateRelatedPosts', retries: 2,
  handler: async ({ input, req }) => {
    const posts = await req.payload.find({ collection: 'posts', where: { category: { equals: input.categoryId } } })
    for (const post of posts.docs) {
      await req.payload.update({ collection: 'posts', id: post.id, data: { categoryUpdatedAt: new Date().toISOString() } })
    }
    return { output: { postsUpdated: posts.docs.length } }
  }
}
```

### External API Calls
```ts
{ slug: 'syncToThirdParty', retries: 3,
  handler: async ({ input, req }) => {
    const response = await fetch('https://api.example.com/sync', { method: 'POST', body: JSON.stringify(doc) })
    if (!response.ok) throw new Error(`API error: ${response.statusText}`)
    return { output: { synced: true, apiResponse: await response.json() } }
  }
}
```

## Handling Failures

- **Throw errors** with descriptive messages — stored in `job.error` and visible in Admin UI
- **Prevent retries** by throwing `JobCancelledError`:
  ```ts
  throw new JobCancelledError('Job was cancelled')
  ```
- **Inspect failures** after the fact:
  ```ts
  const job = await payload.findByID({ collection: 'payload-jobs', id: jobId })
  if (job.hasError) {
    console.log(job.error)
    const failedTasks = job.log?.filter(e => e.state === 'failed')
  }
  ```

## Task Execution Lifecycle

1. Worker picks up the job from the queue
2. Handler executes with provided `input`
3. Success → output stored, job completes
4. Error thrown → task retried (up to `retries` count)
5. All retries exhausted → task and job fail

> **Idempotency:** Tasks should produce the same result when run multiple times with the same input, since retries cause repeated execution.

## Task Restoration (`shouldRestore`)

By default, if a workflow is re-run and a task previously passed, the task is **skipped** and previous output is reused.

Override with `retries.shouldRestore`:
- `true` (default) — skip if previously passed
- `false` — always re-run, even if previously passed
- `function({ input }) => boolean` — custom logic (e.g. re-run if date has changed)

```ts
retries: {
  shouldRestore: ({ input }) => new Date(input.someDate) > new Date() ? false : true,
}
```

## Nested Tasks

Tasks can call sub-tasks via `tasks` (registered tasks) and `inlineTask` (anonymous functions):

```ts
handler: async ({ tasks, inlineTask }) => {
  await inlineTask('Sub Task 1', { task: () => ({ output: {} }) })
  await tasks.CreateSimple('Sub Task 2', { input: { message: 'hello' } })
  return { output: {} }
}
```

Enable `jobs.addParentToTaskLog: true` for better observability when using nested tasks.

## Handler File Paths (Advanced)

Pass an absolute path + named export instead of an inline function to avoid bundling large dependencies in Next.js:

```ts
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler'
```

> Requires separate transpilation of handler files and a sophisticated build pipeline. Prefer inline functions unless bundle size is a real concern.

## Key Takeaways

- Tasks are the atomic unit: one slug, one handler, strongly-typed input/output
- `retries` enables durable AI/external API workflows — always set for non-deterministic operations
- `schedule` + matching `autoRun` queue = periodic auto-execution without manual queuing
- Throw errors for failures; use `JobCancelledError` to stop all retries immediately
- Tasks are idempotent by default via `shouldRestore` — re-runs skip already-passed tasks
- Handler file paths are an escape hatch for large dependencies; avoid unless necessary
- Enable `addParentToTaskLog: true` when using nested tasks

## Related

- [[wiki/payloadcms/jobs-queue|Jobs Queue — Overview]]
- [[wiki/payloadcms/jobs-queue-jobs|Jobs Queue — Jobs Detail]]
- [[wiki/payloadcms/jobs-queue-queues|Jobs Queue — Queues & Execution]]
- [[wiki/payloadcms/jobs-queue-schedules|Jobs Queue — Schedules]]
- [[wiki/payloadcms/jobs-queue-quick-start|Jobs Queue — Quick Start Examples]]
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] (triggering tasks from afterChange)
