Custom CMS Field Type
SkillAI & modelsGuides your agent through building a custom content-management-system field type with its own validation rules.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Custom CMS Field Type skill
About this skill
How to implement a custom CMS field type that integrates with the model builder's fluent API. Covers extending DataFieldBuilder, composing validator interfaces, creating a FieldTypeFactory, registering via DI, and module augmentation for TypeScript autocomplete on the fields() registry.
What this skill tells your AI
The instructions your AI receives, as published by webiny/webiny-js in skills/user-skills/api/custom-field-type/SKILL.md and read by ahel’s review.
TL;DR
A custom field type is a class that extends DataFieldBuilder<"yourType">, paired with a factory class implementing FieldType.Factory. Register the factory with container.register(YourFieldType). Add a module augmentation on "webiny/api/cms/model" so the fields registry method gets TypeScript autocomplete.
When to Use This
Use a custom field type when:
- You need a field with a storage format or validation logic not covered by the built-in types (
text,number,boolean,datetime,file,ref,object,richText,longText,json,dynamicZone) - You want to expose a fluent builder API (e.g.,
fields.slug(),fields.color()) inModelFactoryimplementations
Field Type Structure
A custom field type consists of three parts:
- Builder interface — extends
DataFieldBuilder<"type">plusFieldTypeValidator.*types - Builder class — implements the interface, calls
this.validation()for each validator - Factory class — implements
FieldType.Factory, creates builder instances
As a standalone extension (not part of a larger feature), the directory layout is:
extensions/
└── SlugFieldType/
├── SlugFieldType.ts # builder interface, builder class, factory class
└── feature.ts # createFeature — registers the factory into the DI container
feature.ts:
// extensions/SlugFieldType/feature.ts
import { createFeature } from "webiny/api";
import { SlugFieldType } from "./SlugFieldType.js";
export const SlugFieldTypeFeature = createFeature({
name: "SlugFieldType",
register(container) {
container.register(SlugFieldType);
}
});
Register in the API entry point:
// api/Extension.ts
import { createFeature } from "webiny/api";
import { SlugFieldTypeFeature } from "~/extensions/SlugFieldType/feature.js";
export const Extension = createFeature({
name: "MyExtension",
register(container) {
SlugFieldTypeFeature.register(container);
}
});
Complete Example
// extensions/SlugFieldType/SlugFieldType.ts
import { DataFieldBuilder, FieldType } from "webiny/api/cms/model";
import type { FieldTypeValidator } from "webiny/api/cms/model";
// 1. Builder interface — extends DataFieldBuilder + desired FieldTypeValidator types
export interface ISlugFieldBuilder
extends
DataFieldBuilder<"slug">,
FieldTypeValidator.Required,
FieldTypeValidator.Pattern,
FieldTypeValidator.Unique {}
// 2. Module augmentation — adds fields.slug() to the registry
declare module "webiny/api/cms/model" {
interface IFieldBuilderRegistry {
slug(): ISlugFieldBuilder;
}
interface IFieldRendererRegistry {
myCustomRenderer: {
fieldType: "text" | "number";
settings: undefined;
};
}
}
// 3. Builder class — implements each validator method via this.validation()
class SlugFieldBuilder extends DataFieldBuilder<"slug"> implements ISlugFieldBuilder {
constructor() {
super("slug");
}
required(message?: string): this {
return this.validation({
name: "required",
message: message || "Value is required.",
settings: {}
});
}
pattern(regex: string, flags = "", message?: string): this {
return this.validation({
name: "pattern",
message: message || "Invalid value.",
settings: { preset: "custom", regex, flags }
});
}
unique(message?: string): this {
return this.validation({
name: "unique",
message: message || "Value must be unique.",
settings: {}
});
}
}
// 4. Factory class — implements FieldType.Factory
class SlugFieldTypeFactory implements FieldType.Factory {
readonly type = "slug";
create(): ISlugFieldBuilder {
return new SlugFieldBuilder();
}
}
// 5. Export as a FieldType implementation
export const SlugFieldType = FieldType.createImplementation({
implementation: SlugFieldTypeFactory,
dependencies: []
});
Using the Custom Field in a Model
After registration, fields.slug() is available in any ModelFactory implementation:
import { ModelFactory } from "webiny/api/cms/model";
class ProductModelImpl implements ModelFactory.Interface {
async execute(builder: ModelFactory.Builder) {
return [
builder
.public({ modelId: "product", name: "Product", group: "ungrouped" })
.fields(fields => ({
name: fields.text().label("Name").required(),
slug: fields
.slug()
.label("Slug")
.required("Slug is required.")
.unique()
.pattern("^[a-z0-9-]+$", "", "Only lowercase letters, numbers, and hyphens.")
}))
.layout([["name", "slug"]])
.titleFieldId("name")
.singularApiName("Product")
.pluralApiName("Products")
];
}
}
DataFieldBuilder API
All methods return this for chaining.
| Method | Description |
|---|---|
label(text) | Field label shown in the Admin editor |
help(text) | Help text shown below the field |
description(text) | Field description |
fieldId(id) | Override the auto-derived field ID |
storageId(id) | Override the storage identifier |
placeholder(text) | Placeholder text for the input |
defaultValue(value) | Default value for new entries |
list() | Make the field accept multiple values (array) |
listMinLength(n, msg?) | Minimum number of list items |
listMaxLength(n, msg?) | Maximum number of list items |
tags(tags) | Arbitrary tags for filtering/querying |
renderer(name, settings?) | Set the Admin UI renderer |
settings(settings) | Set arbitrary field settings |
Protected Methods (for use inside validator implementations only)
| Method | Description |
|---|---|
this.validation(rule) | Append a CmsModelFieldValidation to the field's validation array |
this.listValidation(rule) | Append a CmsModelFieldValidation to the list validation array |
A CmsModelFieldValidation has the shape:
{
name: string; // validator name (e.g., "required", "minLength", "pattern")
message: string; // error message shown to the user
settings: Record<string, any>; // validator-specific config
}
Available Validators
Import via import type { FieldTypeValidator } from "webiny/api/cms/model" and extend your builder interface with them. Each type adds one method to your interface:
| Type | Method signature |
|---|---|
FieldTypeValidator.Required | required(message?) |
FieldTypeValidator.Unique | unique(message?) |
FieldTypeValidator.MinLength | minLength(value, message?) |
FieldTypeValidator.MaxLength | maxLength(value, message?) |
FieldTypeValidator.Pattern | pattern(regex, flags?, message?) |
FieldTypeValidator.Email | email(message?) |
FieldTypeValidator.Url | url(message?) |
FieldTypeValidator.LowerCase | lowerCase(message?) |
FieldTypeValidator.UpperCase | upperCase(message?) |
FieldTypeValidator.LowerCaseSpace | lowerCaseSpace(message?) |
FieldTypeValidator.UpperCaseSpace | upperCaseSpace(message?) |
FieldTypeValidator.Gte | gte(value, message?) |
FieldTypeValidator.Lte | lte(value, message?) |
FieldTypeValidator.DateGte | dateGte(value, message?) |
FieldTypeValidator.DateLte | dateLte(value, message?) |
FieldTypeValidator.ListMinLength | listMinLength(value, message?) |
FieldTypeValidator.ListMaxLength | listMaxLength(value, message?) |
When implementing a validator method in the builder class, call this.validation() with the appropriate name and settings. For ListMinLength/ListMaxLength, call this.listValidation() instead. The settings shapes:
| Validator | name | settings |
|---|---|---|
| Required, Unique | "required" / "unique" | {} |
| MinLength, MaxLength | "minLength" / "maxLength" | { value: String(n) } |
| Gte, Lte | "gte" / "lte" | { value: String(n) } |
| DateGte, DateLte | "dateGte" / "dateLte" | { value } |
| Pattern | "pattern" | { preset: "custom", regex, flags } |
"pattern" | { preset: "email", regex: null, flags: null } | |
| Url | "pattern" | { preset: "url", regex: null, flags: null } |
| LowerCase / UpperCase / etc. | "pattern" | { preset: "lowerCase" / "upperCase" / etc., regex: null, flags: null } |
Key Rules
typestring must be unique — the factory'sreadonly typemust not collide with any built-in type (text,number,boolean,datetime,file,ref,object,richText,longText,json,dynamicZone) or other custom types.- Module augmentation target — augment
"webiny/api/cms/model"usingnamespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }. validation()is protected — never call it from outside the builder class. Expose validators as named methods on the interface (e.g.,required(),minLength()).dependencies: []— field type factories have no DI dependencies; always pass an empty array.- Registration order — register custom
FieldTypeimplementations beforeFieldBuilderRegistryis resolved (i.e., in the sameregister()call or before it runs). The registry collects allFieldTypeinstances at construction time.
Related Skills
- webiny-api-cms-content-models — Using the model builder's fluent API to define CMS models
- webiny-api-cms-catalog — Full catalog of CMS abstractions including
ModelFactory,FieldType,DataFieldBuilder - webiny-dependency-injection — The
createImplementationpattern and DI scoping
Signals
- GitHub stars
- 8k
- Forks
- 679
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
webiny-api-cms-custom-field-type- Source
- github.com/webiny/webiny-js