Skip to main content
Custom fields extend a built-in model’s forms, detail sections, and list table without forking the page. You add a field to the create and edit forms, a row to the detail view, an action to a section menu, or a column to the list. You write one file per model under src/custom-fields/ and default-export a defineCustomFieldsConfig. The config targets a model, optionally fetches linked module data alongside it, and describes what to add to each surface.

Add a custom field

1

Create a custom-fields file

Create a file under src/custom-fields/ and default-export defineCustomFieldsConfig with the model you want to extend. Start with product.
apps/vendor/src/custom-fields/product.tsx
2

Describe the field with createFormHelper

Import createFormHelper from @mercurjs/dashboard-shared and turn a Zod schema into an input type and validation. The Zod schema drives both the default input and the validation.
apps/vendor/src/custom-fields/product.tsx
3

Add the field to a form

Add the field to a built-in form zone and tab. This injects an ERP ID field into the product edit form.
apps/vendor/src/custom-fields/product.tsx
4

Run the panel

Start the panel and open the product edit form. The field renders in its tab.
Terminal
For product, values submit under additional_data and persist onto the product’s metadata. See Persistence for other models.

Configuration

defineCustomFieldsConfig takes one object.

Add fields to a form

Inject fields into a built-in create, edit, or onboarding form.
Each field is a CustomFormField.
A form-field component renders inside the Form.Field chain. It receives field (the bound name, value, onChange, and onBlur) and data (the loaded entity). Values live in form state under additional_data. For product, custom fields persist onto the product’s metadata.

Replace or hide a built-in field

A fields key that matches one of the form’s own fields overrides it instead of adding a new one. null hides the field. A field definition replaces how it renders, and the replacement still writes to the built-in form key.
The overridable field ids are typed per model and autocomplete in your editor. Validation of a built-in field stays the built-in schema’s. A hidden field keeps its default value, so hide a required field only when that default is valid.

Fields in tabbed create forms

In a tabbed create form, set tab to place a field on a tab. A field without a tab renders on the first tab. Continue validates the custom fields of the tab you leave, and Publish validates them all.
The product create form has the tabs details, organize, attributes, and variants. Product-level values reach the productsCreated hook of createProductsWorkflow as additional_data.

Add columns to a spreadsheet tab

Some forms are spreadsheets, such as the product create form’s variants tab or the offer create form’s stockLevelsAndPrices tab. Target the tab and the same rules apply to its columns: a built-in column id with null hides it, a definition replaces it, and any other key adds an editable column. Added values are stored per row under additional_data. A spreadsheet tab only takes fields that name it in tab; untabbed fields never become columns.
Each row’s values reach a workflow hook, keyed by the id of the record the row created or updated. Persist them from the hook. A replaced spreadsheet column with a component is read-only and receives the row as data. Without a component, it keeps editing the built-in value with the cell for its type.

Customize the onboarding wizard

The vendor onboarding wizard is the onboarding form zone of the seller model. Each step is a tab. Use the same forms entries to add fields to a step, add a step, remove a step, or reorder the steps.
apps/vendor/src/custom-fields/seller.tsx
The built-in steps and their defaults: The last step’s button reads Finish and creates the seller. Values from every step’s custom fields are merged into one additional_data object on POST /vendor/sellers, which reaches the sellerAccountCreated hook of createSellerAccountWorkflow. A skipped step contributes nothing, even if its fields are required.
Always set tab on an onboarding entry. An entry without a tab renders its fields on every step.

Extend detail sections

Add, replace, or remove fields on a detail section, and add actions to its ActionMenu.
fields[] is CustomDisplayField. A display component receives the loaded detail entity as data, including any linked module data. If the config declares link: "brand", read it off data.brand.
actions[] is SectionAction, the same shape as list bulkActions.

Extend the list table

Override or add columns, register bulk actions and filters, and set view defaults on the model’s list table.
columns[] is CustomColumn. filters[] is CustomListFilter. The key is the query param the filter writes, so it matches a built-in filter by its param name (customer_id, status, created_at, …). A replacement keeps the built-in filter’s position; added filters go after the built-in ones.

Extend other tables of the model

A list block without a zone targets the model’s list page. Other tables of the same model, such as a detail-page section or a picker inside a create form, have their own zone. Pass an array to extend several tables. The zones and column ids are typed per model.
Every table keeps its row selection checkbox as the first column and its row actions menu as the last; extensions add and reorder columns between them.
Bulk-action rendering is deferred in the MVP. bulkActions are accepted and surfaced by the config, but not yet mounted into the list toolbar.
The vendor product list is field-constrained. It must use the curated fields from useProductTableQuery. The SDK merges link fetches with the + and - convention, never bare fields, or the list returns a 500. You never hand-write the field list. The link declaration drives it.

Combine configs from plugins and your app

Every config for a model applies in order: installed plugins first, then your app’s src/custom-fields/ files. A later config sees everything earlier configs added, not only the built-ins, so your app can change what a plugin contributed. link, bulkActions, display actions, activity, and actionLocks add up across configs.

Fetch linked module data

Declare link to fetch a module link alongside the entity. Its data rides on the data passed to displays and on the row passed to columns.

Persistence

The MVP is a UI surface only. Custom fields render, validate, and display through the built-in forms, sections, and tables. There is no generic core-side write path. For product, values submit under additional_data and persist onto metadata. To store data for other models, wire your own route or workflow, or use the backend Custom Fields module.

Next steps

Widgets

Render a component in a slot on an existing page.

Create a new page

Add a route with file-based routing and register it in the sidebar.