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.

7,955 stars

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

$curl -o ~/.claude/skills/custom-field-type/SKILL.md --create-dirs "https://raw.githubusercontent.com/webiny/webiny-js/main/skills/user-skills/api/custom-field-type/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/custom-field-type/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How webiny-api-cms-custom-field-type Compares

Feature / Agentwebiny-api-cms-custom-field-typeStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

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 scoping

Related Skills

webiny-v5-to-v6-migration

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

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

7955
from webiny/webiny-js

Infrastructure — 33 abstractions. Infrastructure extensions.

webiny-extensions-catalog

7955
from webiny/webiny-js

extensions — 5 abstractions.

webiny-cli-command-catalog

7955
from webiny/webiny-js

cli/command — 1 abstractions.

webiny-cli-catalog

7955
from webiny/webiny-js

cli — 2 abstractions.

webiny-api-tenant-manager-catalog

7955
from webiny/webiny-js

API — Tenant Manager — 2 abstractions. Tenant management event handlers and use cases.