Learn How Virtual Fields Can Help Solve Common CMS Challenges

Payload CMS Virtual Fields

In Payload, the term "virtual field" is used to describe a field that's dynamically populated and is not stored in the database.

At a high level, you can create virtual fields by using beforeChange and afterRead field hooks to attach and remove data from your document. The field itself can be either visible or hidden in the Admin UI.

Use Cases

When you need to access or manipulate data that doesn't need to be stored in your database, virtual fields are the way to go. This is an incredibly useful feature that has countless possibilities. Here are a few common use cases:

  • A full name field dynamically joining the user's first and last name

  • A dynamic title field that takes the values of several text fields and combines them 

  • Users count field which runs a fetch request on the users collection and returns the total number of users

  • Calculations where the field reads values from other number fields, computes the calculation and returns the output 

  • Reference field which fetches and returns data from another document

To understand how virtual fields can improve CMS usability, imagine you are working with nested pages, you have two parent pages—let's say Sales and Technical. Both of these pages have a child Contact page.

At a glance, both child pages are just labeled as "Contact". To determine which is which, you need to look at the parent page it's associated with.

This is where virtual fields can help. You can generate a fullTitle field that takes the page title with the parent title and return it as a combined string i.e. contact - sales and then set your useAsTitle property to this field. Boom, across the CMS and in your frontend, this page will display the new dynamic title.

Of course, you can achieve this with any regular text field, but since the data it stores is duplicative, it doesn't make as much sense to store the data. Just compute it virtually.


To follow along and see virtual fields in action, let's boot up a quick virtual fields demo.

Location Virtual Field

In the Location collection, we have three standard text fields defined: City, State and Country.

For every document in a collection, the admin UI will display the id as its title by default, which is shown at the top of the document view, in the collection list view and in relationship fields.

We can set useAsTitle on the Collection to assign our own field as the title, but what field should we use? State or Country will be the same in many cases, and if we use the City field we can still run into cases where this City exists in another Country i.e. London, Canada and London, UK.

We can solve this scenario with a virtual field to read the values of City, State and Country and return them as one combined string. And since this data is duplicative, it really doesn't need to get stored in the database.

Here are our three standard text fields:

type: 'row',
fields: [
name: 'city',
type: 'text',
required: true,
name: 'state',
type: 'text',
required: true,
name: 'country',
type: 'text',
required: true,

Now let's take a look at the virtual location field itself

The essential properties here are beforeChange and afterRead . Setting admin.hidden: true is optional but considered best practice. Being that an admin can't really edit this field, the only reason to show it in the admin UI is if it's relevant to your editors. In this case, they are already seeing the City, State, and Country fields, so no need to add additional clutter.

name: 'location',
type: 'text',
admin: {
hidden: true, // hides the field from the admin panel
hooks: {
beforeChange: [
({ siblingData }) => {
// ensures data is not stored in DB
delete siblingData['location']
afterRead: [
({ data }) => {
return `${data.city}${data.state ? `, ${data.state},` : ','} ${data.country}`;
The afterRead Field Hook

The afterRead hook is responsible for retrieving and combining the data that will be returned as the field's value. This hook runs as the last step before a document gets returned from Payload.

Below, we've written a quick formatLocation hook that uses the data arg to access the values of City, State and Country, returning them in one combined string.

Note: the FieldHook type can be imported directly from Payload.

Field-based Access Control

Setting the create and update access controls to false denies the ability to set or update a field's value and will discard any values passed to the field during these operations. 

Some virtual fields can function without restricting permissions however it is best practice to prevent unintended changes to the field.

Hiding the field

As mentioned above, you might want to hide the field from the admin UI if it's duplicative or unnecessary. That's as easy as setting admin.hidden to true. The functionality will remain unaffected but the field itself will not be visible in the admin panel.

Testing it out

To test out the virtual field, create a document in the Location collection and enter a City, State and Country. When you access the response directly in the browser at /api/locations/[id] we can see the location field has formatted these fields into a single string. 

In MongoDB, after navigating to the database for this repo, you will see the data shape is unchanged and the location field is not present keeping the data shape clean and easier to work with.

Virtual Field API Response

Here is the response from the rest API - notice the Location field working it's magic.

Virtual Field Database

And here is the same document in MongoDB - real simple and clean.

Async Virtual Fields

All hooks can be written as either synchronous or asynchronous functions. Using async and await will suspend any action until the promise is fulfilled. This is super valuable if your virtual field relies on asynchronous actions such as fetching data.

Let’s take a look at the virtual field staff which uses the async getLocationStaff hook.

name: 'staff',
type: 'relationship',
relationTo: 'staff',
hasMany: true,
access: {
create: () => false,
update: () => false,
admin: {
readOnly: true,
hooks: {
beforeChange: [({ siblingData }) => {
delete siblingData.staff;
afterRead: [getLocationStaff],

In this demo, the Staff collection has a relationship to Locations.

With the getLocationStaff hook, we are using payload.find to fetch all documents in the staff collection where the location is equal to the current location. By using async and await, this function will not return a value until the payload.find request has run. This virtual field then functions as an inverse relationship field to the staff collection.


Virtual fields unlock an extra level of control across your data and CMS, allowing you to perform and streamline complex logic while keeping your code clean and simple. With this article, hopefully you have gained a basic understanding of virtual fields along with the ability to setup and configure own.

If you have any questions or feedback, reach out on GitHub or Discord.

Learn More

For more information about topics we touched on, checkout the following:

Get up and running with one line

Getting started with Payload is easy—and free forever. Just fire up a new terminal window and run the following command:

npx degit github:payloadcms/payload/examples/virtual-fields

Like what we're doing? Give us a star on GitHub

We're trying to change the CMS status quo by delivering editors with a great experience, but first and foremost, giving developers a CMS that they don't absolutely despise working with. All of our new features are meant to be extensible and work simply and sanely.