WHAT YOU'LL LEARN
  • Why define content models via code?
  • How to create a content model using the ModelFactory API?
  • How to define reference fields and object fields?
  • How to register models as extensions?

Overview
anchor

Content models can be defined in two ways: via the Admin UI Content Model Editor, or programmatically in code. Defining models via code keeps them in version control, makes them reproducible across environments, and ensures only developers can make structural changes.

Once registered, code-defined models behave identically to UI-created models — they appear in the Admin, generate GraphQL APIs, and support all the same field types.

Both approaches are valid. Use the UI for quick prototyping; use code for production projects, CI/CD pipelines, and team environments where model changes should go through code review.

Basic Example
anchor

Content models are created using the ModelFactory API. Create an extension file and define your model using the fluent builder:

extensions/productCategoryModel.ts

Then register it in webiny.config.tsx:

webiny.config.tsx

Builder API
anchor

Model configuration

MethodDescription
.public({ modelId, name, group })Model is visible in the Admin sidebar and exposed via the Read, Preview, and Manage GraphQL endpoints
.private({ modelId, name, group })Model is not visible in the Admin sidebar and not exposed via the public GraphQL endpoints; useful for internal or system-level models
.description()Model description shown in the Admin
.singularApiName() / .pluralApiName()GraphQL query names
.layout()Defines how fields are arranged in rows in the Admin UI ([["field1", "field2"], ["field3"]])
.titleFieldId()Which field to use as the entry title in list views

Field types

MethodDescription
fields.text()Single-line text
fields.longText()Multi-line text
fields.richText()Rich text with formatting
fields.number()Integer or float
fields.boolean()True/false toggle
fields.datetime()Date and time
fields.file()File or image upload
fields.ref()Reference to another model
fields.object()Nested object with subfields

Field methods

MethodDescription
.label()Display name
.help()Helper text shown in the editor
.required(message)Makes field required
.unique()Ensures values are unique across entries
.minLength() / .maxLength()Length validation
.gte() / .lte()Numeric range validation
.pattern(regex, message)Regex validation
.renderer(name)Specifies the UI renderer (see Available Renderers)

Reference Fields
anchor

Reference fields link entries from one model to another. Use fields.ref() with .models() to specify which model(s) can be referenced:

extensions/productModel.ts

Key reference field options:

  • .models([{ modelId }]) — which model(s) this field references
  • .list() — after .ref(), allows multiple references (use "refDialogMultiple" renderer)
  • Single reference renderer: "refDialogSingle" or "refAutocompleteSingle"
  • Multiple reference renderer: "refDialogMultiple" or "refAutocompleteMultiple"

Object Fields
anchor

Object fields contain nested subfields. Use multipleValues to allow a list of objects:

For a list of objects (e.g., multiple reviews), call .list() and use "objectAccordionMultiple" renderer:

Available Renderers
anchor

All renderer names are available via TypeScript autocomplete when calling .renderer(), so you don’t need to memorise this list — your editor will suggest the valid options for each field type.

Field typeRenderers
TexttextInput, textarea, lexicalEditor
Text (list)textInputs, textareas, lexicalEditors
NumbernumberInput, numberInputs
Booleanswitch
Selectiondropdown, radioButtons, checkboxes, tags
Date/timedateTimeInput, dateTimeInputs
ReferencerefDialogSingle, refDialogMultiple, refAutocompleteSingle, refAutocompleteMultiple, refRadioButtons, refCheckboxes
Filefile, files
ObjectobjectAccordionSingle, objectAccordionMultiple
SpecialdynamicZone, hidden, passthrough
UI elementsuiSeparator, uiAlert, uiTabs

Deploying Changes
anchor

After creating or modifying a model extension, deploy the API:

During development, use watch mode for automatic redeployment:

After deployment, the model appears in the Admin and its GraphQL API is generated automatically.

If you define a model via code that already exists in the UI with the same modelId, you must delete the UI-created model before deploying to avoid conflicts.

Tenant Scope
anchor

By default, a model defined via code is registered for all tenants in your Webiny project. If you need a model to exist only on a specific tenant, use the TenantContext to check the current tenant at runtime and conditionally return the model:

extensions/productCategoryModel.ts

Best Practices
anchor

Never Change These Properties After Creation
anchor

Changing the following properties after a model has entries will cause data loss or corruption:

Model-level:

  • modelId — changing this orphans all existing entries

Field-level:

  • fieldId — changing this breaks the field’s data mapping
  • type — field type cannot change once data exists
  • multipleValues — changing from single to list or vice versa corrupts data
  • settings.models — you can add models to a reference field but never remove them
  • settings.fields — same rules apply to nested object fields

Models Defined via Code Cannot Be Edited in the Admin UI
anchor

All structural changes (adding, removing, or modifying fields) must be made in code. The Admin UI will display the model and its entries, but the model editor will not be available for code-defined models.