Skip to main content

Event Listeners & Event-Driven Patterns

Overview

Ever Works uses the @nestjs/event-emitter package (built on eventemitter2) for in-process event-driven communication. The system defines two layers of events: domain events (work creation, user lifecycle, generation completion) handled by NestJS services via the @OnEvent() decorator, and pipeline runtime events emitted during step execution. Events decouple producers from consumers, allowing services like mail, cleanup, and analytics to react to domain changes without tight coupling.

Architecture

Source Files

FilePurpose
packages/agent/src/events/base.tsAbstract BaseEvent class with static EVENT_NAME
packages/agent/src/events/work-created.event.tsWorkCreatedEvent domain event
packages/agent/src/events/work-generation-completed.event.tsWorkGenerationCompletedEvent domain event
apps/api/src/events/index.tsAPI-layer user lifecycle events (created, forgot password, etc.)
packages/plugin/src/events/event-types.tsTyped event names and payload interfaces for the plugin system
packages/agent/src/pipeline/step-pipeline-executor.service.tsPipeline runtime event emission
packages/agent/src/pipeline/executable-pipeline.class.tsPipeline runner state change events
apps/api/src/mail/mail.service.tsEvent listener: sends emails on user events
apps/api/src/works/tasks/work-cleanup.service.tsEvent listener: clears cache on generation completion

Key Classes

BaseEvent (Agent Package)

The abstract base for all agent-level domain events:

export abstract class BaseEvent {
static EVENT_NAME: string;
}

Domain Event Classes

Each event extends BaseEvent and carries a typed payload:

export class WorkCreatedEvent extends BaseEvent {
static EVENT_NAME = 'work.created';

constructor(public readonly work: Work) {
super();
}
}

export class WorkGenerationCompletedEvent extends BaseEvent {
static EVENT_NAME = 'work.generation.completed';

constructor(public readonly work: Work) {
super();
}
}

API User Events

The API application defines its own event hierarchy for user lifecycle operations:

export abstract class BaseUserEvent {
public abstract user: User;
}

export class UserCreatedEvent extends BaseUserEvent {
static EVENT_NAME = 'user.created';

constructor(
public user: User,
public confirmationToken: string,
public confirmationUrl: string
) {
super();
}
}

export class MemberInvitedEvent {
static EVENT_NAME = 'work.member_invited';

constructor(
public invitee: User,
public inviter: User,
public work: Work,
public role: string,
public workUrl: string
) {}
}

Full list of API user events:

Event ClassEVENT_NAMEPayload
UserCreatedEventuser.createduser, confirmationToken, confirmationUrl
UserForgotPasswordEventuser.forgot_passworduser, resetToken, resetUrl, expiresIn
UserPasswordChangedEventuser.password_changeduser, changedAt, ipAddress, location, device
UserConfirmedEventuser.confirmeduser, dashboardUrl
UserNewDeviceLoginEventuser.new_device_loginuser, loginTime, device, browser, location, ipAddress
UserAccountDeletionEventuser.delete_accountuser, deleteToken, deleteUrl, expiresIn
MemberInvitedEventwork.member_invitedinvitee, inviter, work, role, workUrl

Plugin Event Type System

The @ever-works/plugin package defines a comprehensive typed event system for plugins:

// Event name union types
type PluginLifecycleEvent =
| 'plugin:loaded'
| 'plugin:enabled'
| 'plugin:disabled'
| 'plugin:unloaded'
| 'plugin:error'
| 'plugin:settings-changed';

type WorkEvent =
| 'work:created'
| 'work:updated'
| 'work:deleted'
| 'work:deployed'
| 'work:generation-started'
| 'work:generation-completed'
| 'work:generation-failed';

type ItemEvent = 'item:created' | 'item:updated' | 'item:deleted' | 'item:extracted' | 'item:validated';

type PipelineEvent =
| 'pipeline:started'
| 'pipeline:step-started'
| 'pipeline:step-completed'
| 'pipeline:step-failed'
| 'pipeline:completed'
| 'pipeline:failed'
| 'pipeline:cancelled';

type SystemEvent = 'system:startup' | 'system:shutdown' | 'system:health-check';

Each event name maps to a typed payload interface via PluginEventPayloads:

interface PluginEventPayloads {
'plugin:loaded': PluginLoadedPayload;
'plugin:error': PluginErrorPayload;
'plugin:settings-changed': PluginSettingsChangedPayload;
'work:generation-completed': WorkGenerationCompletedPayload;
'pipeline:step-completed': PipelineStepCompletedPayload;
'pipeline:failed': PipelineFailedPayload;
// ... all events mapped to payloads
}

Pipeline Runtime Events

The pipeline executor emits events during step execution:

export const PipelineEvents = {
STARTED: 'pipeline:started',
STEP_STARTED: 'pipeline:step-started',
STEP_COMPLETED: 'pipeline:step-completed',
STEP_FAILED: 'pipeline:step-failed',
STEP_SKIPPED: 'pipeline:step-skipped',
COMPLETED: 'pipeline:completed',
FAILED: 'pipeline:failed',
CANCELLED: 'pipeline:cancelled'
} as const;

The ExecutablePipelineRunner also emits lower-level state change events:

export const PipelineRuntimeEvents = {
STATE_CHANGED: 'pipeline:state-changed',
STEP_STATUS_CHANGED: 'pipeline:step-status-changed'
} as const;

