webiny-event-handler-pattern

EventHandler implementation pattern — handle method, event payloads, filtering, DI, domain event definition, publishing events from UseCases, and reacting to external events. Use this skill to implement any Webiny EventHandler (before/after hooks) or to define and publish your own domain events.

7,955 stars

Best use case

webiny-event-handler-pattern is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

EventHandler implementation pattern — handle method, event payloads, filtering, DI, domain event definition, publishing events from UseCases, and reacting to external events. Use this skill to implement any Webiny EventHandler (before/after hooks) or to define and publish your own domain events.

Teams using webiny-event-handler-pattern 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/event-handler-pattern/SKILL.md --create-dirs "https://raw.githubusercontent.com/webiny/webiny-js/main/skills/user-skills/api/event-handler-pattern/SKILL.md"

Manual Installation

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

How webiny-event-handler-pattern Compares

Feature / Agentwebiny-event-handler-patternStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

EventHandler implementation pattern — handle method, event payloads, filtering, DI, domain event definition, publishing events from UseCases, and reacting to external events. Use this skill to implement any Webiny EventHandler (before/after hooks) or to define and publish your own domain events.

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

# EventHandler Pattern

## What It Is

An **EventHandler** reacts to domain events in the Webiny lifecycle (e.g., `EntryBeforeCreateEventHandler`, `TenantAfterDeleteEventHandler`). Each handler is a DI abstraction with a single `handle` method.

## Naming Convention

- `{Entity}Before{Operation}EventHandler` — fires before persistence, can validate/transform/reject
- `{Entity}After{Operation}EventHandler` — fires after persistence, for side effects

## Interface Shape

```ts
interface SomeEventHandler.Interface {
    handle(event: SomeEventHandler.Event): Promise<void>;
}
```

The `Event` is a `DomainEvent<Payload>` where the payload contains the entity and input data.

## Architecture Rule: Always Wrap Logic in a Reusable Abstraction (MANDATORY)

**Never put business logic directly inside an EventHandler.** EventHandlers are thin orchestrators — they receive an event and delegate to an injected service or use case. The real logic lives in a dedicated abstraction.

**Why:** Inline handler logic cannot be reused by other handlers, GraphQL resolvers, or CLI commands.

**Always follow this structure:**

```
features/
├── myService/             ← the reusable abstraction
│   ├── abstractions.ts
│   ├── feature.ts
│   └── MyService.ts
└── syncOnCreate/          ← thin handler that injects the service
    ├── feature.ts
    └── EntryAfterCreateHandler.ts
```

The EventHandler feature and the service feature are **registered separately** in `Extension.ts`.

## How to Implement

```ts
import { SomeEventHandler } from "webiny/api/<category>";
import { MyService } from "../myService/abstractions.js";

// ✅ Handler is a thin orchestrator — no business logic here
class MyHandler implements SomeEventHandler.Interface {
  constructor(private myService: MyService.Interface) {}

  async handle(event: SomeEventHandler.Event) {
    const { entity, model } = event.payload;

    // For CMS handlers: ALWAYS filter by model
    if (model.modelId !== "myModel") return;

    await this.myService.doWork(entity);
  }
}

export default SomeEventHandler.createImplementation({
  implementation: MyHandler,
  dependencies: [MyService]
});
```

See **webiny-api-architect** for how to define `MyService` as a proper abstraction.

## Injecting Dependencies

EventHandlers can depend on UseCases, platform services, or custom abstractions:

```ts
import { SomeEventHandler } from "webiny/api/<category>";
import { SomeUseCase } from "webiny/api/<category>";

class MyHandler implements SomeEventHandler.Interface {
  constructor(private someUseCase: SomeUseCase.Interface) {}

  async handle(event: SomeEventHandler.Event) {
    const result = await this.someUseCase.execute({ /* ... */ });
  }
}

export default SomeEventHandler.createImplementation({
  implementation: MyHandler,
  dependencies: [SomeUseCase]
});
```

---

## Defining Your Own Domain Events

When your feature needs to notify other parts of the system about important domain actions, define your own events.

### Event Payload Types (in `abstractions.ts`)

Event payloads and handler abstractions live in `abstractions.ts`. The `events.ts` file only contains the event classes.

```ts
// features/disableEntity/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
import type { IEventHandler } from "@webiny/api-core/features/EventPublisher";
import type { Entity } from "~/shared/Entity.js";
// Forward declaration — actual classes are in events.ts
import type { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js";

// Event Payload Types
export interface EntityBeforeDisablePayload {
    entity: Entity;
}

export interface EntityAfterDisablePayload {
    entity: Entity;
}

// Handler Abstractions — one per event
export const EntityBeforeDisableEventHandler = createAbstraction<
    IEventHandler<EntityBeforeDisableEvent>
>("MyPackage/EntityBeforeDisableEventHandler");

export namespace EntityBeforeDisableEventHandler {
    export type Interface = IEventHandler<EntityBeforeDisableEvent>;
    export type Event = EntityBeforeDisableEvent;
}

export const EntityAfterDisableEventHandler = createAbstraction<
    IEventHandler<EntityAfterDisableEvent>
>("MyPackage/EntityAfterDisableEventHandler");

export namespace EntityAfterDisableEventHandler {
    export type Interface = IEventHandler<EntityAfterDisableEvent>;
    export type Event = EntityAfterDisableEvent;
}
```

### Event Class Definition (`events.ts`)

Event classes import payload types and handler abstractions from `abstractions.ts`.

