Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

New in Payload: Trash Support, Job Scheduling, and DX Enhancements

New release from Payload ...
New release from Payload ...

Trash support (soft deletes), data aggregation, background job scheduling, and refreshable drawers — here’s everything new and useful in Payload’s latest release.

Shipping features at Payload remains our top priority and we’ve been hard at work making Payload even more powerful, flexible, and developer-friendly.

In this update, we’re excited to share four new features that expand what you can do with your data and workflows: soft deletes via a new Trash system, a handy refresh method in the List Drawer context, powerful new groupBy querying for backend insights, and native job scheduling in our Jobs API.

Let's dive in ...

Trash (Soft Delete)

This feature introduces *soft delete* support, allowing items to be marked as deleted (e.g. via a `deletedAt` field) without being permanently removed. Users can later restore or filter out trashed items. This enables safer data workflows and aligns with standard practices you'd expect with enterprise CRUD apps.

Enabling trash for your collections is simple:

1
import type { CollectionConfig } from 'payload'
2
3
const Posts: CollectionConfig = {
4
slug: 'posts',
5
trash: true, // <-- New collection config prop @default false
6
fields: [
7
{
8
name: 'title',
9
type: 'text',
10
},
11
// other fields...
12
],
13
}

Benefits of leveraging soft delete:

  • Easier recovery workflows
  • Auditability of deleted content

Expose `refresh` Method on List Drawer Context

With this update, developers can call a `refresh` method directly from the list drawer context—enabling cleaner, more dynamic UI patterns. It allows deeply integrated custom components (e.g. custom lists or drawer-based editors) to trigger a context-aware refresh of data.

1
'use client'
2
import { useListDrawerContext } from '@payloadcms/ui'
3
4
const MyComponent = () => {
5
const { refresh } = useListDrawerContext()
6
7
return (
8
<button
9
onClick={refresh}
10
type="button"
11
>
12
Refresh
13
</button>
14
)
15
}

Use case:

  • A custom list component opens an item in a drawer, updates it, and refreshes the parent view automatically for a seamless user experience

Group By Query Support

This PR enhances Payload's querying capabilities by supporting `group_by`, enabling grouping results by a shared field. Use cases include aggregations such as counting items per category, aggregating metrics, or grouping entries by status.

Enable on any collection by setting the admin.groupBy property:

1
import type { CollectionConfig } from 'payload'
2
3
const MyCollection: CollectionConfig = {
4
// ...
5
admin: {
6
groupBy: true
7
}
8
}

Why it matters:

  • Powerful reporting inside API queries
  • Backend support for aggregate dashboards or admin insights
  • Less need for custom logic or database aggregation layers

Jobs Scheduling & Enhanced Queueing

Many of you have been asking for this feature and we're happy to deliver. Payload’s Jobs Queue now supports scheduling jobs using cron-style `schedule` attributes, plus delayed execution (`waitUntil`). This enables recurring or time-triggered tasks directly within Payload.

API Example:

1
export default buildConfig({
2
// ...
3
jobs: {
4
// ...
5
scheduler: 'manual', // Or `cron` if you're not using serverless. If `manual` is used, then user needs to set up running /api/payload-jobs/handleSchedules or payload.jobs.handleSchedules in regular intervals
6
tasks: [
7
{
8
schedule: [
9
{
10
cron: '* * * * * *',
11
queue: 'autorunSecond',
12
// Hooks are optional
13
hooks: {
14
// Not an array, as providing and calling `defaultBeforeSchedule` would be more error-prone if this was an array
15
beforeSchedule: async (args) => {
16
// Handles verifying that there are no jobs already scheduled or processing.
17
// You can override this behavior by not calling defaultBeforeSchedule, e.g. if you wanted
18
// to allow a maximum of 3 scheduled jobs in the queue instead of 1, or add any additional conditions
19
const result = await args.defaultBeforeSchedule(args)
20
return {
21
...result,
22
input: {
23
message: 'This task runs every second',
24
},
25
}
26
},
27
afterSchedule: async (args) => {
28
await args.defaultAfterSchedule(args) // Handles updating the payload-jobs-stats global
29
args.req.payload.logger.info(
30
'EverySecond task scheduled: ' +
31
(args.status === 'success' ? args.job.id : 'skipped or failed to schedule'),
32
)
33
},
34
},
35
},
36
],
37
slug: 'EverySecond',
38
inputSchema: [
39
{
40
name: 'message',
41
type: 'text',
42
required: true,
43
},
44
],
45
handler: ({ input, req }) => {
46
req.payload.logger.info(input.message)
47
return {
48
output: {},
49
}
50
},
51
}
52
]
53
}
54
})

You can now:

  • Define recurring workflows (e.g. nightly jobs to send digests)
  • Queue one-off jobs with specific execution timing
  • Leverage queues named per worker or cron strategy using payload.jobs.queue

Real-world scenarios:

  • Nightly backfills
  • Scheduled newsletter sync
  • Time-based status publishing