Payload CMS's admin.listSearchableFields only matches fields that live on the same collection as the one you're searching. If the value an editor actually needs to search by — a reference code, an external ID, anything — lives on a related collection instead, the admin search box returns nothing. No error, no warning in the console, just an empty result set that looks like a bug in your data.
The fix doesn't require replacing Payload's List view or standing up a custom search endpoint. It's a single beforeOperation hook on the parent collection's find operation: detect the where clause Payload's admin generates from a search term, resolve matching IDs from the related collection, and append them to the same or condition. This guide walks through exactly how to build that hook, based on the internal where-clause shape in Payload's own source — which isn't documented anywhere — tested against Payload 3.88.0.
I ran into this while adding search to two related collections in a Payload e-commerce setup: a parent orders collection and a child line-items collection carrying the actual reference codes. Editors needed to paste a reference code into the Orders list search box and land on the right order — the natural way anyone searches an order list. listSearchableFields looked like the obvious answer, right up until I typed a reference code into the search box and got nothing back, even though the matching line item was three clicks away.
The problem: listSearchableFields only searches the collection it's set on
referenceCode doesn't exist on orders — it's a field on the child line-items collection, linked back via a order relationship field. Payload doesn't validate listSearchableFields against the collection's own schema, so this config doesn't throw. It just quietly does nothing for that field: the generated query only ever references fields Payload can actually find on , so a search for a reference code matches zero documents.
This is easy to miss in testing, because searching by orderNumber still works fine. The bug only shows up when someone searches by the field that was never really wired up.
How the admin search box actually becomes a query
To fix this, it helps to know exactly what happens between typing into the search box and Payload running a database query — because the natural instinct is to look for a raw search string somewhere in a hook, and that string doesn't exist by the time hooks run.
The admin List view sends a plain ?search= query parameter to the server — it does not build a where clause client-side. That parameter gets converted into an actual where clause server-side, inside @payloadcms/next's list view renderer, before the Local API's find() is ever called. The function responsible is , and its source (from ) is short enough to read in full:
For a collection configured with listSearchableFields: ["orderNumber"] and a search term of "9142", this produces:
json
{"or":[{"orderNumber":{"like":"9142"}}]}
If the list already had another filter applied — a status filter, a baseListFilter, anything — hoistQueryParamsToAnd nests both together under an and, so the exact shape you'll see in practice can be more deeply nested than the bare example above. The important part is that this all happens before any collection hook runs. By the time your code sees the request, there is no search string left anywhere — only this generated where object.
Why beforeOperation is the right hook point
Payload's find operation runs a beforeOperation hook right at the start, before access control is applied and before the query is sent to the database. Here's the relevant part of payload/dist/collections/operations/find.js:
And buildBeforeOperation confirms that mutating args.where in place — without needing to return a brand-new object — is enough for the change to persist through to the executed query:
One detail worth knowing before you write the hook: Payload maps both find and findByID to the same hook operation name, — a backward-compatible alias. That means your hook needs to distinguish the two itself. A call's has a property; a call's has an property instead. Checking before doing anything else keeps the hook from running on single-document lookups where it has nothing to do.
Implementation: searching line-item reference codes from the Orders list
With the mechanism clear, the fix is two pieces: the listSearchableFields config for the field that does live on orders, and a hook that extends the same search to referenceCode on line-items.
listSearchableFields only lists orderNumber here, since that's the only field the search box can natively match on this collection. The hook is what extends the match to the related collection.
Three things are doing the actual work here. walks the tree — through any / nesting might have introduced — looking for the exact array produces for this collection's . It returns that array by reference, not a copy, which matters for the next step. pulls the literal search string back out of that shape. And once a term is found, the hook queries for a matching , collects the parent IDs, and pushes one more condition — — directly into the same array reference returned. Because it's the same array object, no reassignment of is needed; the mutation is visible to once the hook returns.
If there's no search term at all — a normal list page load — extractSearchTerm returns undefined and the hook returns immediately. The sub-query against line-items only ever runs when someone is actually searching.
Gotchas
The biggest one: this hook works by pattern-matching an internal shape that Payload's own admin code generates, and that shape is not a public, documented contract. A future Payload release could change how mergeListSearchAndWhere builds its where clause, and if it does, isSearchOrConditions would stop matching — the augmentation would silently stop firing while listSearchableFields itself kept working normally. Re-verify this after any Payload version bump; it's a five-minute smoke test, not a rewrite.
Two smaller things matter for correctness and performance. First, index the field you're sub-querying — referenceCode in this example — the same way you'd index any field you expect to filter large tables on; the extra query only costs what a normal indexed lookup costs. Second, pass req through to the sub-query and leave at its default (), rather than setting it to . That keeps the related collection's normal access control — tenant scoping, publish-state checks, whatever you have — applied to the IDs this hook is allowed to surface, instead of quietly bypassing it. If you're touching inside hooks at all, it's worth understanding the transaction pitfalls that come with it — see for two patterns that keep background work and the original request's transaction from interfering with each other.
There's more than one way to solve "search should reach into a relationship," and which one is worth it depends on how far you need to go:
For most admin panels, the hook is the right default: it's a few dozen lines, it doesn't touch the UI, and it degrades gracefully — if the shape detection ever fails to match, you're back to exactly the behavior you had before adding it, not a broken search box.
Frequently asked questions
Can I just build a custom search box instead?
Yes — Payload supports overriding List view components, including replacing the search UI entirely with your own. That's the right call if you need something the native search box fundamentally can't do, like faceted search or matching across three or more related collections with different weighting. For most single-relationship cases, though, it's a lot more code than the hook above for the same end result. If you do go that route, Payload CMS Custom Admin Fields and Views covers the @payloadcms/ui primitives you'd build it from.
Does this affect the REST or GraphQL API, or only the admin UI?
Only the admin UI's generated search. mergeListSearchAndWhere only runs inside the admin panel's server-rendered List view — the REST and GraphQL find handlers never see a search parameter; they only ever accept an explicit clause you construct yourself. This hook still runs for REST/GraphQL requests, since is a collection-level hook, but it only does anything when the incoming happens to match the exact shape the admin search box produces — a hand-built API request with a different shape simply passes through unaffected.
What if I need to match against more than one related collection?
Repeat the sub-query for each related collection, collect each set of matching parent IDs separately, and push one { id: { in: [...] } } condition per collection into the same or array. They all get OR'd together automatically — no change to the detection logic is needed, just more conditions appended after it runs.
Will this hook add noticeable overhead to every list page load?
No. It only runs the related-collection sub-query when extractSearchTerm actually finds a term — a plain list load with no search text returns immediately after the shape check fails to match. The only added cost during an active search is one indexed find against the related collection.
Is this specific to admin-only editorial search, or does it work for public-facing search too?
This pattern is for the Payload admin panel specifically — it hooks into how the built-in editor-facing List view search works. If you're building public, storefront-facing search across a multi-tenant site, that's a different problem with a different tool: see Payload CMS Search: Build a Public Multi-Tenant Index for the @payloadcms/plugin-search-based approach to that.
Conclusion
listSearchableFields covers the common case — searching fields that live on the collection you're viewing — and does it well. It just doesn't extend to relationships, and it fails silently rather than telling you that. The fix is a beforeOperation hook that recognizes the exact where shape Payload's admin generates from a search term, resolves matching IDs on the related collection, and appends them to the same condition — no custom List view, no custom endpoint, no change to how editors already search.
I built and tested this against a production Payload 3.88.0 application, verified it against the collection sources referenced above, and confirmed the search now returns results across the relationship as expected in the live admin panel. Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks, Matija
I'm Matija, an independent developer building and maintaining production Next.js and Payload CMS applications, including e-commerce and multi-tenant admin panels with custom search and access-control requirements. Tested against Payload 3.88.0. Sources: payload/dist/utilities/mergeListSearchAndWhere.js, payload/dist/collections/operations/find.js, and payload/dist/collections/operations/utilities/buildBeforeOperation.js from the installed payload@3.88.0 npm package. Last updated September 1, 2026.
fields
name
"orderNumber"
type
"text"
required
true
// no referenceCode field here — it lives on line-items