```ts
// features/disableEntity/events.ts
import { DomainEvent } from "@webiny/api-core/features/EventPublisher";
import {
    EntityBeforeDisableEventHandler,
    EntityAfterDisableEventHandler
} from "./abstractions.js";
import type {
    EntityBeforeDisablePayload,
    EntityAfterDisablePayload
} from "./abstractions.js";

export class EntityBeforeDisableEvent extends DomainEvent<EntityBeforeDisablePayload> {
    eventType = "entity.beforeDisable" as const;

    getHandlerAbstraction() {
        return EntityBeforeDisableEventHandler;
    }
}

export class EntityAfterDisableEvent extends DomainEvent<EntityAfterDisablePayload> {
    eventType = "entity.afterDisable" as const;

    getHandlerAbstraction() {
        return EntityAfterDisableEventHandler;
    }
}
```

### Publishing Events from a UseCase

```ts
// features/disableEntity/DisableEntityUseCase.ts
import { EventPublisher } from "@webiny/api-core/features/EventPublisher";
import { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js";

class DisableEntityUseCase implements UseCaseAbstraction.Interface {
    constructor(
        private eventPublisher: EventPublisher.Interface,
        private getEntityById: GetEntityByIdUseCase.Interface,
        private updateEntity: UpdateEntityUseCase.Interface
    ) {}

    async execute(entityId: string): Promise<Result<void, UseCaseAbstraction.Error>> {
        const getResult = await this.getEntityById.execute(entityId);
        if (getResult.isFail()) return Result.fail(getResult.error);

        const entity = getResult.value;

        // Publish BEFORE event (can be intercepted to reject)
        await this.eventPublisher.publish(new EntityBeforeDisableEvent({ entity }));

        // Perform the operation
        const updateResult = await this.updateEntity.execute(entityId, { status: "disabled" });
        if (updateResult.isFail()) return Result.fail(updateResult.error);

        // Publish AFTER event (for side effects)
        await this.eventPublisher.publish(new EntityAfterDisableEvent({ entity: updateResult.value }));

        return Result.ok();
    }
}

export default UseCaseAbstraction.createImplementation({
    implementation: DisableEntityUseCase,
    dependencies: [EventPublisher, GetEntityByIdUseCase, UpdateEntityUseCase]
});
```

---

## Reacting to External Events

To react to events from other packages (e.g., CMS entry deletion), implement the external event's handler abstraction:

```ts
// features/cleanupOnEntryDelete/CleanupOnEntryDeleteHandler.ts
import { EntryAfterDeleteEventHandler } from "@webiny/api-headless-cms/features/contentEntry/DeleteEntry/events.js";
import { CleanupService } from "../cleanupService/abstractions.js";
import { MY_MODEL_ID } from "~/shared/constants.js";

class CleanupOnEntryDeleteHandler implements EntryAfterDeleteEventHandler.Interface {
    constructor(private cleanupService: CleanupService.Interface) {}

    async handle(event: EntryAfterDeleteEventHandler.Event): Promise<void> {
        const { entry, model } = event.payload;

        // ALWAYS filter by model — handler fires for ALL models
        if (model.modelId !== MY_MODEL_ID) return;

        if (!event.payload.permanent) return;

        await this.cleanupService.cleanup(entry.entryId);
    }
}

export default EntryAfterDeleteEventHandler.createImplementation({
    implementation: CleanupOnEntryDeleteHandler,
    dependencies: [CleanupService]
});
```

---

## Event Naming Conventions

| Artifact           | Pattern                                        | Example                            |
| ------------------ | ---------------------------------------------- | ---------------------------------- |
| `eventType`        | `"entity.beforeAction"` / `"entity.afterAction"` | `"tenant.beforeDisable"`          |
| Handler abstraction | `{Entity}{Before\|After}{Action}EventHandler` | `TenantBeforeDisableEventHandler` |
| Event class        | `{Entity}{Before\|After}{Action}Event`         | `TenantBeforeDisableEvent`        |

## Registration

**YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"@/extensions/my-handler.ts"}`, NOT `src={"@/extensions/my-handler"}`. Omitting the file extension will cause a build failure.

**YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by an Extension `src` prop. Using a named export (`export const Foo = SomeFactory.createImplementation(...)`) will cause a build failure. Named exports are only valid inside files registered via `createFeature`.

```tsx
<Api.Extension src={"@/extensions/my-handler.ts"} />
```

Deploy with: `yarn webiny deploy api --env=dev`

## Resolving Types (MANDATORY)

**Before writing any code that accesses event payload properties or domain types (CmsEntry, CmsModel, etc.), you MUST read the source file listed in the catalog's `Source` field to verify the exact property names and types. Do not assume or guess property names from memory.**

1. Read the `abstractions.ts` file from the catalog `Source` path — it contains the payload interface
2. Read the `events.ts` file (sibling to `abstractions.ts`) — it contains the `Interface` and `Event` types
3. If the payload references domain types, follow the import and read that declaration

## Key Rules

- **Before handlers**: payload may be mutable — write to it to set computed fields. Throw to reject the operation.
- **After handlers**: payload reflects persisted state — do not mutate. Use for side effects.
- **Filter by entity**: handlers fire for ALL entities of that type. Always check `modelId`, `entity type`, etc.
- **Events extend** `DomainEvent<TPayload>` with `eventType` property (not `static type`)
- **Every event must** implement `getHandlerAbstraction()` returning its handler abstraction
- **Every handler abstraction** must have a namespace with `Interface` and `Event` types
- **Payload types** live in `abstractions.ts`; event classes live in `events.ts`
- **Publish order**: before event → operation → after event
- DI constructor parameter order must match the `dependencies` array order exactly
- Use `.js` extensions in import paths (ES modules)

## Related Skills

- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming
- **webiny-use-case-pattern** — UseCase implementation where events are published from
- **webiny-dependency-injection** — Injectable services catalog

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.