obsidian/wiki/payloadcms/queries.md
2026-05-15 16:32:18 +01:00

8.8 KiB

title aliases tags sources created updated
Payload CMS — Queries Overview
payload-queries
payload-where
payload-filtering
payloadcms
queries
filtering
local-api
rest-api
graphql
raw/queries__overview.md
https://payloadcms.com/docs/queries/overview
https://payloadcms.com/docs/queries/depth
https://payloadcms.com/docs/queries/pagination
https://payloadcms.com/docs/queries/select
https://payloadcms.com/docs/queries/sort
2026-05-15 2026-05-15

PayloadCMS — Queries

Key Takeaways

  • One language, three APIs — same Where object works in wiki/payloadcms/local-api, wiki/payloadcms/rest-api, and wiki/payloadcms/graphql-overview; only the wrapper syntax differs
  • 15 operators cover equality, comparison, text search, list membership, existence, and geospatial queries
  • and/or nesting can be arbitrarily deep for complex filter logic
  • Dot notation ('artists.featured') accesses nested and relationship fields without joins
  • REST queries use qs-esm — complex Where objects stringify cleanly; avoid hand-crafting query strings
  • all operator is MongoDB-only — do not use with Postgres/SQLite adapters
  • Performance stack: index: true + depth: 0 + limit: 1 + pagination: false + select combined is the fastest pattern for unique-field lookups
  • depth is ignored in GraphQL — shape your GraphQL query fields instead
  • Add index: true to any field queried or sorted frequently — biggest single perf win

Overview

Payload's querying language is uniform across all three APIs (Local, REST, GraphQL). You write the same logic; only the syntax wrapper differs.

  • Query operators work on any indexed or non-indexed field
  • and/or can be nested arbitrarily deep
  • Dot notation accesses nested/related fields
  • Add index: true to frequently queried fields for significant speed gains

Operators

Operator Use case
equals Exact match
not_equals Exclude exact value
greater_than / greater_than_equal Numeric or date
less_than / less_than_equal Numeric or date
like Case-insensitive substring; all words must be present
contains Case-insensitive substring (single value)
in Value in comma-delimited list
not_in Value NOT in comma-delimited list
all All values in list present (MongoDB only)
exists true = field present, false = field absent
near Point field: lng,lat,maxMeters,minMeters
within Point field: inside GeoJSON area
intersects Point field: intersects GeoJSON area

Where Syntax

Local API

import type { Where } from 'payload'

const posts = await payload.find({
  collection: 'posts',
  where: {
    color: { equals: 'blue' },
  },
})

REST API

GET /api/posts?where[color][equals]=blue

Use qs-esm for complex queries:

import { stringify } from 'qs-esm'

const query = stringify(
  { where: { color: { equals: 'mint' } } },
  { addQueryPrefix: true },
)
const res = await fetch(`/api/posts${query}`)

GraphQL

query {
  Posts(where: { color: { equals: mint } }) {
    docs { color }
    totalDocs
  }
}

And / Or Logic

const query: Where = {
  or: [
    { color: { equals: 'mint' } },
    {
      and: [
        { color: { equals: 'white' } },
        { featured: { equals: false } },
      ],
    },
  ],
}

Nested / Relational Fields (dot notation)

const query: Where = {
  'artists.featured': { exists: true },
}

Depth

Controls how many levels of relationships are auto-populated. Default: 2. Has no effect in GraphQL (use query shape instead).

depth Behavior
0 Return only IDs for related docs
1 Populate one level
2 (default) Populate two levels
// Local API
await payload.find({ collection: 'posts', depth: 1 })

// REST
fetch('/api/posts?depth=1')

Global default depth

export default buildConfig({
  defaultDepth: 1,
})

Per-field max depth (overrides request depth)

{
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
  maxDepth: 2,
}

Pagination

find queries are paginated by default.

Control Default Description
limit 10 Docs per page; 0 disables pagination
page 1 Page number
pagination true false = return all docs, skip count
// Local API
await payload.find({ collection: 'posts', limit: 10, page: 2 })

// REST
fetch('/api/posts?limit=10&page=2')

Response shape

{
  "docs": [...],
  "totalDocs": 6,
  "limit": 10,
  "totalPages": 1,
  "page": 1,
  "pagingCounter": 1,
  "hasPrevPage": false,
  "hasNextPage": false,
  "prevPage": null,
  "nextPage": null
}

Disable pagination (performance)

await payload.find({
  collection: 'posts',
  where: { slug: { equals: 'my-post' } },
  pagination: false,
  limit: 1,   // combine with limit for best perf
})

Sort

  • Ascending: field name (e.g. title)
  • Descending: prefix with - (e.g. -createdAt)
  • Multiple fields: array (Local) or comma-separated (REST/GraphQL)
  • Only stored (non-virtual) fields can be sorted
// Local API — single field
await payload.find({ collection: 'posts', sort: '-createdAt' })

// Local API — multiple fields
await payload.find({ collection: 'posts', sort: ['priority', '-createdAt'] })

// REST — single
fetch('/api/posts?sort=-createdAt')

// REST — multiple
fetch('/api/posts?sort=priority,-createdAt')

// GraphQL
// Posts(sort: "priority,-createdAt")

Select (Field Projection)

Returns only specified fields. Reduces response size and skips field hooks for unselected fields. id is always returned.

Include mode

await payload.find({
  collection: 'posts',
  select: {
    text: true,
    group: { number: true },  // nested field
    array: true,              // entire array
  },
})

Exclude mode

await payload.find({
  collection: 'posts',
  select: {
    array: false,
    group: { number: false },
  },
})

REST

GET /api/posts?select[color]=true&select[group][number]=true

Or with qs-esm:

const query = stringify({ select: { text: true, group: { number: true } } }, { addQueryPrefix: true })
fetch(`/api/posts${query}`)

Empty select → returns only id

const post = await payload.findByID({ collection: 'posts', id: '1', select: {} })
// { id: '1' }

Populate (override defaultPopulate)

defaultPopulate on a collection sets which fields are returned when that collection is populated as a relationship. Override per-request with populate:

// Collection config
export const Pages: CollectionConfig<'pages'> = {
  slug: 'pages',
  defaultPopulate: { slug: true },  // only return slug when populated
  fields: [{ name: 'slug', type: 'text' }],
}

// Override at query time (Local API)
await payload.find({
  collection: 'posts',
  populate: {
    pages: { text: true },  // override pages' defaultPopulate
  },
})

// REST
fetch('/api/posts?populate[pages][text]=true')

Upload collections with defaultPopulate: always include filename: true alongside url: true, otherwise url returns null.

Entity-level Select Config

Force fields to always be selected regardless of caller's select:

export const Posts: CollectionConfig = {
  slug: 'posts',
  select: ({ select }) => (select ? { ...select, title: true } : undefined),
  fields: [],
}

Return undefined to leave caller's select unchanged. This runs before the read; no per-document data available.

Performance Checklist

  • index: true on frequently queried/sorted fields
  • depth: 0 when you only need IDs
  • limit: 1 + pagination: false when querying by unique field (e.g. slug)
  • select to return only needed fields
  • defaultPopulate to limit relationship payload size

Gotchas

  • all operator only works with MongoDB adapter
  • depth is ignored by GraphQL — control population via query shape
  • beforeRead/afterRead hooks may not receive full doc when select is active
  • Sorting on virtual fields is not supported unless linked to a relationship field
  • limit: 0 has the same effect as pagination: false