PayloadCMS from Scratch #4 - Practical use of hooks
Author
Adrian Maj
Date published

When performing operations such as saving, creating, or deleting records in collections, we often need to execute additional operations - save information to logs, revalidate cache, or integrate with external services. For these types of operations, called side-effects, we use hooks, which are an essential part of every larger application.
Payload allows creating hooks at four different levels - from the application level (Root Hook) down to individual fields in a collection (Field Hook). Each of these levels receives different data and has slightly different implementation, so let's take a detailed look at each one.
Root Hooks
As of writing this post, there's only one hook of this type available - afterError
. As the name suggests, it's triggered when an error occurs in the application. Root Hooks aren't tied to a specific collection but work across the entire application. The afterError
hook can be used, for example, to report errors to external monitoring services.
To add a Root Hook to your project, you need to add a hooks
property to the configuration object in the payload.config.ts
file, which is an object containing the names of available hooks. Each hook is an array of functions executed when it's called. The implementation looks like this:
1import { buildConfig } from 'payload'23export default buildConfig({4 // Your payload config5 hooks: {6 afterError: [7 async ({ error }) => {},8 ],9 },10})
Hooks can be both synchronous and asynchronous and receive different data depending on their type. You can read more about the afterError
hook and Root Hooks in the documentation.
Collection Hooks
Let's move on to the most commonly used hooks - for collections. The operating principle is similar to Root Hooks, except they work within a single collection and are defined there as well. There are significantly more hooks operating at the collection level, so I won't cover each one individually - you'll find their complete documentation here.
Collection hooks in most cases receive the following parameters:
collection
- the collection on which the hook operatesdoc
,originalDoc
orpreviousDoc
- the document on which they're called (for hooks responsible for document changes, variants before and after the operation are available)req
- Web Request object, through which you can gain access to the Local API, among other thingscontext
- additional context allowing data to be passed between different hooks (useful for avoiding infinite hook loops, for example)
Let's see what an example hook looks like:
1import type { CollectionConfig } from "payload";23export const Users: CollectionConfig = {4 slug: "users",5 hooks: {6 afterChange: [7 async ({ req, collection, doc }) => {8 const payload = req.payload;9 const tenant = await payload.findByID({10 collection: "tenants",11 id: typeof doc.tenant === "string" ? doc.tenant : doc.tenant.id12 })13 // bring your own logic here14 },15 ],16 },17}
In this example, the afterChange
hook uses the req
object to gain access to the Local API, then retrieves the tenant assigned to the user. The hook executes after every change to any field in the collection - the logic inside the hook is fully configurable and can be adapted to specific project requirements.
💡 Tip: Relational fields in the returned document (both in doc
and in Local API data) are always typed as the target collection object OR string. This is because types aren't generated based on permissions - if we lack access to the referenced document, we'll only receive its ID. Therefore, you should always verify the type, similar to the example with the id
field above.
Collection Hooks are fully typed, utilizing generic types - this means that when creating a hook, we must provide the slug or collection type (depending on the hook) for which we're defining it. Here are two examples of different typing approaches:
- Typing using collection slug
1import { type CollectionAfterOperationHook } from "payload";23export const afterOperationHook: CollectionAfterOperationHook<"collection-slug"> = async ({4 operation,5 req,6 result,7}) => {8 // some custom logic9 return result;10};11
- Typing using collection type
1import { type CollectionAfterChangeHook } from "payload";2import { type Reservation } from "@/payload-types";34export const afterChangeHook: CollectionAfterChangeHook<Reservation> = async ({5 doc,6 previousDoc,7 req,8}) => {9 // some custom logic10 return doc;11}
A hook prepared this way provides full type-safety and allows you to seamlessly add your own logic to existing operations.
Global Hooks
The configuration and operating principle of Global Hooks is practically identical to Collection Hooks, but we have significantly fewer hooks here - this results from the smaller number of operations possible to perform on global data. The only difference in parameters is replacing collection
with the global
parameter, which returns the global collection handling the given hook.
Since Global Hooks function almost identically to Collection Hooks, there's no point in discussing them in detail - if you need specific information about them, check the Payload documentation.
Field Hooks
Another important and very frequently used type of hooks, which allows you to react to changes in a specific field of a collection/global. Field Hooks are configured at the individual field level and receive slightly different data than Collection Hooks, which you'll find described in the documentation.
It's worth mentioning that Field Hooks typing differs from other hooks - in this case, we have one main type available - FieldHook
, which is also generic but takes as many as 3 arguments:
- Document Type - the type of collection in which this field is located.
- Field Type - if it's a text field, this will be string, etc.
- Sibling Data Type - the type of fields occurring at the same level as the field on which the hook is called (e.g., in the same group).
Here's what an example Field Hook might look like:
1import type { FieldHook } from 'payload'2import { type MyCollection } from "@/payload-types";34type SiblingDataType = {5 price: number;6 description: string;7}89const titleFieldHook: FieldHook<MyCollection, string, SiblingDataType> = (args) => {10 const {11 value, // Title - Typed as `string` as shown above12 data, // Typed as a Partial of MyCollection13 siblingData, // Typed as a Partial of SiblingDataType14 originalDoc, // Typed as MyCollection15 operation,16 req,17 } = args1819 // Do something here...2021 return value;22}
In this example, the hook handles the title
field and thanks to generic types, we get full type-safety for all parameters. The hook must return a value consistent with the field type - in this case string
, undefined
, or null
.
Summary
Hooks constitute a key mechanism for extending Payload's default functionality, enabling you to add your own logic to standard operations in a simple and pleasant way. In this article, you learned about four types of hooks - from Root Hooks operating within the entire application to Field Hooks reacting to changes in individual fields. Thanks to full typing and generic types, you can safely implement side-effects tailored to your project's specific requirements.
In the next article, we'll cover equally useful Custom Fields, which allow you to create your own field types that go beyond Payload's standard capabilities. You'll also learn practical applications of hooks combined with Custom Fields and ways to optimize application performance.
If you want to continue learning and dive deeper into PayloadCMS, I encourage you to follow my blog. Additionally, on my social media profiles, I'll be sharing information about new posts, updates, and projects related to Payload.

Learn how to manage users and define access control to data in PayloadCMS. Explore different types of users and leverage their potential.

Learn how to create and manage collections and global data in PayloadCMS. Understand their differences, and structure your content efficiently.