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.
Best use case
webiny-v5-to-v6-migration is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using webiny-v5-to-v6-migration 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/v5-to-v6-migration/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How webiny-v5-to-v6-migration Compares
| Feature / Agent | webiny-v5-to-v6-migration | 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?
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.
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
Best AI Skills for Claude
Explore the best AI skills for Claude and Claude Code across coding, research, workflow automation, documentation, and agent operations.
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.
SKILL.md Source
# v5 → v6 Migration Patterns
## Overview
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()` |
---
## Pattern 1: Context Plugin → DI Service
### v5
```typescript
new ContextPlugin(async context => {
context.lingotekService = {
translate: async (docId, locale) => {
/* ... */
},
getStatus: async docId => {
/* ... */
},
deleteProject: async projectId => {
/* ... */
}
};
});
```
### v6
```typescript
// 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.
---
## Pattern 2: Event Subscription → EventHandler Feature
### v5
```typescript
context.cms.onEntryAfterCreate.subscribe(async params => {
if (params.model.modelId !== "myModel") return;
await doSomething(params.entry);
});
```
### v6
```typescript
// 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:**
- 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
```typescript
export default () => [
new GraphQLSchemaPlugin({ ... }),
new ContextPlugin(async ctx => { ... }),
myModelPlugin,
eventSubscriptionPlugin
];
```
### v6
```typescript
// 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);
}
});
```
---
## 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
```ts
[
{
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
```ts
[
{
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
```bash
# Read the abstractions file
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()` | `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 |
### Backend: Lifecycle Event Subscriptions → EventHandlers
| 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` |
### Backend: Plugin Classes → v6 Equivalents
| 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 |
### Admin: React Plugins → AdminConfig API
| 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` |
---
## 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.
## 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 servicesRelated Skills
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.
webiny-api-tasks-catalog
api/tasks — 2 abstractions.