--- title: "Field Hooks" aliases: [field-hooks, payload-field-hooks] tags: [payloadcms, hooks, fields, lifecycle] sources: [raw/hooks__fields.md] created: 2026-05-15 updated: 2026-05-15 --- Field Hooks run on individual fields during Document lifecycle events — allowing isolated, per-field logic separate from [[wiki/payloadcms/hooks-collections|Collection Hooks]]. ## Config Add to any field's `hooks` property: ```ts 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|Collection Hooks]] 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: `beforeDuplicate` → `beforeValidate` → `beforeChange`. ## Arguments Reference All field hooks receive the same args object: | Arg | Type | Notes | |-----|------|-------| | `value` | any | Current field value | | `data` | Partial\ | 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|Hooks Context]] | | `req` | Request | Web request (mocked for Local API) | ## Common Patterns ### Normalise input (beforeValidate) ```ts beforeValidate: [ ({ value }) => value?.trim().toLowerCase(), ], ``` ### Branch on operation (beforeChange) ```ts beforeChange: [ ({ value, operation }) => { if (operation === 'create') { /* extra logic */ } return value }, ], ``` ### Log on change (afterChange) ```ts afterChange: [ ({ value, previousValue, req }) => { if (value !== previousValue) { console.log(`Changed: ${previousValue} → ${value}`) } }, ], ``` ### Format for display (afterRead) ```ts afterRead: [ ({ value }) => new Date(value).toLocaleDateString(), ], ``` ### Deduplicate on copy (beforeDuplicate) ```ts beforeDuplicate: [ ({ value }) => (value ?? 0) + 1, ], ``` Default behaviour for text fields: Payload appends `" - Copy"` unless you define your own `beforeDuplicate` hook. ## TypeScript ```ts import type { FieldHook } from 'payload' import type { Post } from '@/payload-types' // Three generics: type PostTitleHook = FieldHook 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` for full TypeScript safety. - `context` (from `req.context`) lets hooks share data without extra DB fetches — see [[wiki/payloadcms/hooks-context|Hooks Context]]. ## Related - [[wiki/payloadcms/hooks-collections|Collection Hooks]] — document-level lifecycle hooks - [[wiki/payloadcms/hooks-context|Hooks Context (req.context)]] — share state across hooks - [[wiki/payloadcms/hooks|Hooks Overview]] — hook categories and execution model - [[wiki/payloadcms/fields-overview|Fields Overview]] — field config, validation, custom components ## Sources - `raw/hooks__fields.md` — https://payloadcms.com/docs/hooks/fields