Skip to content
Content
Skill

/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()

BOOST
From plugin
webiny-js
8k76 skills3 MCP
Install
$ npx -y skills add webiny/webiny-js --skill custom-field-type --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/custom-field-type

Context preview

The summary Claude sees to decide when to auto-load 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()

SKILL.md

custom-field-type.SKILL.md
name: webiny-api-cms-custom-field-type
description: >
  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.

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`:

// 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 th

Read more
Ships withwebiny-js

Open-source content platform. Self-hosted on AWS serverless. Built as a TypeScript framework you extend with code, not a closed product you configure through a UI. Runs on Lambda, DynamoDB, S3, and CloudFront inside your own AWS account. Scales automatically.

Get the whole plugin
Stats
8,049
Stars
682
Forks
Active
Maintenance
TypeScript
Language
18h ago
Last commit
8y ago
Created
2d ago
Added

Repo: webiny/webiny-js

Other skills on webiny-js.