payload

Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.

Install
npx skills add 'https://github.com/payloadcms/payload/tree/main/packages/payload/skills/payload'
Download bundle ↓
main · 9a2cdcbScanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

Payload Field Types Reference

Complete reference for all Payload field types with examples.

Text Field

import type { TextField } from 'payload'

const textField: TextField = {
  name: 'title',
  type: 'text',
  required: true,
  unique: true,
  minLength: 5,
  maxLength: 100,
  index: true,
  localized: true,
  defaultValue: 'Default Title',
  validate: (value) => Boolean(value) || 'Required',
  admin: {
    placeholder: 'Enter title...',
    position: 'sidebar',
    condition: (data) => data.showTitle === true,
  },
}

When to use position: 'sidebar': Reserve the sidebar for short fields that give quick insight into the content — status, category, author, publish date, slug. Avoid it for fields that need horizontal space to be useful, like a description, rich text content, or long text — those belong in the main document area. (title above is shown in the sidebar only to demonstrate the option.)

Slug Field

Use the native slug field type for slugs — never hand-roll a { type: 'text', unique: true } field. useAsSlug is required (names the source field; there is no 'title' default). Defaults to required, unique, index, position: 'sidebar'. Optional server-side slugify fn.

{ name: 'slug', type: 'slug', useAsSlug: 'title' }

Full prop reference: https://payloadcms.com/docs/fields/slug

Rich Text (Lexical)

import type { RichTextField } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { HeadingFeature, LinkFeature } from '@payloadcms/richtext-lexical'

const richTextField: RichTextField = {
  name: 'content',
  type: 'richText',
  required: true,
  localized: true,
  editor: lexicalEditor({
    features: ({ defaultFeatures }) => [
      ...defaultFeatures,
      HeadingFeature({
        enabledHeadingSizes: ['h1', 'h2', 'h3'],
      }),
      LinkFeature({
        enabledCollections: ['posts', 'pages'],
      }),
    ],
  }),
}

Advanced Lexical Configuration

import {
  BoldFeature,
  FixedToolbarFeature,
  HeadingFeature,
  IndentFeature,
  InlineToolbarFeature,
  ItalicFeature,
  LinkFeature,
  OrderedListFeature,
  TableFeature,
  UnderlineFeature,
  UnorderedListFeature,
  lexicalEditor,
} from '@payloadcms/richtext-lexical'

// Global editor config with full features
export default buildConfig({
  editor: lexicalEditor({
    features: () => {
      return [
        UnderlineFeature(),
        BoldFeature(),
        ItalicFeature(),
        OrderedListFeature(),
        UnorderedListFeature(),
        LinkFeature({
          enabledCollections: ['pages'],
          fields: ({ defaultFields }) => {
            const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
              if ('name' in field && field.name === 'url') return false
              return true
            })

            return [
              ...defaultFieldsWithoutUrl,
              {
                name: 'url',
                type: 'text',
                admin: {
                  condition: ({ linkType }) => linkType !== 'internal',
                },
                label: ({ t }) => t('fields:enterURL'),
                required: true,
              },
            ]
          },
        }),
        IndentFeature(),
        TableFeature(),
      ]
    },
  }),
})

// Field-specific editor with custom toolbar
const richTextWithToolbars: RichTextField = {
  name: 'richText',
  type: 'richText',
  editor: lexicalEditor({
    features: ({ rootFeatures }) => {
      return [
        ...rootFeatures,
        HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
        FixedToolbarFeature(),
        InlineToolbarFeature(),
      ]
    },
  }),
  label: false,
}

Relationship

import type { RelationshipField } from 'payload'

// Single relationship
const singleRelationship: RelationshipField = {
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
  required: true,
  maxDepth: 2,
}

// Multiple relationships (hasMany)
const multipleRelationship: RelationshipField = {
  name: 'categories',
  type: 'relationship',
  relationTo: 'categories',
  hasMany: true,
  filterOptions: {
    active: { equals: true },
  },
}

// Polymorphic relationship
const polymorphicRelationship: PolymorphicRelationshipField = {
  name: 'relatedContent',
  type: 'relationship',
  relationTo: ['posts', 'pages'],
  hasMany: true,
}

Array

import type { ArrayField } from 'payload'

const arrayField: ArrayField = {
  name: 'slides',
  type: 'array',
  minRows: 2,
  maxRows: 10,
  labels: {
    singular: 'Slide',
    plural: 'Slides',
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'image',
      type: 'upload',
      relationTo: 'media',
    },
  ],
  admin: {
    initCollapsed: true,
  },
}

