obsidian/wiki/payloadcms/jobs-queue-tasks.md
2026-05-15 16:14:29 +01:00

7.2 KiB

title aliases tags sources created updated
Jobs Queue — Tasks
payload-tasks
jobs-queue-tasks
payloadcms
jobs-queue
tasks
typescript
scheduling
raw/jobs-queue__tasks.md
2026-05-15 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 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:

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

Scheduling Tasks

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

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

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

Common cron patterns:

{ 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

{ 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

{ 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:
    throw new JobCancelledError('Job was cancelled')
    
  • Inspect failures after the fact:
    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)
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):

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:

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