Files
website-enchun-mgr/.agent/skills/payload-cms/references/hooks.md
pkupuk ad8e2e313e chore(agent): configure AI agents and tools
Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
2026-02-11 11:51:23 +08:00

6.7 KiB

Hooks Reference

Hook Lifecycle

Operation: CREATE
  beforeOperation → beforeValidate → beforeChange → [DB Write] → afterChange → afterOperation

Operation: UPDATE
  beforeOperation → beforeValidate → beforeChange → [DB Write] → afterChange → afterOperation

Operation: READ
  beforeOperation → beforeRead → [DB Read] → afterRead → afterOperation

Operation: DELETE
  beforeOperation → beforeDelete → [DB Delete] → afterDelete → afterOperation

Collection Hooks

beforeValidate

Transform data before validation runs:

hooks: {
  beforeValidate: [
    async ({ data, operation, req }) => {
      if (operation === 'create') {
        data.createdBy = req.user?.id
      }
      return data // Always return data
    },
  ],
}

beforeChange

Transform data before database write (after validation):

hooks: {
  beforeChange: [
    async ({ data, operation, originalDoc, req }) => {
      // Auto-generate slug on create
      if (operation === 'create' && data.title) {
        data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
      }

      // Track last modified by
      data.lastModifiedBy = req.user?.id

      return data
    },
  ],
}

afterChange

Side effects after database write:

hooks: {
  afterChange: [
    async ({ doc, operation, req, context }) => {
      // Prevent infinite loops
      if (context.skipAuditLog) return doc

      // Create audit log entry
      await req.payload.create({
        collection: 'audit-logs',
        data: {
          action: operation,
          collection: 'posts',
          documentId: doc.id,
          userId: req.user?.id,
          timestamp: new Date(),
        },
        req, // CRITICAL: maintains transaction
        context: { skipAuditLog: true },
      })

      return doc
    },
  ],
}

beforeRead

Modify query before database read:

hooks: {
  beforeRead: [
    async ({ doc, req }) => {
      // doc is the raw database document
      // Can modify before afterRead transforms
      return doc
    },
  ],
}

afterRead

Transform data before sending to client:

hooks: {
  afterRead: [
    async ({ doc, req }) => {
      // Add computed field
      doc.fullName = `${doc.firstName} ${doc.lastName}`

      // Hide sensitive data for non-admins
      if (!req.user?.roles?.includes('admin')) {
        delete doc.internalNotes
      }

      return doc
    },
  ],
}

beforeDelete

Pre-delete validation or cleanup:

hooks: {
  beforeDelete: [
    async ({ id, req }) => {
      // Cascading delete: remove related comments
      await req.payload.delete({
        collection: 'comments',
        where: { post: { equals: id } },
        req,
      })
    },
  ],
}

afterDelete

Post-delete cleanup:

hooks: {
  afterDelete: [
    async ({ doc, req }) => {
      // Clean up uploaded files
      if (doc.image) {
        await deleteFile(doc.image.filename)
      }
    },
  ],
}

Field Hooks

Hooks on individual fields:

{
  name: 'slug',
  type: 'text',
  hooks: {
    beforeValidate: [
      ({ value, data }) => {
        if (!value && data?.title) {
          return data.title.toLowerCase().replace(/\s+/g, '-')
        }
        return value
      },
    ],
    afterRead: [
      ({ value }) => value?.toLowerCase(),
    ],
  },
}

Context Pattern

Prevent infinite loops and share state between hooks:

hooks: {
  afterChange: [
    async ({ doc, req, context }) => {
      // Check context flag to prevent loops
      if (context.skipNotification) return doc

      // Trigger related update with context flag
      await req.payload.update({
        collection: 'related',
        id: doc.relatedId,
        data: { updated: true },
        req,
        context: {
          ...context,
          skipNotification: true, // Prevent loop
        },
      })

      return doc
    },
  ],
}

Transactions

CRITICAL: Always pass req for transaction integrity:

hooks: {
  afterChange: [
    async ({ doc, req }) => {
      // ✅ Same transaction - atomic
      await req.payload.create({
        collection: 'audit-logs',
        data: { documentId: doc.id },
        req, // REQUIRED
      })

      // ❌ Separate transaction - can leave inconsistent state
      await req.payload.create({
        collection: 'audit-logs',
        data: { documentId: doc.id },
        // Missing req!
      })

      return doc
    },
  ],
}

Next.js Revalidation with Context Control

import { revalidatePath, revalidateTag } from 'next/cache'

hooks: {
  afterChange: [
    async ({ doc, context }) => {
      // Skip revalidation for internal updates
      if (context.skipRevalidation) return doc

      revalidatePath(`/posts/${doc.slug}`)
      revalidateTag('posts')

      return doc
    },
  ],
}

Auth Hooks (Auth Collections Only)

export const Users: CollectionConfig = {
  slug: 'users',
  auth: true,
  hooks: {
    afterLogin: [
      async ({ doc, req }) => {
        // Log login
        await req.payload.create({
          collection: 'login-logs',
          data: { userId: doc.id, timestamp: new Date() },
          req,
        })
        return doc
      },
    ],
    afterLogout: [
      async ({ req }) => {
        // Clear session data
      },
    ],
    afterMe: [
      async ({ doc, req }) => {
        // Add extra user info
        return doc
      },
    ],
    afterRefresh: [
      async ({ doc, req }) => {
        // Custom token refresh logic
        return doc
      },
    ],
    afterForgotPassword: [
      async ({ args }) => {
        // Custom forgot password notification
      },
    ],
  },
  fields: [...],
}

Hook Arguments Reference

All hooks receive these base arguments:

Argument Description
req Request object with payload, user, locale
context Shared context object between hooks
collection Collection config

Operation-specific arguments:

Hook Additional Arguments
beforeValidate data, operation, originalDoc
beforeChange data, operation, originalDoc
afterChange doc, operation, previousDoc
beforeRead doc
afterRead doc
beforeDelete id
afterDelete doc, id

Best Practices

  1. Always return the data/doc - Even if unchanged
  2. Use context for loop prevention - Check before triggering recursive operations
  3. Pass req for transactions - Maintains atomicity
  4. Keep hooks focused - One responsibility per hook
  5. Use field hooks for field-specific logic - Better encapsulation
  6. Avoid heavy operations in beforeRead - Runs on every query
  7. Use afterChange for side effects - Email, webhooks, etc.