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.

Hooks Overview

Hooks allow you to execute your own side effects during specific events of the Document lifecycle. They allow you to do things like mutate data, perform business logic, integrate with third-parties, or anything else, all during precise moments within your application.

With Hooks, you can transform Payload from a traditional CMS into a fully-fledged application framework. There are many use cases for Hooks, including:

  • Modify data before it is read or updated
  • Encrypt and decrypt sensitive data
  • Integrate with a third-party CRM like HubSpot or Salesforce
  • Send a copy of uploaded files to Amazon S3 or similar
  • Process orders through a payment provider like Stripe
  • Send emails when contact forms are submitted
  • Track data ownership or changes over time

There are four main types of Hooks in Payload:

Root Hooks

Root Hooks are not associated with any specific Collection, Global, or Field. They are useful for globally-oriented side effects, such as when an error occurs at the application level.

To add Root Hooks, use the hooks property in your Payload Config:

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ...
5
hooks: {
6
afterError:[() => {...}]
7
},
8
})

The following options are available:

Option

Description

afterError

Runs after an error occurs in the Payload application.

afterError

The afterError Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code.

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ...
5
hooks: {
6
afterError: [
7
async ({ error }) => {
8
// Do something
9
},
10
],
11
},
12
})

The following arguments are provided to the afterError Hook:

Argument

Description

error

The error that occurred.

context

Custom context passed between Hooks. More details.

graphqlResult

The GraphQL result object, available if the hook is executed within a GraphQL context.

req

The PayloadRequest object that extends Web Request. Contains currently authenticated user and the Local API instance payload.

collection

The Collection in which this Hook is running against. This will be undefined if the hook is executed from a non-collection endpoint or GraphQL.

result

The formatted error result object, available if the hook is executed from a REST context.

Awaited vs. non-blocking hooks

Hooks can either block the request until they finish or run without blocking it. What matters is whether your hook returns a Promise.

Awaited (blocking): If your hook returns a Promise (for example, if it’s declared async), Payload will wait for it to resolve before continuing that lifecycle step. Use this when your hook needs to modify data or influence the response. Hooks that return Promises run in series at the same lifecycle stage.

Non-blocking (sometimes called “fire-and-forget”): If your hook does not return a Promise (returns nothing), Payload will not wait for it to finish. This can be useful for side-effects that don’t affect the outcome of the operation, but keep in mind that any work started this way might continue after the request has already completed.

Declaring a function with async does not make it “synchronous.” The async keyword simply makes the function return a Promise automatically — which is why Payload then awaits it.

Awaited

1
const beforeChange = async ({ data }) => {
2
const enriched = await fetchProfile(data.userId) // Payload waits here
3
return { ...data, profile: enriched }
4
}

Non-blocking

1
const afterChange = ({ doc }) => {
2
// Trigger side-effect without blocking
3
void pingAnalyticsService(doc.id)
4
// No return → Payload does not wait
5
}

Server-only Execution

Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client.

Performance

Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized.

When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be.

Writing efficient hooks

Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often.

For example, the read operation runs on every read request, so avoid putting expensive logic in a beforeRead or afterRead hook.

1
{
2
hooks: {
3
beforeRead: [
4
async () => {
5
// This runs on every read request - avoid expensive logic here
6
await doSomethingExpensive()
7
return data
8
},
9
],
10
},
11
}

Instead, you might want to use a beforeChange or afterChange hook, which only runs when a document is created or updated.

1
{
2
hooks: {
3
beforeChange: [
4
async ({ context }) => {
5
// This is more acceptable here, although still should be mindful of performance
6
await doSomethingExpensive()
7
// ...
8
},
9
]
10
},
11
}

Using Hook Context

Use Hook Context to avoid infinite loops or to prevent repeating expensive operations across multiple hooks in the same request.

1
{
2
hooks: {
3
beforeChange: [
4
async ({ context }) => {
5
const somethingExpensive = await doSomethingExpensive()
6
context.somethingExpensive = somethingExpensive
7
// ...
8
},
9
],
10
},
11
}

To learn more, see the Hook Context documentation.

Offloading to the jobs queue

If your hooks perform any long-running tasks that don't directly affect the request lifecycle, consider offloading them to the jobs queue. This will free up the request to continue processing without waiting for the task to complete.

1
{
2
hooks: {
3
afterChange: [
4
async ({ doc, req }) => {
5
// Offload to job queue
6
await req.payload.jobs.queue(...)
7
// ...
8
},
9
],
10
},
11
}

To learn more, see the Job Queue documentation.

Next

Collection Hooks