Blocks

import type { BlocksField, Block } from 'payload'

const HeroBlock: Block = {
  slug: 'hero',
  interfaceName: 'HeroBlock',
  fields: [
    {
      name: 'heading',
      type: 'text',
      required: true,
    },
    {
      name: 'background',
      type: 'upload',
      relationTo: 'media',
    },
  ],
}

const ContentBlock: Block = {
  slug: 'content',
  fields: [
    {
      name: 'text',
      type: 'richText',
    },
  ],
}

const blocksField: BlocksField = {
  name: 'layout',
  type: 'blocks',
  blocks: [HeroBlock, ContentBlock],
}

Select

import type { SelectField } from 'payload'

// Use select for genuine taxonomy. For publish state, enable versions.drafts
// and rely on the auto-injected _status field instead of a custom select.
const selectField: SelectField = {
  name: 'priority',
  type: 'select',
  options: [
    { label: 'Low', value: 'low' },
    { label: 'Medium', value: 'medium' },
    { label: 'High', value: 'high' },
  ],
  defaultValue: 'medium',
  required: true,
}

// Multiple select
const multiSelectField: SelectField = {
  name: 'tags',
  type: 'select',
  hasMany: true,
  options: ['tech', 'news', 'sports'],
}

Upload

import type { UploadField } from 'payload'

const uploadField: UploadField = {
  name: 'featuredImage',
  type: 'upload',
  relationTo: 'media',
  required: true,
  filterOptions: {
    mimeType: { contains: 'image' },
  },
}

Point (Geolocation)

Point fields store geographic coordinates with automatic 2dsphere indexing for geospatial queries.

import type { PointField } from 'payload'

const locationField: PointField = {
  name: 'location',
  type: 'point',
  label: 'Location',
  required: true,
}

// Returns [longitude, latitude]
// Example: [-122.4194, 37.7749] for San Francisco

Geospatial Queries

// Query by distance (sorted by nearest first)
const nearbyLocations = await payload.find({
  collection: 'stores',
  where: {
    location: {
      near: [10, 20], // [longitude, latitude]
      maxDistance: 5000, // in meters
      minDistance: 1000,
    },
  },
})

// Query within polygon area
const polygon: Point[] = [
  [9.0, 19.0], // bottom-left
  [9.0, 21.0], // top-left
  [11.0, 21.0], // top-right
  [11.0, 19.0], // bottom-right
  [9.0, 19.0], // closing point
]

const withinArea = await payload.find({
  collection: 'stores',
  where: {
    location: {
      within: {
        type: 'Polygon',
        coordinates: [polygon],
      },
    },
  },
})

// Query intersecting area
const intersecting = await payload.find({
  collection: 'stores',
  where: {
    location: {
      intersects: {
        type: 'Polygon',
        coordinates: [polygon],
      },
    },
  },
})

Note: Point fields are not supported in SQLite.

Join Fields

Join fields create reverse relationships, allowing you to access related documents from the "other side" of a relationship.

import type { JoinField } from 'payload'

// From Users collection - show user's orders
const ordersJoinField: JoinField = {
  name: 'orders',
  type: 'join',
  collection: 'orders',
  on: 'customer', // The field in 'orders' that references this user
  admin: {
    allowCreate: false,
    defaultColumns: ['id', 'createdAt', 'total', 'currency', 'items'],
  },
}

// From Users collection - show user's cart
const cartJoinField: JoinField = {
  name: 'cart',
  type: 'join',
  collection: 'carts',
  on: 'customer',
  admin: {
    allowCreate: false,
    defaultColumns: ['id', 'createdAt', 'total', 'currency'],
  },
}

Virtual Fields

import type { TextField } from 'payload'

// Computed from siblings
const computedVirtualField: TextField = {
  name: 'fullName',
  type: 'text',
  virtual: true,
  hooks: {
    afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
  },
}

// From relationship path
const pathVirtualField: TextField = {
  name: 'authorName',
  type: 'text',
  virtual: 'author.name',
}

Do not set admin.useAsTitle to a computed field configured with virtual: true. Computed virtual fields are not queryable, and Payload rejects them as collection titles. Use a stored top-level field instead. The string-path form, such as virtual: 'author.name', is queryable and can be used as admin.useAsTitle when the title must come from a related document.

Conditional Fields

import type { UploadField, CheckboxField } from 'payload'

// Simple boolean condition
const enableFeatureField: CheckboxField = {
  name: 'enableFeature',
  type: 'checkbox',
}

