5.7 KiB
| title | aliases | tags | sources | created | updated | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Field Hooks |
|
|
|
2026-05-15 | 2026-05-15 |
Field Hooks run on individual fields during Document lifecycle events — allowing isolated, per-field logic separate from wiki/payloadcms/hooks-collections.
Config
Add to any field's hooks property:
const field: Field = {
name: 'username',
type: 'text',
hooks: {
beforeValidate: [(args) => { ... }],
beforeChange: [(args) => { ... }],
beforeDuplicate:[(args) => { ... }],
afterChange: [(args) => { ... }],
afterRead: [(args) => { ... }],
},
}
Each hook type accepts an array of sync or async functions. The return value replaces the field value for that operation.
GraphQL warning: Changing the type of data returned from a field breaks GraphQL. Use wiki/payloadcms/hooks-collections for type changes.
Hook Types
| Hook | Runs | Use for |
|---|---|---|
beforeValidate |
create + update, after client validation | Normalise/format input before server validation |
beforeChange |
create + update, before validation | Transform or gate data; data is unvalidated here |
afterChange |
create + update, after save | Side effects, logging, downstream triggers |
afterRead |
read operations | Format for output (dates, masks, derived values) |
beforeDuplicate |
document duplication | Avoid unique constraint violations; runs before beforeValidate |
Hook Execution Order (create/update)
beforeValidate(client → server)validate(server)beforeChange- DB write
afterChange
On read: afterRead.
On duplicate: beforeDuplicate → beforeValidate → beforeChange.
Arguments Reference
All field hooks receive the same args object:
| Arg | Type | Notes |
|---|---|---|
value |
any | Current field value |
data |
Partial<Doc> | Incoming data (create/update) or full doc (afterRead) |
originalDoc |
Doc | Doc before changes (update); resulting doc (afterChange) |
previousDoc |
Doc | Doc before changes in afterChange |
previousValue |
any | Previous field value — beforeChange and afterChange only |
siblingData |
object | Adjacent field values |
siblingFields |
Field[] | Adjacent field definitions |
previousSiblingDoc |
object | Sibling data before changes — beforeChange/afterChange only |
siblingDocWithLocales |
object | Sibling data with all locales |
operation |
string | 'create' | 'update' — useful for branching logic |
field |
Field | Field definition |
path |
string[] | Runtime path |
schemaPath |
string[] | Schema-level path |
collection |
Collection | null | null if field belongs to a Global |
global |
Global | null | null if field belongs to a Collection |
findMany |
boolean | afterRead only — differentiates find-one vs find-many |
overrideAccess |
boolean | Whether access control is bypassed |
context |
object | Shared context across hooks — see [[wiki/payloadcms/hooks-context |
req |
Request | Web request (mocked for Local API) |
Common Patterns
Normalise input (beforeValidate)
beforeValidate: [
({ value }) => value?.trim().toLowerCase(),
],
Branch on operation (beforeChange)
beforeChange: [
({ value, operation }) => {
if (operation === 'create') { /* extra logic */ }
return value
},
],
Log on change (afterChange)
afterChange: [
({ value, previousValue, req }) => {
if (value !== previousValue) {
console.log(`Changed: ${previousValue} → ${value}`)
}
},
],
Format for display (afterRead)
afterRead: [
({ value }) => new Date(value).toLocaleDateString(),
],
Deduplicate on copy (beforeDuplicate)
beforeDuplicate: [
({ value }) => (value ?? 0) + 1,
],
Default behaviour for text fields: Payload appends " - Copy" unless you define your own beforeDuplicate hook.
TypeScript
import type { FieldHook } from 'payload'
import type { Post } from '@/payload-types'
// Three generics: <DocumentType, ValueType, SiblingDataType>
type PostTitleHook = FieldHook<Post, string, Post>
const slugifyTitle: PostTitleHook = ({ value, siblingData }) => {
if (!siblingData.slug && value) {
return value.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
}
return value
}
Use all three generics for full autocomplete on value, data, siblingData, and originalDoc.
Key Takeaways
- Field hooks are the preferred place to isolate per-field logic — keeps Collection hooks clean.
operationarg lets you branchbeforeChange/afterChangebetween create and update.beforeChangereceives unvalidated data — validate manually if using for side effects.- Changing the return type breaks GraphQL — stick to same type or use Collection hooks.
beforeDuplicateruns beforebeforeValidate; default appends" - Copy"to unique text fields.- Use three-generic
FieldHook<Doc, Val, Sibling>for full TypeScript safety. context(fromreq.context) lets hooks share data without extra DB fetches — see wiki/payloadcms/hooks-context.
Related
- wiki/payloadcms/hooks-collections — document-level lifecycle hooks
- wiki/payloadcms/hooks-context — share state across hooks
- wiki/payloadcms/hooks — hook categories and execution model
- wiki/payloadcms/fields-overview — field config, validation, custom components
Sources
raw/hooks__fields.md— https://payloadcms.com/docs/hooks/fields