Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement event-driven communication using BlueprintEventBus for cross-module coordination. Use this skill when modules need to communicate without tight coupling, broadcasting domain events (task.created, member.added), subscribing to events with proper lifecycle management, and implementing event-driven workflows. Ensures events follow naming conventions ([module].[action]), include Blueprint context, and use takeUntilDestroyed() for automatic cleanup.
.claude/skills/aiskillstore-blueprinteventbus-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 106% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 100% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 101% | 0% |
This skill helps implement event-driven architecture using BlueprintEventBus.
✅ DO use EventBus for:
❌ DON'T use EventBus for:
typescriptinterface BlueprintEvent<T = any> { type: string; // Format: [module].[action] blueprintId: string; timestamp: Date; actor: string; // User ID who triggered event data: T; metadata?: Record<string, any>; }
[module].[action]Examples:
task.createdtask.updatedtask.deletedtask.assignedmember.addedmember.removedfile.uploadedblueprint.archivedtypescriptimport { inject } from '@angular/core'; import { BlueprintEventBus } from '@core/services/blueprint-event-bus.service'; @Injectable({ providedIn: 'root' }) export class TaskService { private eventBus = inject(BlueprintEventBus); private taskRepository = inject(TaskRepository); async createTask(blueprintId: string, task: CreateTaskDto): Promise<Task> { // 1. Execute business logic const created = await this.taskRepository.create(blueprintId, task); // 2. Publish domain event this.eventBus.publish({ type: 'task.created', blueprintId, timestamp: new Date(), actor: this.getCurrentUserId(), data: created }); return created; } async updateTask(taskId: string, updates: Partial<Task>): Promise<Task> { const task = await this.taskRepository.findById(taskId); if (!task) throw new Error('Task not found'); const updated = await this.taskRepository.update(taskId, updates); // Publish update event with before/after data this.eventBus.publish({ type: 'task.updated', blueprintId: task.blueprintId, timestamp: new Date(), actor: this.getCurrentUserId(), data: updated, metadata: { before: task, changes: updates } }); return updated; } }
typescript// Define event types interface TaskCreatedEvent extends BlueprintEvent<Task> { type: 'task.created'; } interface TaskAssignedEvent extends BlueprintEvent<{ task: Task; assignee: string; assigneeType: 'user' | 'team' | 'partner'; }> { type: 'task.assigned'; } // Publish with type safety this.eventBus.publish<TaskCreatedEvent>({ type: 'task.created', blueprintId: created.blueprintId, timestamp: new Date(), actor: this.getCurrentUserId(), data: created });
typescriptimport { Component, inject, signal, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BlueprintEventBus } from '@core/services/blueprint-event-bus.service'; @Component({ selector: 'app-task-list', template: `...` }) export class TaskListComponent { private eventBus = inject(BlueprintEventBus); private destroyRef = inject(DestroyRef); tasks = signal<Task[]>([]); ngOnInit(): void { // Subscribe to task.created events this.eventBus.subscribe('task.created') .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(event => { console.log('New task created:', event.data); this.tasks.update(tasks => [...tasks, event.data]); }); } }
typescriptimport { filter } from 'rxjs/operators'; @Component({ selector: 'app-blueprint-tasks', template: `...` }) export class BlueprintTasksComponent { private eventBus = inject(BlueprintEventBus); private destroyRef = inject(DestroyRef); blueprintId = input.required<string>(); tasks = signal<Task[]>([]); ngOnInit(): void { // Only listen to events in current Blueprint this.eventBus.subscribe('task.created') .pipe( filter(event => event.blueprintId === this.blueprintId()), takeUntilDestroyed(this.destroyRef) ) .subscribe(event => { this.tasks.update(tasks => [...tasks, event.data]); }); // Listen to updates this.eventBus.subscribe('task.updated') .pipe( filter(event => event.blueprintId === this.blueprintId()), takeUntilDestroyed(this.destroyRef) ) .subscribe(event => { this.tasks.update(tasks => tasks.map(t => t.id === event.data.id ? event.data : t) ); }); // Listen to deletions this.eventBus.subscribe('task.deleted') .pipe( filter(event => event.blueprintId === this.blueprintId()), takeUntilDestroyed(this.destroyRef) ) .subscribe(event => { this.tasks.update(tasks => tasks.filter(t => t.id !== event.data.id) ); }); } }
typescriptimport { merge } from 'rxjs'; ngOnInit(): void { // Listen to multiple event types merge( this.eventBus.subscribe('task.created'), this.eventBus.subscribe('task.updated'), this.eventBus.subscribe('task.deleted') ) .pipe( filter(event => event.blueprintId === this.blueprintId()), takeUntilDestroyed(this.destroyRef) ) .subscribe(event => { console.log('Task event:', event.type, event.data); this.refreshTasks(); }); }
typescript@Injectable({ providedIn: 'root' }) export class AuditLogService { private eventBus = inject(BlueprintEventBus); private auditLogRepository = inject(AuditLogRepository); constructor() { // Listen to ALL events for audit trail this.eventBus.subscribeAll() .pipe(takeUntilDestroyed()) .subscribe(event => { this.logEvent(event); }); } private async logEvent(event: BlueprintEvent): Promise<void> { await this.auditLogRepository.create({ eventType: event.type, blueprintId: event.blueprintId, actor: event.actor, timestamp: event.timestamp, data: event.data, metadata: event.metadata }); } }
typescript@Injectable({ providedIn: 'root' }) export class NotificationService { private eventBus = inject(BlueprintEventBus); private notificationRepository = inject(NotificationRepository); constructor() { // Listen to events that trigger notifications this.setupNotificationListeners(); } private setupNotificationListeners(): void { // Task assigned → notify assignee this.eventBus.subscribe('task.assigned') .pipe(takeUntilDestroyed()) .subscribe(async event => { await this.notifyUser(event.data.assignee, { title: 'New Task Assigned', message: `You have been assigned: ${event.data.task.title}`, blueprintId: event.blueprintId }); }); // Member added → notify member this.eventBus.subscribe('member.added') .pipe(takeUntilDestroyed()) .subscribe(async event => { await this.notifyUser(event.data.userId, { title: 'Added to Blueprint', message: `You have been added to ${event.data.blueprintName}`, blueprintId: event.blueprintId }); }); } }
typescript@Component({ selector: 'app-activity-feed', template: ` <div class="activity-feed"> @for (activity of activities(); track activity.id) { <div class="activity-item"> <span class="timestamp">{{ activity.timestamp | date }}</span> <span class="message">{{ activity.message }}</span> </div> } </div> ` }) export class ActivityFeedComponent { private eventBus = inject(BlueprintEventBus); private destroyRef = inject(DestroyRef); blueprintId = input.required<string>(); activities = signal<Activity[]>([]); ngOnInit(): void { // Listen to all events in Blueprint this.eventBus.subscribeAll() .pipe( filter(event => event.blueprintId === this.blueprintId()), takeUntilDestroyed(this.destroyRef) ) .subscribe(event => { this.addActivity({ id: crypto.randomUUID(), type: event.type, message: this.formatEventMessage(event), timestamp: event.timestamp }); }); } private formatEventMessage(event: BlueprintEvent): string { switch (event.type) { case 'task.created': return `Task "${event.data.title}" was created`; case 'task.assigned': return `Task assigned to ${event.data.assignee}`; case 'member.added': return `${event.data.userName} joined the Blueprint`; default: return `Event: ${event.type}`; } } private addActivity(activity: Activity): void { this.activities.update(activities => [activity, ...activities].slice(0, 50)); } }
typescript@Injectable({ providedIn: 'root' }) export class TaskWorkflowService { private eventBus = inject(BlueprintEventBus); private taskRepository = inject(TaskRepository); private notificationService = inject(NotificationService); constructor() { this.setupWorkflows(); } private setupWorkflows(): void { // When task is completed → trigger follow-up actions this.eventBus.subscribe('task.completed') .pipe(takeUntilDestroyed()) .subscribe(async event => { const task = event.data; // 1. Check if task has dependencies const dependentTasks = await this.taskRepository .findDependentTasks(task.id); // 2. Update dependent tasks for (const depTask of dependentTasks) { await this.taskRepository.update(depTask.id, { status: 'ready' }); } // 3. Notify stakeholders await this.notificationService.notifyTaskCompletion(task); }); } }
Reference implementation:
typescriptimport { Injectable } from '@angular/core'; import { Subject, Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; export interface BlueprintEvent<T = any> { type: string; blueprintId: string; timestamp: Date; actor: string; data: T; metadata?: Record<string, any>; } @Injectable({ providedIn: 'root' }) export class BlueprintEventBus { private eventStream = new Subject<BlueprintEvent>(); /** * Publish event to all subscribers */ publish<T = any>(event: BlueprintEvent<T>): void { this.eventStream.next(event); } /** * Subscribe to specific event type */ subscribe<T = any>(eventType: string): Observable<BlueprintEvent<T>> { return this.eventStream.asObservable().pipe( filter(event => event.type === eventType) ); } /** * Subscribe to all events */ subscribeAll(): Observable<BlueprintEvent> { return this.eventStream.asObservable(); } /** * Subscribe to events in specific Blueprint */ subscribeToBlueprintEvents(blueprintId: string): Observable<BlueprintEvent> { return this.eventStream.asObservable().pipe( filter(event => event.blueprintId === blueprintId) ); } }
typescriptdescribe('EventBus Integration', () => { let eventBus: BlueprintEventBus; let taskService: TaskService; beforeEach(() => { eventBus = TestBed.inject(BlueprintEventBus); taskService = TestBed.inject(TaskService); }); it('should publish event when task is created', (done) => { // Subscribe to event eventBus.subscribe('task.created').subscribe(event => { expect(event.type).toBe('task.created'); expect(event.data.title).toBe('Test Task'); done(); }); // Create task (triggers event) taskService.createTask('blueprint1', { title: 'Test Task' }); }); it('should filter events by Blueprint', (done) => { let receivedEvents = 0; eventBus.subscribeAll() .pipe(filter(event => event.blueprintId === 'blueprint1')) .subscribe(() => { receivedEvents++; }); // Publish events to different Blueprints eventBus.publish({ type: 'task.created', blueprintId: 'blueprint1', timestamp: new Date(), actor: 'user1', data: {} }); eventBus.publish({ type: 'task.created', blueprintId: 'blueprint2', timestamp: new Date(), actor: 'user1', data: {} }); setTimeout(() => { expect(receivedEvents).toBe(1); done(); }, 100); }); });
When integrating EventBus:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | pass→pass | 9,934 | 9,789 | -1% | 1 | 1 | 0% | 1,794 | 5,786 | +223% | 0 | 0 | — |
case-05 | pass→pass | 12,323 | 8,965 | -27% | 1 | 1 | 0% | 2,253 | 5,437 | +141% | 0 | 0 | — |
case-07 | fail→pass | 14,698 | 9,643 | -34% | 1 | 1 | 0% | 2,739 | 5,650 | +106% | 0 | 0 | — |
case-06 | pass→pass | 9,878 | 9,410 | -5% | 1 | 1 | 0% | 1,833 | 5,590 | +205% | 0 | 0 | — |
case-01 | fail→pass | 13,653 | 8,059 | -41% | 1 | 1 | 0% | 2,788 | 5,420 | +94% | 0 | 0 | — |
case-02 | fail→pass | 19,233 | 13,991 | -27% | 1 | 1 | 0% | 4,081 | 6,703 | +64% | 0 | 0 | — |
case-03 | fail→pass | 14,638 | 7,858 | -46% | 1 | 1 | 0% | 2,651 | 5,305 | +100% | 0 | 0 | — |
case-04 | pass→pass | 12,311 | 8,240 | -33% | 1 | 1 | 0% | 2,562 | 5,245 | +105% | 0 | 0 | — |
case-08 | fail→pass | 14,837 | 8,136 | -45% | 1 | 1 | 0% | 2,713 | 5,451 | +101% | 0 | 0 | — |
case-09 | pass→pass | 16,606 | 9,806 | -41% | 1 | 1 | 0% | 2,875 | 5,855 | +104% | 0 | 0 | — |
case-10 | pass→pass | 9,547 | 7,018 | -26% | 1 | 1 | 0% | 1,776 | 5,241 | +195% | 0 | 0 | — |
case-11 | fail→pass | 19,208 | 12,655 | -34% | 1 | 1 | 0% | 3,851 | 6,255 | +62% | 0 | 0 | — |
case-12 | pass→pass | 19,358 | 14,578 | -25% | 1 | 1 | 0% | 3,555 | 6,696 | +88% | 0 | 0 | — |
case-13 | fail→pass | 20,473 | 9,387 | -54% | 1 | 1 | 0% | 1,951 | 5,588 | +186% | 0 | 0 | — |
case-15 | fail→pass | 15,515 | 12,329 | -21% | 1 | 1 | 0% | 3,000 | 6,295 | +110% | 0 | 0 | — |
case-16 | fail→pass | 9,692 | 3,616 | -63% | 1 | 1 | 0% | 1,622 | 4,386 | +170% | 0 | 0 | — |
case-17 | fail→pass | 13,421 | 6,385 | -52% | 1 | 1 | 0% | 2,425 | 4,917 | +103% | 0 | 0 | — |
case-18 | pass→pass | 9,073 | 7,920 | -13% | 1 | 1 | 0% | 1,734 | 5,574 | +221% | 0 | 0 | — |
case-19 | pass→pass | 11,835 | 7,056 | -40% | 1 | 1 | 0% | 1,945 | 5,021 | +158% | 0 | 0 | — |
case-20 | pass→pass | 12,850 | 8,434 | -34% | 1 | 1 | 0% | 2,203 | 5,341 | +142% | 0 | 0 | — |
case-21 | pass→fail | 9,802 | 22,378 | +128% | 1 | 1 | 0% | 1,914 | 4,638 | +142% | 0 | 0 | — |
case-22 | pass→pass | 10,559 | 7,326 | -31% | 1 | 1 | 0% | 1,772 | 5,146 | +190% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +41 percentage points is the difference between those two pass rates over the 21 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.