export interface StateChangePayload {
stepId: string;
previousStatus: StepStatus;
newStatus: StepStatus;
timestamp: number;
}

Configuration

EventEmitter Module Registration

The EventEmitterModule is registered at the root of both the API application and the Pipeline module:

// apps/api/src/api.module.ts
@Module({
imports: [
EventEmitterModule.forRoot()
// ... other modules
]
})
export class ApiModule {}

// packages/agent/src/pipeline/pipeline.module.ts
@Module({
imports: [FacadesModule, EventEmitterModule.forRoot()]
// ...
})
export class PipelineModule {}

Listener Registration

Services become event listeners by using the @OnEvent() decorator on methods:

import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class MailService {
@OnEvent(UserCreatedEvent.EVENT_NAME)
async sendSignupConfirmation(data: UserCreatedEvent): Promise<void> {
await this.mailerService.sendMail({
/* ... */
});
}
}

Code Examples

Emitting a Domain Event

import { EventEmitter2 } from '@nestjs/event-emitter';
import { WorkCreatedEvent } from '@ever-works/agent/events';

@Injectable()
export class WorkLifecycleService {
constructor(private readonly eventEmitter: EventEmitter2) {}

async createWork(dto: CreateWorkDto, userId: string): Promise<Work> {
const work = await this.repository.save(/* ... */);

// Emit the event -- all @OnEvent('work.created') listeners fire
this.eventEmitter.emit(WorkCreatedEvent.EVENT_NAME, new WorkCreatedEvent(work));

return work;
}
}

Listening for Generation Completion

import { OnEvent } from '@nestjs/event-emitter';
import { WorkGenerationCompletedEvent } from '@ever-works/agent/events';

@Injectable()
export class WorkCleanupService {
@OnEvent(WorkGenerationCompletedEvent.EVENT_NAME)
clearWorkCache(data: WorkGenerationCompletedEvent) {
this.cacheRepository.typeormAdapter
.deleteUnscopedEntriesLike(data.work.id)
.then(() => {
this.logger.log(`Cache cleared for work ${data.work.id}`);
})
.catch((err) => {
this.logger.error('Failed to clear cache:', err);
});
}
}

Listening for Pipeline Events

import { OnEvent } from '@nestjs/event-emitter';
import type { PipelineStepCompletedPayload, PipelineFailedPayload } from '@ever-works/plugin';

@Injectable()
export class PipelineMonitoringService {
@OnEvent('pipeline:step-completed')
onStepCompleted(payload: PipelineStepCompletedPayload) {
this.logger.log(
`Step "${payload.stepName}" completed in ${payload.duration}ms ` +
`(${payload.stepIndex + 1}/${payload.totalSteps})`
);
}

@OnEvent('pipeline:failed')
onPipelineFailed(payload: PipelineFailedPayload) {
this.logger.error(`Pipeline failed at step "${payload.failedStep}": ${payload.error}`);
// Send notification, update status, etc.
}
}

Creating a Custom Event

import { BaseEvent } from '@ever-works/agent/events';
import { Work } from '@ever-works/agent/entities';

export class WorkDeployedEvent extends BaseEvent {
static EVENT_NAME = 'work.deployed';

constructor(
public readonly work: Work,
public readonly deployUrl: string,
public readonly provider: string,
) {
super();
}
}

// Emit it:
this.eventEmitter.emit(
WorkDeployedEvent.EVENT_NAME,
new WorkDeployedEvent(work, url, 'vercel'),
);

// Listen for it:
@OnEvent(WorkDeployedEvent.EVENT_NAME)
async onWorkDeployed(event: WorkDeployedEvent) {
await this.notificationService.notify(
event.work.creator.id,
`Work deployed to ${event.deployUrl}`,
);
}

Plugin Event Handler Type Safety

import type { EventHandler, PluginEventName } from '@ever-works/plugin';

// Type-safe event handler
const handler: EventHandler<'pipeline:step-completed'> = (payload) => {
// payload is PipelineStepCompletedPayload
console.log(payload.stepName, payload.duration);
};

// Plugin event emitter interface
interface PluginEventEmitter {
on<T extends PluginEventName>(event: T, handler: EventHandler<T>): EventSubscription;
once<T extends PluginEventName>(event: T, handler: EventHandler<T>): EventSubscription;
emit<T extends PluginEventName>(event: T, payload: PluginEventPayloads[T]): void;
}

Best Practices

  1. Use static EVENT_NAME constants -- define the event name as a static property on the event class to avoid magic strings and enable IDE navigation.

  2. Extend BaseEvent -- for agent-level domain events, extend the BaseEvent class to maintain consistency across the codebase.

  3. Keep listeners idempotent -- event handlers may be retried or called multiple times. Design them to be safe to repeat.

  4. Handle errors in listeners -- wrap listener logic in try/catch blocks. An unhandled error in a listener will not propagate back to the emitter by default, but it may crash the process.

  5. Use typed payloads -- always type the event parameter in @OnEvent() handlers to match the event class or payload interface.

  6. Prefer async listeners -- use async methods for @OnEvent() handlers to avoid blocking the event loop during I/O operations.

  7. Register EventEmitterModule once -- the module should be registered with forRoot() at the application root. Additional calls in sub-modules are harmless but unnecessary.

  8. Use the plugin event type system -- when building plugins, use the typed event names and payloads from @ever-works/plugin for type safety across the event boundary.