---
title: "How to Implement Payload Jobs for Background Operations in Next.js on Vercel"
slug: "how-to-implement-payload-jobs-for-background-operations-in-next-js-on-vercel"
published: "2025-05-28"
updated: "2026-03-21"
validated: "2026-03-21"
categories:
  - "Payload"
tags:
  - "payload jobs"
  - "payload jobs queue"
  - "vercel background jobs"
  - "vercel cron jobs"
  - "nextjs background jobs"
  - "vercel waituntil"
  - "nextjs after"
  - "payload cms cron job"
  - "nextjs job queue"
  - "payload cms background tasks"
llm-intent: "reference"
audience-level: "advanced"
framework-versions:
  - "nextjs@15"
  - "payload@2"
  - "vercel@serverless"
status: "stable"
llm-purpose: "Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs."
llm-prereqs:
  - "Access to Payload"
  - "Access to Nextjs"
llm-outputs:
  - "Completed outcome: Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs."
---

**Summary Triples**
- (Payload after-change hooks, must not perform, long-running or heavy operations synchronously (they block uploads and freeze the app))
- (Vercel serverless environment, does not allow, persistent background processes (so daemons or in-process queues are not reliable))
- (Recommended solution, uses, a job queue pattern that persists jobs and processes them asynchronously)
- (Gallery sync on upload, should be implemented by, enqueueing a job in a Payload collection (or persistent store) from the upload hook, not performing the work inline)
- (Job processing, can run via, serverless API routes (Next.js API routes on Vercel) or webhooks that process jobs on-demand)
- (System reliability, requires, fallbacks and retries (e.g., requeuing failed jobs, idempotent processors, and monitoring))
- (Security, requires, production-ready authentication for job execution endpoints (e.g., signed tokens or secrets))
- (User experience, benefits from, immediate success responses for uploads while background sync runs asynchronously)

### {GOAL}
Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs.

### {PREREQS}
- Access to Payload
- Access to Nextjs

### {STEPS}
1. Understanding the Architecture
2. Configure Payload Jobs
3. Create the Media Collection Hook
4. Add the Hook to Your Media Collection
5. Create the Immediate Execution Endpoint
6. Enhance the Job Execution Endpoint
7. Configure Vercel Cron Jobs
8. Set Up Your Gallery Block
9. Environment Variables
10. Testing Your Implementation

<!-- llm:goal="Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs." -->
<!-- llm:prereq="Access to Payload" -->
<!-- llm:prereq="Access to Nextjs" -->
<!-- llm:output="Completed outcome: Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs." -->

# How to Implement Payload Jobs for Background Operations in Next.js on Vercel
> Step-by-step guide to running background jobs in Payload CMS on Vercel with Next.js. Covers job queues, Vercel cron, after(), waitUntil, and CRON_SECRET auth.
Matija Žiberna · 2025-05-28

# How to Implement Payload Jobs for Background Operations in Next.js on Vercel

Payload CMS has a built-in job queue that lets you offload heavy work — image processing, data syncing, email sending — to background tasks that run without blocking your users. On Vercel, where serverless functions terminate after sending a response, you need a specific pattern to make this work: queue a job, trigger it with `after()` or `waitUntil` for immediate execution, and set up a Vercel cron as a safety net. This guide walks through the full setup, from defining tasks in `payload.config.ts` to configuring cron authentication and debugging stuck jobs.

I ran into this problem on a client project where media uploads needed to trigger gallery syncs across multiple pages. The `afterChange` hook was doing the sync inline, freezing the upload for 5+ seconds while it crawled every page looking for gallery blocks. On Vercel, that meant timeouts and a terrible editing experience. Payload's job queue solved it, but the docs don't cover the Vercel-specific patterns you need to make it production-ready.

## Quick Reference

| Approach | When to Use | Next.js Version |
|---|---|---|
| `after()` from `next/server` | Immediate background work triggered by a user action | 15.1+ |
| `waitUntil` from `@vercel/functions` | Same as above, for older Next.js versions | Any |
| Vercel Cron + `/api/payload-jobs/run` | Scheduled cleanup, batch processing, retry safety net | Any |
| Payload `autoRun` | Self-hosted environments with persistent processes | N/A on Vercel |

