---
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\<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|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: <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|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
