Headless CMS > Define Content Models via Code
Define Content Models via Code
Learn how to define content models and content model groups programmatically using the ModelFactory API
- Why define content models via code?
- How to create a content model using the
ModelFactoryAPI? - How to define reference fields and object fields?
- How to register models as extensions?
Overview
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
Content models are created using the ModelFactory API. Create an extension file and define your model using the fluent builder:
Then register it in webiny.config.tsx:
Builder API
Model configuration
| Method | Description |
|---|---|
.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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
.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
Reference fields link entries from one model to another. Use fields.ref() with .models() to specify which model(s) can be referenced:
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
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
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 type | Renderers |
|---|---|
| Text | textInput, textarea, lexicalEditor |
| Text (list) | textInputs, textareas, lexicalEditors |
| Number | numberInput, numberInputs |
| Boolean | switch |
| Selection | dropdown, radioButtons, checkboxes, tags |
| Date/time | dateTimeInput, dateTimeInputs |
| Reference | refDialogSingle, refDialogMultiple, refAutocompleteSingle, refAutocompleteMultiple, refRadioButtons, refCheckboxes |
| File | file, files |
| Object | objectAccordionSingle, objectAccordionMultiple |
| Special | dynamicZone, hidden, passthrough |
| UI elements | uiSeparator, uiAlert, uiTabs |
Deploying Changes
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
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:
Best Practices
Never Change These Properties After Creation
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 mappingtype— field type cannot change once data existsmultipleValues— changing from single to list or vice versa corrupts datasettings.models— you can add models to a reference field but never remove themsettings.fields— same rules apply to nested object fields
Models Defined via Code Cannot Be Edited in the Admin UI
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.