obsidian/wiki/payloadcms/hooks-fields.md
2026-05-15 15:51:51 +01:00

5.7 KiB

title aliases tags sources created updated
Field Hooks
field-hooks
payload-field-hooks
payloadcms
hooks
fields
lifecycle
raw/hooks__fields.md
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)

  1. beforeValidate (client → server)
  2. validate (server)
  3. beforeChange
  4. DB write
  5. afterChange

On read: afterRead. On duplicate: beforeDuplicatebeforeValidatebeforeChange.

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.
  • operation arg lets you branch beforeChange/afterChange between create and update.
  • beforeChange receives unvalidated data — validate manually if using for side effects.
  • Changing the return type breaks GraphQL — stick to same type or use Collection hooks.
  • beforeDuplicate runs before beforeValidate; default appends " - Copy" to unique text fields.
  • Use three-generic FieldHook<Doc, Val, Sibling> for full TypeScript safety.
  • context (from req.context) lets hooks share data without extra DB fetches — see wiki/payloadcms/hooks-context.

Sources