If you are on Next.js 15.1+, use `after()`. If you are on an older version, use `waitUntil`. Both allow your serverless function to send a response immediately and continue running code in the background. The Vercel cron acts as a fallback that picks up any jobs that were queued but never executed.

## Essential Reading

Before diving into the implementation, read these official Payload documentation pages:

- [Jobs Queue Overview](https://payloadcms.com/docs/jobs-queue/overview) — how the job system works under the hood
- [Tasks Documentation](https://payloadcms.com/docs/jobs-queue/tasks) — defining tasks, input schemas, and handlers
- [Queues Documentation](https://payloadcms.com/docs/jobs-queue/queues) — managing queues and processing jobs

## Understanding after() and waitUntil

Vercel's serverless functions normally terminate the moment a response is sent. Any `async` work still running gets killed. This is why you can't just fire off a `payload.jobs.run()` call after returning a response — the function shuts down before the job finishes.

Next.js 15.1 introduced `after()` from `next/server`, which tells the runtime to keep the function alive after the response is sent. Vercel's `waitUntil` from `@vercel/functions` does the same thing for older Next.js versions.

Here is the pattern with `after()`:

```typescript
// File: src/app/api/example/route.ts
import { after } from 'next/server'
import { NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'

export async function POST(request: Request) {
  const payload = await getPayload({ config })
  const data = await request.json()

  // Queue the job
  await payload.jobs.queue({
    task: 'myBackgroundTask',
    input: { id: data.id },
  })

  // Tell Next.js to keep the function alive after responding
  after(async () => {
    await payload.jobs.run({ limit: 10, queue: 'default' })
  })

  // User gets an immediate response
  return NextResponse.json({ status: 'queued' })
}
```

The `after()` callback runs after the response is sent to the client. The function stays alive long enough for `payload.jobs.run()` to process the queued job. If the function terminates before the job finishes (due to Vercel's execution time limit), the job stays queued and the cron picks it up later.

For older Next.js versions, the equivalent pattern uses `waitUntil`:

```typescript
// File: src/app/api/example/route.ts
import { waitUntil } from '@vercel/functions'
import { NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'

export async function POST(request: Request) {
  const payload = await getPayload({ config })
  const data = await request.json()

  await payload.jobs.queue({
    task: 'myBackgroundTask',
    input: { id: data.id },
  })

  // waitUntil extends function lifetime after response
  waitUntil(
    payload.jobs.run({ limit: 10, queue: 'default' })
  )

  return NextResponse.json({ status: 'queued' })
}
```

The key difference: `after()` is a Next.js framework feature that works across deployment targets. `waitUntil` is a Vercel platform primitive. On Vercel with Next.js 15.1+, `after()` is the recommended choice.

## The Architecture

Here is the full flow we are building:

```
User triggers action (upload, form submit, etc.)
    |
    v
afterChange hook queues a Payload job
    |
    v
after() runs payload.jobs.run() in background
    |
    +---> Success: job completes, user already has their response
    |
    +---> Function terminated early: job stays queued
                                        |
                                        v
                              Vercel cron hits /api/payload-jobs/run
                              every minute and picks up pending jobs
```

This gives you two layers of execution. The `after()` call handles the fast path — most jobs complete within a few seconds. The cron handles the safety net — if the function was killed, the job is still in the database waiting to be processed.

## Step 1: Configure Payload Jobs

Start by defining your task in `payload.config.ts`. The task specifies what inputs it expects and what the handler does with them.

```typescript
// File: payload.config.ts
import { buildConfig } from 'payload'
import type { PayloadRequest } from 'payload'

export default buildConfig({
  // ... your existing config

  jobs: {
    // Authentication for the job runner endpoint
    access: {
      run: ({ req }: { req: PayloadRequest }): boolean => {
        // Allow authenticated admin users
        if (req.user) return true

        // Allow Vercel cron with CRON_SECRET
        const secret = process.env.CRON_SECRET
        if (!secret) return false

        const authHeader = req.headers.get('authorization')
        return authHeader === `Bearer ${secret}`
      },
    },

    tasks: [
      {
        slug: 'syncMediaToGalleries',
        label: 'Sync Media to Gallery Blocks',
        inputSchema: [
          {
            name: 'mediaId',
            type: 'text',
            required: true,
          },
        ],
        handler: async ({ input, req }) => {
          req.payload.logger.info(
            `Starting gallery sync for media ${input.mediaId}`,
          )

          // Verify the media document exists (with retries for eventual consistency)
          let mediaDoc
          let retries = 3

          while (retries > 0) {
            try {
              mediaDoc = await req.payload.findByID({
                collection: 'media',
                id: input.mediaId,
              })
              break
            } catch (error) {
              retries--
              if (retries > 0) {
                await new Promise((resolve) => setTimeout(resolve, 1000))
              } else {
                throw new Error(
                  `Media ${input.mediaId} not found after retries`,
                )
              }
            }
          }

          if (!mediaDoc) {
            req.payload.logger.warn(
              `Media ${input.mediaId} not found, skipping`,
            )
            return { output: {} }
          }

          // Find pages with gallery blocks that have auto-sync enabled
          const pages = await req.payload.find({
            collection: 'pages',
            depth: 2,
            limit: 1000,
          })

          let pagesUpdated = 0
          let galleriesUpdated = 0

          for (const page of pages.docs) {
            if (!page.layout || !Array.isArray(page.layout)) continue

            let hasUpdates = false
            const updatedLayout = page.layout.map((block: any) => {
              if (
                block.blockType === 'gallery' &&
                block.autoSyncMedia === true
              ) {
                if (!block.images) block.images = []

                const alreadyExists = block.images.some(
                  (id: string) => id === input.mediaId,
                )

                if (!alreadyExists) {
                  block.images.push(input.mediaId)
                  hasUpdates = true
                  galleriesUpdated++
                }
              }
              return block
            })

            if (hasUpdates) {
              await req.payload.update({
                collection: 'pages',
                id: page.id,
                data: { layout: updatedLayout },
                depth: 0,
                overrideAccess: true,
              })
              pagesUpdated++
            }
          }

          req.payload.logger.info(
            `Gallery sync complete: ${pagesUpdated} pages, ${galleriesUpdated} galleries updated`,
          )

          return {
            output: { pagesUpdated, galleriesUpdated, mediaId: input.mediaId },
          }
        },
      },
    ],
  },

  // ... rest of your config
})
```

A few things to note in this configuration. The `jobs.access.run` function handles authentication at the Payload config level. This means you do not need to duplicate auth logic in every route handler — Payload checks it before running any jobs. The retry loop in the handler accounts for eventual consistency: when a media document is freshly created, it might not be immediately available in the database by the time the job runs.

The `overrideAccess: true` flag on the page update bypasses access control because this is a system-level operation running in a background context where there is no authenticated user session.

Since Payload v3.49.0, you can also configure concurrency control for `autoRun` to prevent job overload. On Vercel, `autoRun` does not apply (there is no persistent process), so we rely on HTTP triggers and cron instead.

## Step 2: Create the Collection Hook

The hook fires when new media is uploaded, queues a background job, and uses `after()` to trigger immediate execution without blocking the upload response.

```typescript
// File: src/collections/Media/hooks/syncGalleryBlocks.ts
import { CollectionAfterChangeHook } from 'payload'
import { after } from 'next/server'

export const syncGalleryBlocks: CollectionAfterChangeHook = async ({
  doc,
  req,
  operation,
}) => {
  if (operation !== 'create') {
    return doc
  }

  try {
    // Queue the job
    const job = await req.payload.jobs.queue({
      task: 'syncMediaToGalleries',
      input: { mediaId: doc.id },
    })

    req.payload.logger.info(
      `Queued gallery sync job ${job.id} for media ${doc.id}`,
    )

    // Run the job in the background after the response is sent
    after(async () => {
      try {
        await req.payload.jobs.run({ limit: 10, queue: 'default' })
      } catch (error) {
        req.payload.logger.error(
          `Background job execution failed for media ${doc.id}: ${(error as Error).message}`,
        )
      }
    })
  } catch (error) {
    req.payload.logger.error(
      `Failed to queue gallery sync for media ${doc.id}: ${(error as Error).message}`,
    )
  }

  return doc
}
```

This is a significant improvement over the `setTimeout` pattern. With `after()`, the response returns to the user immediately. The job queue call and the `after()` callback both happen without blocking. If the background execution fails or the function is terminated, the job remains in the queue for the cron to pick up.

The `operation !== 'create'` check ensures this only runs for new uploads. Updates to existing media (like changing alt text) do not trigger a gallery sync.

If you are on a Next.js version older than 15.1, replace the `after()` import and call with `waitUntil` from `@vercel/functions`:

```typescript
// File: src/collections/Media/hooks/syncGalleryBlocks.ts (pre-15.1)
import { waitUntil } from '@vercel/functions'

// Inside the hook, replace after() with:
waitUntil(
  req.payload.jobs.run({ limit: 10, queue: 'default' }).catch((error) => {
    req.payload.logger.error(
      `Background job execution failed: ${(error as Error).message}`,
    )
  }),
)
```

## Step 3: Register the Hook

Add the hook to your Media collection configuration.

```typescript
// File: src/collections/Media.ts
import { CollectionConfig } from 'payload'
import { syncGalleryBlocks } from './Media/hooks/syncGalleryBlocks'

export const Media: CollectionConfig = {
  slug: 'media',
  labels: {
    singular: 'Media',
    plural: 'Media',
  },
  hooks: {
    afterChange: [syncGalleryBlocks],
  },
  upload: {
    // ... your upload config
  },
  fields: [
    // ... your fields
  ],
}
```

The `afterChange` hook fires after the media document is saved to the database. This guarantees the media ID is valid and available when the background job looks it up.

## Step 4: Create the Job Runner Endpoint

This endpoint processes queued jobs. It serves two purposes: the Vercel cron calls it on a schedule, and you can call it manually for testing or immediate processing.

```typescript
// File: src/app/api/payload-jobs/run/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'

export async function GET(request: NextRequest) {
  const startTime = Date.now()

  try {
    const payload = await getPayload({ config })

    const result = await payload.jobs.run({
      limit: 10,
      queue: 'default',
    })

    const executionTime = Date.now() - startTime

    payload.logger.info(
      `Job runner completed in ${executionTime}ms: ${JSON.stringify(result)}`,
    )

    return NextResponse.json({
      success: true,
      result,
      executionTimeMs: executionTime,
    })
  } catch (error) {
    const executionTime = Date.now() - startTime
    console.error('Error running jobs:', error)

    return NextResponse.json(
      {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
        executionTimeMs: executionTime,
      },
      { status: 500 },
    )
  }
}

export async function POST(request: NextRequest) {
  return GET(request)
}
```

Notice there is no manual auth check in this route. Authentication is handled by the `jobs.access.run` function in `payload.config.ts` (Step 1). When `payload.jobs.run()` is called, Payload checks that access function before processing any jobs. Vercel sends the `CRON_SECRET` as an `Authorization: Bearer` header automatically when triggering cron endpoints.

The endpoint supports both GET and POST because Vercel cron uses GET requests by default, while manual triggers or webhook integrations might use POST.

The `limit: 10` parameter processes up to 10 jobs per invocation. This prevents a single cron tick from running too long and hitting Vercel's function timeout. If more than 10 jobs are queued, the next cron tick picks up the rest.

## Step 5: Configure Vercel Cron Jobs

Add a `vercel.json` file to your project root (or update your existing one) with a cron schedule:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    {
      "path": "/api/payload-jobs/run",
      "schedule": "*/1 * * * *"
    }
  ]
}
```

The `*/1 * * * *` schedule runs the job endpoint every minute. For most applications this provides fast enough pickup. If your jobs are less time-sensitive, you can reduce the frequency:

| Schedule | Frequency | Good For |
|---|---|---|
| `*/1 * * * *` | Every minute | Real-time sync, notifications |
| `*/5 * * * *` | Every 5 minutes | Email digests, report generation |
| `0 * * * *` | Every hour | Data aggregation, cleanup tasks |
| `0 0 * * *` | Daily at midnight | Backups, daily summaries |

Vercel automatically sends a `CRON_SECRET` header when it triggers cron endpoints, provided you have set the `CRON_SECRET` environment variable in your project settings. There is a known issue (tracked on GitHub) where Vercel occasionally fails to send the header. If you encounter 401 errors on cron runs, check the Vercel function logs to confirm the header is present. A workaround is to also check for Vercel's `x-vercel-cron` header as a secondary validation.

Vercel's cron frequency limits depend on your plan. Hobby plans support cron jobs running once per day. Pro plans support up to every minute. Check Vercel's pricing page for current limits.

## Step 6: Set Up the Gallery Block

For the gallery auto-sync use case, you need a block with an `autoSyncMedia` toggle and an `images` field.

```typescript
// File: src/blocks/general/Gallery/config.ts
import type { Block } from 'payload'

const GalleryBlock: Block = {
  slug: 'gallery',
  interfaceName: 'GalleryBlock',
  labels: {
    singular: 'Gallery',
    plural: 'Galleries',
  },
  fields: [
    {
      name: 'autoSyncMedia',
      type: 'checkbox',
      label: 'Auto-sync new media uploads',
      defaultValue: false,
      admin: {
        description:
          'When enabled, new uploaded images will automatically be added to this gallery',
      },
    },
    {
      name: 'images',
      type: 'upload',
      relationTo: 'media',
      hasMany: true,
      label: 'Gallery Images',
    },
  ],
}

export default GalleryBlock
```

The `autoSyncMedia` checkbox gives editors control over which galleries receive new uploads. The default is `false`, so galleries only auto-sync when explicitly opted in. The `hasMany: true` on the images field allows multiple media documents per gallery.

## Step 7: Environment Variables

Set these in your Vercel project dashboard under Settings > Environment Variables:

```bash
# Required for cron authentication in production
CRON_SECRET=your-secure-random-string-here

# Your deployment URL
NEXT_PUBLIC_SERVER_URL=https://your-domain.vercel.app
```

Generate the `CRON_SECRET` with a cryptographically secure method — at least 32 characters. You can generate one with `openssl rand -base64 32`.

In local development, you can skip `CRON_SECRET` entirely. The `jobs.access.run` function in your Payload config returns `false` when no secret is set, but authenticated admin users can still trigger jobs through the admin panel.

## Step 8: Testing

Test the full flow by uploading a media file through the Payload admin panel and checking the logs. You can also trigger the job runner directly:

```bash
# Test locally
curl -X GET http://localhost:3000/api/payload-jobs/run

# Test on Vercel (with auth)
curl -X GET https://your-domain.vercel.app/api/payload-jobs/run \
  -H "Authorization: Bearer your-cron-secret"
```

Watch the Vercel function logs (or your local terminal) for output like:

```
Starting gallery sync for media abc123
Gallery sync complete: 2 pages, 3 galleries updated
Job runner completed in 1250ms
```

If you want a more thorough test, create a script:

```javascript
// File: scripts/test-gallery-sync.js
const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
const CRON_SECRET = process.env.CRON_SECRET
const MEDIA_ID = process.argv[2]

if (!MEDIA_ID) {
  console.error('Usage: node scripts/test-gallery-sync.js <mediaId>')
  process.exit(1)
}

async function test() {
  console.log(`Testing gallery sync for media: ${MEDIA_ID}`)

  const response = await fetch(`${SERVER_URL}/api/payload-jobs/run`, {
    method: 'GET',
    headers: {
      ...(CRON_SECRET && { Authorization: `Bearer ${CRON_SECRET}` }),
    },
  })

  const result = await response.json()
  console.log('Result:', JSON.stringify(result, null, 2))
}

test().catch(console.error)
```

Run it with `node scripts/test-gallery-sync.js YOUR_MEDIA_ID`.

## Troubleshooting

### Jobs not executing

The most common cause is that `after()` is not keeping the function alive. Make sure you are on Next.js 15.1+ and that `after()` is imported from `next/server`. If you are on an older version, use `waitUntil` from `@vercel/functions` instead. Also verify that the job runner endpoint (`/api/payload-jobs/run`) is returning 200 when hit directly.

### "Task not found" or jobs stuck in pending

If you queue a job with a task slug that does not match any task registered in `payload.config.ts`, the job will be queued in the database but never execute. Double-check that the `slug` in your `payload.jobs.queue()` call matches the `slug` in your `jobs.tasks` array exactly.

### Vercel cron returning 401 Unauthorized

Verify that `CRON_SECRET` is set in your Vercel environment variables and that the `jobs.access.run` function in your Payload config checks the `Authorization` header correctly. There is a known Vercel issue where the `CRON_SECRET` header is occasionally not sent. Check the function logs to confirm. As a workaround, you can add a secondary check for the `x-vercel-cron` header:

```typescript
// Fallback check in jobs.access.run
const isVercelCron = req.headers.get('x-vercel-cron') === '1'
if (isVercelCron && secret) {
  return true
}
```

### Jobs stuck in "processing" state

This happens when a function is terminated mid-execution (timeout, crash, deployment). The job is marked as "processing" and never completes. Check Vercel function logs for timeout errors. To clear stuck jobs, you can query the `payload-jobs` collection in your database and reset their status, or use `payload.jobs.run()` with appropriate options to retry them.

### Vercel function timeout

Vercel's default function timeout is 10 seconds on the Hobby plan and up to 300 seconds on Pro. If your job handler takes longer than the timeout, the function is killed. Keep job handlers fast by processing one item at a time and queuing separate jobs for batch operations. The `limit: 10` parameter on `payload.jobs.run()` also helps — it processes at most 10 jobs per invocation, leaving the rest for the next cron tick.

### Gallery images not appearing

Check that the gallery block has `autoSyncMedia` set to `true` in the Payload admin panel. Verify the media ID exists by checking the media collection. Look at the function logs for the "Gallery sync complete" message to confirm the job ran and which pages were updated.

## Frequently Asked Questions

**Can I use Payload's autoRun feature on Vercel?**

No. `autoRun` relies on a persistent Node.js process that continuously polls for new jobs. Vercel's serverless functions are ephemeral — they spin up for a request and shut down afterward. Use the HTTP endpoint + cron pattern described in this guide instead.

**What happens if the same job is queued twice?**

Payload does not deduplicate jobs automatically. If you need deduplication, add a check in your hook before queuing (for example, query the `payload-jobs` collection for an existing pending job with the same input), or handle idempotency in the task handler itself (the gallery sync example already does this by checking if the media ID already exists in the gallery).

**Can I use this pattern for tasks other than gallery sync?**

Absolutely. The pattern (queue in hook, execute via `after()`, cron as safety net) works for any background operation: sending emails, generating thumbnails, syncing data to external APIs, running reports. Define a new task in `payload.config.ts` and queue it from any hook or API route.

**How do I monitor job execution in production?**

Vercel's function logs show all `payload.logger` output. You can also query the `payload-jobs` collection through the Payload admin panel or REST API to see job statuses, execution times, and error messages.

**Should I use Inngest or Trigger.dev instead of Payload jobs?**

For simple background operations tightly coupled to your Payload data, the built-in job queue is the simplest option — no external services, no additional billing, no webhook configuration. Inngest and Trigger.dev are better choices when you need complex multi-step workflows, fan-out patterns, rate limiting across services, or jobs that run for minutes rather than seconds.

## What You Have Built

This setup gives you non-blocking uploads where users get instant responses, automatic background processing via `after()` for the fast path, a Vercel cron safety net that catches any jobs missed by the immediate path, and centralized authentication through Payload's `jobs.access.run` config. The gallery auto-sync example demonstrates the pattern, but it applies to any background operation you need to run in a serverless Payload CMS deployment.

The full flow is: hook queues job, `after()` processes it immediately in the background, cron picks up stragglers every minute. Three layers of reliability with minimal code.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks,
Matija

## LLM Response Snippet
```json
{
  "goal": "Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs.",
  "responses": [
    {
      "question": "What does the article \"How to Implement Payload Jobs for Background Operations in Next.js on Vercel\" cover?",
      "answer": "Learn how to auto-sync gallery blocks in Payload CMS when new media is uploaded—without blocking uploads or relying on cron jobs."
    }
  ]
}
```