const conditionalField: TextField = {
  name: 'featureText',
  type: 'text',
  admin: {
    condition: (data) => data.enableFeature === true,
  },
}

// Sibling data condition (from hero field pattern)
const typeField: SelectField = {
  name: 'type',
  type: 'select',
  options: ['none', 'highImpact', 'mediumImpact', 'lowImpact'],
  defaultValue: 'lowImpact',
}

const mediaField: UploadField = {
  name: 'media',
  type: 'upload',
  relationTo: 'media',
  admin: {
    condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type),
  },
  required: true,
}

Radio

Radio fields present options as radio buttons for single selection.

import type { RadioField } from 'payload'

const radioField: RadioField = {
  name: 'priority',
  type: 'radio',
  options: [
    { label: 'Low', value: 'low' },
    { label: 'Medium', value: 'medium' },
    { label: 'High', value: 'high' },
  ],
  defaultValue: 'medium',
  admin: {
    layout: 'horizontal', // or 'vertical'
  },
}

Row (Layout)

Row fields arrange fields horizontally in the admin panel (presentational only).

import type { RowField } from 'payload'

const rowField: RowField = {
  type: 'row',
  fields: [
    {
      name: 'firstName',
      type: 'text',
      admin: { width: '50%' },
    },
    {
      name: 'lastName',
      type: 'text',
      admin: { width: '50%' },
    },
  ],
}

Collapsible (Layout)

Collapsible fields group fields in an expandable/collapsible section.

import type { CollapsibleField } from 'payload'

const collapsibleField: CollapsibleField = {
  label: ({ data }) => data?.title || 'Advanced Options',
  type: 'collapsible',
  admin: {
    initCollapsed: true,
  },
  fields: [
    { name: 'customCSS', type: 'textarea' },
    { name: 'customJS', type: 'code' },
  ],
}

UI (Custom Components)

UI fields allow fully custom React components in the admin (no data stored).

import type { UIField } from 'payload'

const uiField: UIField = {
  name: 'customMessage',
  type: 'ui',
  admin: {
    components: {
      Field: '/path/to/CustomFieldComponent',
      Cell: '/path/to/CustomCellComponent', // For list view
    },
  },
}

Tabs & Groups

import type { TabsField, GroupField } from 'payload'

// Tabs
const tabsField: TabsField = {
  type: 'tabs',
  tabs: [
    {
      label: 'Content',
      fields: [
        { name: 'title', type: 'text' },
        { name: 'body', type: 'richText' },
      ],
    },
    {
      label: 'SEO',
      fields: [
        { name: 'metaTitle', type: 'text' },
        { name: 'metaDescription', type: 'textarea' },
      ],
    },
  ],
}

// Group (named)
const groupField: GroupField = {
  name: 'meta',
  type: 'group',
  fields: [
    { name: 'title', type: 'text' },
    { name: 'description', type: 'textarea' },
  ],
}

Reusable Field Factories

Create composable field patterns that can be customized with overrides.

import type { Field, GroupField } from 'payload'

// Utility for deep merging
const deepMerge = <T>(target: T, source: Partial<T>): T => {
  // Implementation would deeply merge objects
  return { ...target, ...source }
}

// Reusable link field factory
type LinkType = (options?: {
  appearances?: ('default' | 'outline')[] | false
  disableLabel?: boolean
  overrides?: Record<string, unknown>
}) => GroupField

export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
  const linkField: GroupField = {
    name: 'link',
    type: 'group',
    admin: {
      hideGutter: true,
    },
    fields: [
      {
        type: 'row',
        fields: [
          {
            name: 'type',
            type: 'radio',
            options: [
              { label: 'Internal link', value: 'reference' },
              { label: 'Custom URL', value: 'custom' },
            ],
            defaultValue: 'reference',
            admin: {
              layout: 'horizontal',
              width: '50%',
            },
          },
          {
            name: 'newTab',
            type: 'checkbox',
            label: 'Open in new tab',
            admin: {
              width: '50%',
              style: {
                alignSelf: 'flex-end',
              },
            },
          },
        ],
      },
      {
        name: 'reference',
        type: 'relationship',
        relationTo: ['pages'],
        required: true,
        maxDepth: 1,
        admin: {
          condition: (_, siblingData) => siblingData?.type === 'reference',
        },
      },
      {
        name: 'url',
        type: 'text',
        label: 'Custom URL',
        required: true,
        admin: {
          condition: (_, siblingData) => siblingData?.type === 'custom',
        },
      },
    ],
  }

  if (!disableLabel) {
    linkField.fields.push({
      name: 'label',
      type: 'text',
      required: true,
    })
  }

  if (appearances !== false) {
    linkField.fields.push({
      name: 'appearance',
      type: 'select',
      defaultValue: 'default',
      options: [
        { label: 'Default', value: 'default' },
        { label: 'Outline', value: 'outline' },
      ],
    })
  }

  return deepMerge(linkField, overrides) as GroupField
}

