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.
v6 replaces v5's plugin-based architecture with feature-based DI. The key shifts:
| v5 Concept | v6 Equivalent |
|---|---|
ContextPlugin | createAbstraction + createImplementation (Service) |
| Plugin array | createFeature + container.register() |
context.myService | DI injection via constructor |
onEntryAfterCreate.subscribe() | EventHandler feature |
new GraphQLSchemaPlugin() | GraphQLSchemaFactory.createImplementation() |
new ContextPlugin(async context => {
context.lingotekService = {
translate: async (docId, locale) => {
/* ... */
},
getStatus: async docId => {
/* ... */
},
deleteProject: async projectId => {
/* ... */
}
};
});
// features/lingotekService/abstractions.ts
import { createAbstraction } from "@webiny/feature/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;
}
// features/lingotekService/LingotekService.ts
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]
});
// features/lingotekService/feature.ts
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.
context.cms.onEntryAfterCreate.subscribe(async params => {
if (params.model.modelId !== "myModel") return;
await doSomething(params.entry);
});
// features/syncOnCreate/EntryAfterCreateHandler.ts
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]
});
// features/syncOnCreate/feature.ts
export const SyncOnCreateFeature = createFeature({
name: "SyncOnCreate",
register(container) {
container.register(SyncOnCreateHandler);
}
});
Key differences:
syncOnCreate), not by event namemodel.modelId — handler fires for ALL modelsexport default () => [
new GraphQLSchemaPlugin({ ... }),
new ContextPlugin(async ctx => { ... }),
myModelPlugin,
eventSubscriptionPlugin
];
// api/Extension.ts
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);
}
});
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.
[
{
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: ""
}
];
content.i18n no longer existsmodels is an array of model.modelId stringsgroups 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: ""
}
];
When working with Webiny abstractions, always verify types from source before writing code.
Use MCP skills or generated catalogs to look up the abstraction (e.g., RoleFactory).
The catalog entry includes a Source field pointing to the abstraction definition.
# Read the abstractions file
cat node_modules/@webiny/api-core/features/security/roles/shared/abstractions.d.ts
| 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 |
| v5 Pattern | v6 Equivalent |
|---|---|
context.cms.getModel() | GetModelUseCase |
context.cms.createModel() | CreateModelUseCase |
context.cms.updateEntry() | UpdateEntryUseCase |
context.cms.getSingletonEntryManager() | GetSingletonEntryUseCase |
context.tenancy.getCurrentTenant() | TenantContext.getTenant() |
context.security.withoutAuthorization() | IdentityContext.withoutAuthorization() |
context.aco.folder.delete() | DeleteFolderUseCase |
context.aco.folder.get() | GetFolderUseCase |
context.plugins.register() | DI container registration |
context.plugins.byType() | DI container injection |
v5 Pattern (.subscribe()) | v6 EventHandler |
|---|---|
cms.onEntryBeforeCreate | EntryBeforeCreateEventHandler |
cms.onEntryAfterCreate | EntryAfterCreateEventHandler |
cms.onEntryBeforeUpdate | EntryBeforeUpdateEventHandler |
cms.onEntryAfterUpdate | EntryAfterUpdateEventHandler |
cms.onEntryBeforeDelete | EntryBeforeDeleteEventHandler |
cms.onEntryAfterDelete | EntryAfterDeleteEventHandler |
cms.onEntryBeforeMove | EntryBeforeMoveEventHandler |
cms.onEntryBeforePublish | EntryBeforePublishEventHandler |
cms.onEntryBeforeUnpublish | EntryBeforeUnpublishEventHandler |
aco.folder.onFolderBeforeUpdate | FolderBeforeUpdateEventHandler |
aco.folder.onFolderAfterCreate | FolderAfterCreateEventHandler |
aco.folder.onFolderAfterUpdate | FolderAfterUpdateEventHandler |
| v5 Plugin | v6 Equivalent |
|---|---|
ContextPlugin | DI-registered implementations |
createContextPlugin | DI-registered implementations |
CmsModelPlugin | ModelFactory |
GraphQLSchemaPlugin | GraphQLSchemaFactory |
createGraphQLSchemaPlugin | GraphQLSchemaFactory |
createTaskDefinition | TaskDefinition |
CmsModelFieldToGraphQLPlugin | TODO |
createSecurityRolePlugin | RoleFactory |
createSecurityTeamPlugin | TeamFactory |
StorageTransformPlugin | TODO |
createApiGatewayRoute | TODO (Adrian) |
CmsModelFieldValidatorPlugin | TODO |
createCmsGraphQLSchemaSorterPlugin | TODO |
createCmsEntryElasticsearchBodyModifierPlugin | TODO |
| v5 Pattern | v6 Equivalent |
|---|---|
createComponentPlugin | Component.createDecorator |
RoutePlugin | <AdminConfig.Route/> |
AddMenu / menu components | <AdminConfig.Menu/> |
HasPermission | HasPermission or createHasPermission with new schema |
GraphQLPlaygroundTabPlugin | TODO |
CmsModelFieldTypePlugin | <CmsModelFieldType/> |
CmsModelFieldRendererPlugin | <CmsModelFieldRenderer/> |
AdminAppPermissionRendererPlugin | createPermissionSchema / <Security.Permissions/> |
webiny/app/config | EnvConfig |
v5 habit: separate plugins per action. v6: group related operations into a multi-method Service.
v5 habit: thinking in terms of hooks (onEntryAfterCreate). v6: features describe business capability (syncToLingotek). Files inside can be named technically (EntryAfterCreateHandler.ts).
v6 factories sometimes return plain objects, sometimes builder objects. Always read source types first, to understand what the factory in question returns.
v5 habit: grouping by type. v6: handlers are features — they go in features/.