Skip to main content الرئيسية المنشئون webiny webiny-js webiny-v5-to-v6-migration
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.
الانتقال إلى التثبيت سوق المهارات اكتشف واستكشف مهارات الذكاء الاصطناعي التي بناها المجتمع.
المهن ذات الصلة SOC
استنادا إلى تصنيف SOC المهني
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
نسخ Promptعرض تفاصيل Prompt يتجاوز الأمر المباشر Prompt المخصّص للمراجعة. افحص المصدر قبل تشغيله.
npx skills add https://github.com/webiny/webiny-js --skill webiny-v5-to-v6-migrationيبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
تحميل Zip جاري التحميل... المزيد من هذا المستودع Adding a new feature flag to the Webiny system. Use this skill when creating a new feature flag (simple boolean or nested group), gating a feature at the config/admin/API level, or wiring a flag into the WCP license system. Covers IFeatureFlagsDto, KnownFeatureFlag, Zod schema, FeatureFlag.CanUse components, useFeatureFlags().isEnabled(), the API FeatureFlags abstraction, toDto(), and the LICENSE_CHECKS decorator pattern.
webiny-admin-ui-extensions Customizing the Webiny Admin UI -- white-labeling, custom data list columns, page-type forms, named dialogs, named drawers, and Lexical editor plugins. Use this skill when the developer wants to change branding (logo, title, theme colors), add custom columns to content entry list views, create custom forms for Website Builder page types, register named dialogs or drawers that can be opened from anywhere with params, or extend the Lexical rich text editor. Covers AdminConfig, ContentEntryListConfig, useDialog, useDrawer, useOpenDialog, useOpenDrawer.
webiny-api-graphql-catalog api/graphql — 7 abstractions.
name webiny-v5-to-v6-migration description 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.
v5 → v6 Migration Patterns
Overview
v6 replaces v5's plugin-based architecture with feature-based DI . The key shifts:
v5 Concept v6 Equivalent ContextPlugincreateAbstraction + createImplementation (Service)Plugin array createFeature + container.register()context.myServiceDI injection via constructor onEntryAfterCreate.subscribe()EventHandler featurenew GraphQLSchemaPlugin()GraphQLSchemaFactory.createImplementation()
Pattern 1: Context Plugin → DI Service
v5
new ContextPlugin (async context => {
context.lingotekService = {
translate : async (docId, locale) => {
},
getStatus : docId => {
},
: projectId => {
}
};
});
async
deleteProject
async
v6
import { createAbstraction } from "webiny/api" ;
export interface ILingotekService {
translate (docId : string , locale : string ): Promise <Result <void , Error >>;
getStatus (docId : string ): Promise <Result <TranslationStatus , Error >>;
deleteProject (projectId : string ): Promise <Result <void , Error >>;
}
export const LingotekService = createAbstraction<ILingotekService >("MyExt/LingotekService" );
export namespace LingotekService {
export type Interface = ILingotekService ;
}
class LingotekServiceImpl implements LingotekService .Interface {
constructor (private buildParams : BuildParams .Interface ) {}
async translate (docId : string , locale : string ) {
}
async getStatus (docId : string ) {
}
async deleteProject (projectId : string ) {
}
}
export default LingotekService .createImplementation ({
implementation : LingotekServiceImpl ,
dependencies : [BuildParams ]
});
export const LingotekServiceFeature = createFeature ({
name : "LingotekService" ,
register (container ) {
container.register (LingotekServiceImpl ).inSingletonScope ();
}
});
Key difference: v5 attaches to context object. v6 uses DI — consumers declare the service as a constructor dependency.
Pattern 2: Event Subscription → EventHandler Feature
v5 context.cms .onEntryAfterCreate .subscribe (async params => {
if (params.model .modelId !== "myModel" ) return ;
await doSomething (params.entry );
});
v6
import { EntryAfterCreateEventHandler } from "webiny/api/cms/entry" ;
import { LingotekService } from "../lingotekService/abstractions.js" ;
import { MY_MODEL_ID } from "~/shared/constants.js" ;
class SyncOnCreateHandler implements EntryAfterCreateEventHandler .Interface {
constructor (private lingotekService : LingotekService .Interface ) {}
async handle (event : EntryAfterCreateEventHandler .Event ) {
const { entry, model } = event.payload ;
if (model.modelId !== MY_MODEL_ID ) return ;
await this .lingotekService .translate (entry.entryId , "en" );
}
}
export default EntryAfterCreateEventHandler .createImplementation ({
implementation : SyncOnCreateHandler ,
dependencies : [LingotekService ]
});
export const SyncOnCreateFeature = createFeature ({
name : "SyncOnCreate" ,
register (container ) {
container.register (SyncOnCreateHandler );
}
});
Feature directory is named by business capability (syncOnCreate), not by event name
Handler is a thin orchestrator — business logic lives in the injected service
Must filter by model.modelId — handler fires for ALL models
Pattern 3: Plugin Array → Feature Registration
v5 export default () => [
new GraphQLSchemaPlugin ({ ... }),
new ContextPlugin (async ctx => { ... }),
myModelPlugin,
eventSubscriptionPlugin
];
v6
import { createFeature } from "webiny/api" ;
export const Extension = createFeature ({
name : "MyExtension" ,
register (container ) {
container.register (MyModel );
container.register (MyGraphQLSchema );
SyncOnCreateFeature .register (container);
LingotekServiceFeature .register (container);
}
});
Pattern 4: Async Service Bootstrap → ServiceProvider Pattern When a v5 service was initialized with async data (loading settings, fetching config), v6 uses the ServiceProvider pattern — a provider abstraction with async getService() that lazily creates and caches the service.
See the ServiceProvider Pattern section in webiny-api-architect for the full pattern with abstractions, implementation, and consumer examples.
Pattern 6: Permissions objects
v5 [
{
name : "content.i18n" ,
locales : ["en-US" ]
},
{
name : "cms.endpoint.read"
},
{
name : "cms.endpoint.manage"
},
{
name : "cms.endpoint.preview"
},
{
name : "cms.contentModelGroup" ,
groups : {
"en-US" : [LT_TRANSLATION_MODEL_GROUP_ID ]
},
rwd : "rw" ,
own : false ,
pw : ""
},
{
name : "cms.contentModel" ,
models : {
"en-US" : [
LT_TRANSLATION_DOCUMENT_MODEL_ID ,
LT_CONFIG_MODEL_ID ,
LT_TRANSLATION_PROJECT_MODEL_ID
]
},
rwd : "rwd" ,
own : false ,
pw : ""
},
{
name : "cms.contentEntry" ,
rwd : "rwd" ,
own : false ,
pw : ""
}
];
v6
content.i18n no longer exists
locale codes no longer exist
models is an array of model.modelId strings
groups is an array of group.slug strings
[
{
name : "cms.endpoint.read"
},
{
name : "cms.endpoint.manage"
},
{
name : "cms.endpoint.preview"
},
{
name : "cms.contentModelGroup" ,
groups : ["LT_TRANSLATION_MODEL_GROUP_ID" ],
rwd : "rw" ,
own : false ,
pw : ""
},
{
name : "cms.contentModel" ,
models : [
"LT_TRANSLATION_DOCUMENT_MODEL_ID" ,
"LT_CONFIG_MODEL_ID" ,
"LT_TRANSLATION_PROJECT_MODEL_ID"
],
rwd : "rwd" ,
own : false ,
pw : ""
},
{
name : "cms.contentEntry" ,
rwd : "rwd" ,
own : false ,
pw : ""
}
];
Type Resolution Guide When working with Webiny abstractions, always verify types from source before writing code.
Step 1: Find the catalog entry Use MCP skills or generated catalogs to look up the abstraction (e.g., RoleFactory).
Step 2: Get the source path The catalog entry includes a Source field pointing to the abstraction definition.
Step 3: Read the type definition
cat node_modules/@webiny/api-core/features/security/roles/shared/abstractions.d.ts
Common type patterns Pattern What to expect Factories Return Promise<Type[]> or Promise<Builder[]> UseCases Have Input type and return Result<Data, Error> EventHandlers Have Event with payload property Repositories Return Result<T, Error> — wrap CMS errors
Migration Map: v5 → v6 Equivalents
Backend: Context Method Calls → Use Cases v5 Pattern v6 Equivalent context.cms.getModel()GetModelUseCasecontext.cms.createModel()CreateModelUseCasecontext.cms.updateEntry()UpdateEntryUseCasecontext.cms.getSingletonEntryManager()GetSingletonEntryUseCasecontext.tenancy.getCurrentTenant()TenantContext.getTenant()context.security.withoutAuthorization()IdentityContext.withoutAuthorization()context.aco.folder.delete()DeleteFolderUseCasecontext.aco.folder.get()GetFolderUseCasecontext.plugins.register()DI container registration context.plugins.byType()DI container injection
Backend: Lifecycle Event Subscriptions → EventHandlers v5 Pattern (.subscribe()) v6 EventHandler cms.onEntryBeforeCreateEntryBeforeCreateEventHandlercms.onEntryAfterCreateEntryAfterCreateEventHandlercms.onEntryBeforeUpdateEntryBeforeUpdateEventHandlercms.onEntryAfterUpdateEntryAfterUpdateEventHandlercms.onEntryBeforeDeleteEntryBeforeDeleteEventHandlercms.onEntryAfterDeleteEntryAfterDeleteEventHandlercms.onEntryBeforeMoveEntryBeforeMoveEventHandlercms.onEntryBeforePublishEntryBeforePublishEventHandlercms.onEntryBeforeUnpublishEntryBeforeUnpublishEventHandleraco.folder.onFolderBeforeUpdateFolderBeforeUpdateEventHandleraco.folder.onFolderAfterCreateFolderAfterCreateEventHandleraco.folder.onFolderAfterUpdateFolderAfterUpdateEventHandler
Backend: Plugin Classes → v6 Equivalents v5 Plugin v6 Equivalent ContextPluginDI-registered implementations createContextPluginDI-registered implementations CmsModelPluginModelFactoryGraphQLSchemaPluginGraphQLSchemaFactorycreateGraphQLSchemaPluginGraphQLSchemaFactorycreateTaskDefinitionTaskDefinitionCmsModelFieldToGraphQLPluginCmsModelFieldToGraphQLcreateSecurityRolePluginRoleFactorycreateSecurityTeamPluginTeamFactoryStorageTransformPluginStorageTransformcreateApiGatewayRouteApi.Route (webiny.config.tsx) and Route.Interface (imported from webiny/api)CmsModelFieldValidatorPluginCmsModelFieldValidatorcreateCmsGraphQLSchemaSorterPluginCmsGraphQLSchemaSortercreateCmsEntryElasticsearchBodyModifierPluginCmsEntryOpenSearchBodyModifier
Admin: React Plugins → AdminConfig API v5 Pattern v6 Equivalent createComponentPluginComponent.createDecoratorRoutePlugin<AdminConfig.Route/>AddMenu / menu components<AdminConfig.Menu/>HasPermissionHasPermission or createHasPermission with new schemaGraphQLPlaygroundTabPluginNot migratable. CmsModelFieldTypePlugin<CmsModelFieldType/>CmsModelFieldRendererPlugin<CmsModelFieldRenderer/>AdminAppPermissionRendererPlugincreatePermissionSchema / <Security.Permissions/>webiny/app/configEnvConfig
Common Migration Mistakes
1. Creating one abstraction per operation v5 habit: separate plugins per action. v6: group related operations into a multi-method Service .
2. Naming features by technical event v5 habit: thinking in terms of hooks (onEntryAfterCreate). v6: features describe business capability (syncToLingotek). Files inside can be named technically (EntryAfterCreateHandler.ts).
3. Assuming builder patterns v6 factories sometimes return plain objects, sometimes builder objects. Always read source types first, to understand what the factory in question returns.
4. Putting event handlers in a handlers/ directory v5 habit: grouping by type. v6: handlers are features — they go in features/.
5. Attaching to context v5: context.myService = { ... }. v6: create an abstraction and register it in the DI container via the parent feature, or a standalone feature (createFeature).
6. Inline business logic in event handlers v5 habit: putting logic directly in the subscription callback. v6: handlers are thin orchestrators — extract logic into a Service or UseCase.
Pattern 7: CmsModelFieldToGraphQLPlugin → CmsModelFieldToGraphQL
v5 import { CmsModelFieldToGraphQLPlugin } from "@webiny/api-headless-cms" ;
new CmsModelFieldToGraphQLPlugin ({
fieldType : "myField" ,
isSearchable : true ,
isSortable : false ,
read : {
createTypeField ({ field } ) {
return `${field.fieldId} : String` ;
},
createListFilters ({ field } ) {
return `${field.fieldId} : String` ;
}
},
manage : {
createTypeField ({ field } ) {
return `${field.fieldId} : String` ;
},
createInputField ({ field } ) {
return `${field.fieldId} : String` ;
}
}
});
v6 Import from webiny/api/cms/graphql.js. Implement CmsModelFieldToGraphQL.Interface — split read/manage into separate classes, export via CmsModelFieldToGraphQL.createImplementation, and register in a createFeature container.
import { createFeature } from "webiny/api" ;
import { CmsModelFieldToGraphQL } from "webiny/api/cms/graphql" ;
class ReadApi implements CmsModelFieldToGraphQL .ReadApi {
createTypeField ({ field }: CmsModelFieldToGraphQL .TypeFieldParams ): string {
return `${field.fieldId} : String` ;
}
createListFilters ({ field }: CmsModelFieldToGraphQL .ListFiltersParams ): string {
return `${field.fieldId} : String` ;
}
}
class ManageApi implements CmsModelFieldToGraphQL .ManageApi {
createTypeField ({ field }: CmsModelFieldToGraphQL .TypeFieldParams ): string {
return `${field.fieldId} : String` ;
}
createInputField ({ field }: CmsModelFieldToGraphQL .TypeFieldParams ): string {
return `${field.fieldId} : String` ;
}
}
class MyFieldToGraphQL implements CmsModelFieldToGraphQL .Interface {
public readonly fieldType = "myField" ;
public readonly isSearchable = true ;
public readonly isSortable = false ;
public readonly isFullTextSearchable = false ;
public readonly read = new ReadApi ();
public readonly manage = new ManageApi ();
getReadApi ( ) {
return this .read ;
}
getManageApi ( ) {
return this .manage ;
}
}
export const MyFieldToGraphQLImplementation = CmsModelFieldToGraphQL .createImplementation ({
implementation : MyFieldToGraphQL ,
dependencies : []
});
export const MyGraphQLFeature = createFeature ({
name : "MyApp/MyGraphQLFeature" ,
register : container => {
container.register (MyFieldToGraphQLImplementation );
}
});
Looking up a field handler by type Use CmsModelFieldToGraphQLRegistry (also from webiny/api/cms/graphql.js) to retrieve any registered field handler by its fieldType string. Inject it as a dependency:
import { CmsModelFieldToGraphQL } from "webiny/api/cms/graphql" ;
import { CmsModelFieldToGraphQLRegistry } from "webiny/api/cms/graphql" ;
class MyFieldToGraphQL implements CmsModelFieldToGraphQL .Interface {
constructor (private readonly registry : CmsModelFieldToGraphQLRegistry .Interface ) {}
someMethod (fieldType : string ) {
const handler = this .registry .get (fieldType);
}
}
export const MyFieldToGraphQLImplementation = CmsModelFieldToGraphQL .createImplementation ({
implementation : MyFieldToGraphQL ,
dependencies : [CmsModelFieldToGraphQLRegistry ]
});
registry.getAll() returns every registered handler when you need to iterate.
Pattern 8: createCmsGraphQLSchemaSorterPlugin → CmsGraphQLSchemaSorter
v5 import { createCmsGraphQLSchemaSorterPlugin } from "@webiny/api-headless-cms" ;
createCmsGraphQLSchemaSorterPlugin ({
sorter ({ model, sorters } ) {
return [...sorters, `${model.singularApiName} CustomSort_ASC` ];
}
});
v6 import { createFeature } from "webiny/api" ;
import { CmsGraphQLSchemaSorter } from "webiny/api/cms/graphql" ;
class MyCustomSorter implements CmsGraphQLSchemaSorter .Interface {
execute ({ model, sorters }: CmsGraphQLSchemaSorter .Params ): string [] {
return [...sorters, `${model.singularApiName} CustomSort_ASC` ];
}
}
export const MyCustomSorterImplementation = CmsGraphQLSchemaSorter .createImplementation ({
implementation : MyCustomSorter ,
dependencies : []
});
export const MySorterFeature = createFeature ({
name : "MyApp/MySorterFeature" ,
register : container => {
container.register (MyCustomSorterImplementation );
}
});
Pattern 9: StorageTransformPlugin → StorageTransform
v5 import { StorageTransformPlugin } from "@webiny/api-headless-cms" ;
new StorageTransformPlugin ({
fieldType : "myField" ,
async toStorage ({ value } ) {
return serialize (value);
},
async fromStorage ({ value } ) {
return deserialize (value);
}
});
v6 Import from webiny/api/cms/storage.js. Implement StorageTransform.Interface with toStorage and fromStorage async methods. Register in a createFeature container.
import { createFeature } from "webiny/api" ;
import { StorageTransform } from "webiny/api/cms/storage" ;
class MyStorageTransform implements StorageTransform .Interface {
public readonly fieldType = "myField" ;
async toStorage ({ value }: StorageTransform .ToStorageParams ): Promise <unknown > {
return serialize (value);
}
async fromStorage ({ value }: StorageTransform .FromStorageParams ): Promise <unknown > {
return deserialize (value);
}
}
export const MyStorageTransformImpl = StorageTransform .createImplementation ({
implementation : MyStorageTransform ,
dependencies : []
});
export const MyStorageFeature = createFeature ({
name : "MyApp/MyStorageFeature" ,
register : container => {
container.register (MyStorageTransformImpl );
}
});
Use fieldType: "*" for a catch-all transform that applies to all field types without a specific handler.
Looking up a transform by type Inject StorageTransformRegistry (also from webiny/api/cms/storage.js) to retrieve any registered transform:
import { StorageTransform } from "webiny/api/cms/storage" ;
import { StorageTransformRegistry } from "webiny/api/cms/storage" ;
class MyStorageTransform implements StorageTransform .Interface {
constructor (private readonly registry : StorageTransformRegistry .Interface ) {}
async toStorage ({ value, field }: StorageTransform .ToStorageParams ): Promise <unknown > {
const delegate = this .registry .get (field.type );
}
}
export const MyStorageTransformImpl = StorageTransform .createImplementation ({
implementation : MyStorageTransform ,
dependencies : [StorageTransformRegistry ]
});
registry.getAll() returns every registered transform when you need to iterate.
Pattern 10: CmsModelFieldValidatorPlugin → CmsModelFieldValidator
v5 import { CmsModelFieldValidatorPlugin } from "@webiny/api-headless-cms" ;
new CmsModelFieldValidatorPlugin ({
validator : {
name : "myValidator" ,
async validate ({ value, validator } ) {
if (!meetsCondition (value, validator.settings )) {
throw new Error ("Validation failed." );
}
}
}
});
v6 Import from webiny/api/cms/validation.js. Implement CmsModelFieldValidator.Interface with a name string and an async validate method that returns boolean. Register in a createFeature container.
import { createFeature } from "webiny/api" ;
import { CmsModelFieldValidator } from "webiny/api/cms/validation" ;
class MyValidatorImpl implements CmsModelFieldValidator .Interface {
public readonly name = "myValidator" ;
async validate ({ value, validator }: CmsModelFieldValidator .Params ): Promise <boolean > {
return meetsCondition (value, validator.settings );
}
}
export const MyValidator = CmsModelFieldValidator .createImplementation ({
implementation : MyValidatorImpl ,
dependencies : []
});
export const MyValidationFeature = createFeature ({
name : "MyApp/MyValidationFeature" ,
register : container => {
container.register (MyValidator );
}
});
Looking up a validator by name Inject CmsModelFieldValidatorRegistry (also from webiny/api/cms/validation.js) to retrieve any registered validator:
import { CmsModelFieldValidator } from "webiny/api/cms/validation" ;
import { CmsModelFieldValidatorRegistry } from "webiny/api/cms/validation" ;
class MyValidatorImpl implements CmsModelFieldValidator .Interface {
constructor (private readonly registry : CmsModelFieldValidatorRegistry .Interface ) {}
async validate ({ value, validator }: CmsModelFieldValidator .Params ): Promise <boolean > {
const delegate = this .registry .get (validator.name );
return true ;
}
}
export const MyValidator = CmsModelFieldValidator .createImplementation ({
implementation : MyValidatorImpl ,
dependencies : [CmsModelFieldValidatorRegistry ]
});
registry.getAll() returns every registered validator when you need to iterate.
Pattern 11: createCmsEntryElasticsearchBodyModifierPlugin → CmsEntryOpenSearchBodyModifier Note: this abstraction lives in the api-headless-cms-ddb-es package (the DynamoDB + OpenSearch storage driver), not api-headless-cms. Only register it when that storage driver is in use.
v5 import { createCmsEntryElasticsearchBodyModifierPlugin } from "@webiny/api-headless-cms-ddb-es" ;
createCmsEntryElasticsearchBodyModifierPlugin ({
modelId : "myModel" ,
modifyBody ({ body, model, where } ) {
body.query .bool .filter .push ({ term : { tenant : where.tenant } });
}
});
v6 Import from webiny/api/cms/opensearch.js. Implement CmsEntryOpenSearchBodyModifier.Interface with a synchronous modifyBody method. The optional modelId property scopes the modifier to a single model; omit it to apply to all models.
import { createFeature } from "webiny/api" ;
import { CmsEntryOpenSearchBodyModifier } from "webiny/api/cms/opensearch" ;
class MyBodyModifier implements CmsEntryOpenSearchBodyModifier .Interface {
public readonly modelId = "myModel" ;
modifyBody ({ body, model, where }: CmsEntryOpenSearchBodyModifier .Params ): void {
body.query .bool .filter .push ({ term : { tenant : where.tenant } });
}
}
export const MyBodyModifierImpl = CmsEntryOpenSearchBodyModifier .createImplementation ({
implementation : MyBodyModifier ,
dependencies : []
});
export const MyOpenSearchFeature = createFeature ({
name : "MyApp/MyOpenSearchFeature" ,
register : container => {
container.register (MyBodyModifierImpl );
}
});
Related Skills
webiny-api-architect — Full v6 architecture, Services vs UseCases, anti-patterns
webiny-use-case-pattern — UseCase implementation details
webiny-event-handler-pattern — EventHandler and domain event patterns
webiny-dependency-injection — DI pattern and injectable services