// Usage
const navItem = link({ appearances: false })
const ctaButton = link({
  overrides: {
    name: 'cta',
    admin: {
      description: 'Call to action button',
    },
  },
})

Field Type Guards

Type guards for runtime field type checking and safe type narrowing.

Type GuardChecks ForUse When
fieldAffectsDataField stores data (has name, not UI-only)Need to access field data or name
fieldHasSubFieldsField contains nested fields (group/array/row/collapsible)Need to recursively traverse fields
fieldIsArrayTypeField is array typeDistinguish arrays from other containers
fieldIsBlockTypeField is blocks typeHandle blocks-specific logic
fieldIsGroupTypeField is group typeHandle group-specific logic
fieldSupportsManyField can have multiple values (select/relationship/upload)Check for hasMany support
fieldHasMaxDepthField supports population depth controlControl relationship/upload/join depth
fieldIsPresentationalOnlyField is UI-only (no data storage)Exclude from data operations
fieldIsSidebarField positioned in sidebarSeparate sidebar rendering
fieldIsIDField name is 'id'Special ID field handling
fieldIsHiddenOrDisabledField is hidden or disabledFilter from UI operations
fieldShouldBeLocalizedField needs localization handlingProper locale table checks
fieldIsVirtualField is virtual (computed/no DB column)Skip in database transforms
tabHasNameTab is named (stores data)Distinguish named vs unnamed tabs
groupHasNameGroup is named (stores data)Distinguish named vs unnamed groups
optionIsObjectOption is {label, value} formatAccess option properties safely
optionsAreObjectsAll options are objectsBatch option processing
optionIsValueOption is string valueHandle string options
valueIsValueWithRelationValue is polymorphic relationshipHandle polymorphic relationships
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload'

function processField(field: Field) {
  if (fieldAffectsData(field)) {
    // Safe to access field.name
    console.log(field.name)
  }

  if (fieldHasSubFields(field)) {
    // Safe to access field.fields
    field.fields.forEach(processField)
  }
}

See FIELD-TYPE-GUARDS.md for detailed usage patterns.

Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 13.
| ------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- || Auto-generate slugs      | `{ type: 'slug', useAsSlug: 'title' }`                                     | [FIELDS.md#slug-field](reference/FIELDS.md#slug-field)                                                                           || Restrict content by user | Access control with query                                                  | [ACCESS-CONTROL.md#row-level-security-with-complex-queries](reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries) |
SKILL.mdView in source ↗
Source excerpt starting at line 17.
| Draft/publish workflow   | `versions: { drafts: true }`                                               | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts)                                                 || Computed fields          | `virtual: true` with **field-level** `hooks.afterRead` returning the value | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields)                                                                   || Document titles          | Stored top-level field in `admin.useAsTitle`                               | [COLLECTIONS.md#useastitle](reference/COLLECTIONS.md#useastitle)                                                                 || Conditional fields       | `admin.condition`                                                          | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields)                                                           || Custom field validation  | `validate` function                                                        | [FIELDS.md#validation](reference/FIELDS.md#validation)                                                                           || Filter relationship list | `filterOptions` on field                                                   | [FIELDS.md#relationship](reference/FIELDS.md#relationship)                                                                       || Select specific fields   | `select` parameter                                                         | [QUERIES.md#field-selection](reference/QUERIES.md#field-selection)                                                               |
SKILL.mdView in source ↗
Source excerpt starting at line 26.
| Cascading deletes        | beforeDelete hook                                                          | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks)                                                                 || Geospatial queries       | `point` field with `near`/`within`                                         | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation)                                                             || Reverse relationships    | `join` field type                                                          | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields)                                                                         || Next.js revalidation     | Context control in afterChange                                             | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control)                 |
SKILL.mdView in source ↗
Source excerpt starting at line 158.
For all field types (array, blocks, point, join, virtual, conditional, etc.), see [FIELDS.md](reference/FIELDS.md).
SKILL.mdView in source ↗
Source excerpt starting at line 508.
- **[FIELDS.md](reference/FIELDS.md)** - All field types, validation, admin options- **[FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md)** - Type guards for runtime field type checking and narrowing