webiny-api-cms-custom-field-type
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.
Best use case
webiny-api-cms-custom-field-type is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using webiny-api-cms-custom-field-type should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/custom-field-type/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How webiny-api-cms-custom-field-type Compares
| Feature / Agent | webiny-api-cms-custom-field-type | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
AI Agents for Coding
Browse AI agent skills for coding, debugging, testing, refactoring, code review, and developer workflows across Claude, Cursor, and Codex.
AI Agent for YouTube Script Writing
Find AI agent skills for YouTube script writing, video research, content outlining, and repeatable channel production workflows.
AI Agents for Marketing
Discover AI agents for marketing workflows, from SEO and content production to campaign research, outreach, and analytics.
SKILL.md Source
# Custom CMS Field Type
## 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()`) in `ModelFactory` implementations
## Field Type Structure
A custom field type consists of three parts:
1. **Builder interface** — extends `DataFieldBuilder<"type">` plus `FieldTypeValidator.*` types
2. **Builder class** — implements the interface, calls `this.validation()` for each validator
3. **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`:
```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:
```ts
// 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
```ts
// 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:
```ts
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:
```ts
{
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 }` |
| Email | `"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
1. **`type` string must be unique** — the factory's `readonly type` must not collide with any built-in type (`text`, `number`, `boolean`, `datetime`, `file`, `ref`, `object`, `richText`, `longText`, `json`, `dynamicZone`) or other custom types.
2. **Module augmentation target** — augment `"webiny/api/cms/model"` using `namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }`.
3. **`validation()` is protected** — never call it from outside the builder class. Expose validators as named methods on the interface (e.g., `required()`, `minLength()`).
4. **`dependencies: []`** — field type factories have no DI dependencies; always pass an empty array.
5. **Registration order** — register custom `FieldType` implementations before `FieldBuilderRegistry` is resolved (i.e., in the same `register()` call or before it runs). The registry collects all `FieldType` instances 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 `createImplementation` pattern and DI scopingRelated Skills
webiny-v5-to-v6-migration
Migration patterns for converting v5 Webiny code to v6 architecture. Use this skill when migrating existing v5 plugins to v6 features, converting context plugins to DI services, adapting v5 event subscriptions to v6 EventHandlers, or understanding how v5 patterns translate to v6. Targeted at AI agents performing migrations.
webiny-api-permissions
Schema-based permission system for API features. Use this skill when implementing authorization in use cases, defining permission schemas with createPermissionSchema, creating injectable permissions via createPermissionsAbstraction/createPermissionsFeature, checking read/write/delete/publish permissions, handling own-record scoping, or testing permission scenarios. Covers the full pattern from schema definition to use case integration to test matrices.
webiny-admin-permissions
Admin-side permission UI registration and DI-backed permission checking. Use this skill when adding permission controls to the admin UI — schema-based auto-generated forms, injectable permissions via createPermissionsAbstraction/ createPermissionsFeature, typed hooks (createUsePermissions), the HasPermission component (createHasPermission), and the Security.Permissions component props. Covers both simple apps and complex multi-entity permission schemas.
webiny-sdk
Using @webiny/sdk to read and write CMS data from external applications. Use this skill when the developer is building a Next.js, Vue, Node.js, or any external app that needs to fetch or write content to Webiny, set up the SDK, use the Result pattern, list/get/create/update/publish entries, filter and sort queries, use TypeScript generics for type safety, work with the File Manager, or create API keys programmatically. Covers read vs preview mode, the `values` wrapper requirement, correct method names, and the `fields` required parameter.
webiny-project-structure
Webiny project layout, webiny.config.tsx anatomy, and extension registration. Use this skill when the developer asks about folder structure, where custom code goes, how to register extensions, what webiny.config.tsx does, or how the project is organized. Also use when they need to understand the relationship between extensions/, webiny.config.tsx, and the different extension types (Api, Admin, Infra, CLI).
webiny-local-development
Deploying, developing locally, managing environments, and debugging Webiny projects. Use this skill when the developer asks about deployment commands (deploy, destroy, info), local development with watch mode (API or Admin), the Local Lambda Development system, environment management (long-lived vs short-lived, production vs dev modes), build parameters, state files, debugging API/Admin/Infrastructure errors, or the redeploy-after-watch requirement.
webiny-infrastructure-extensions
Modifying AWS infrastructure using Pulumi handlers and declarative Infra components. Use this skill when the developer wants to customize AWS infrastructure, add Pulumi handlers, configure OpenSearch, VPC, resource tags, regions, custom domains, blue-green deployments, environment-conditional config, or manage production vs development infrastructure modes. Covers CorePulumi.Interface, all <Infra.*> declarative components, and <Infra.Env.Is>.
webiny-infra-catalog
Infrastructure — 33 abstractions. Infrastructure extensions.
webiny-extensions-catalog
extensions — 5 abstractions.
webiny-cli-command-catalog
cli/command — 1 abstractions.
webiny-cli-catalog
cli — 2 abstractions.
webiny-api-tenant-manager-catalog
API — Tenant Manager — 2 abstractions. Tenant management event handlers and use cases.