"Create an event-handler worker (on-* operation) that subscribes to an existing or new domain event and does the work directly, optionally AI-powered and optionally emitting completion events. Use when adding an async subscriber to the v2 pub-sub architecture; enforces AsyncAPI handbook rules, snapshot payloads, x-subscription/x-emits wiring, and validation."
Install
npx skillscat add specfuse/authoring/create-worker Install via the SkillsCat registry.
Create an event-handler worker (on-* operation) that subscribes to an existing or new domain event.
Enforces: .specfuse/authoring/handbooks/AsyncAPI_Handbook.md
Before writing any files, read and internalize the authoritative rules:
- Read
.specfuse/authoring/handbooks/AsyncAPI_Handbook.md— Sections 0.4, 4.3, 5.1 - Read
api/specs/CLAUDE.md— Section 10 (AsyncAPI Files — Critical Rules) - Read
.specfuse/authoring/samples/message-samples.yaml— samples 5 (event handler) and 7 (emit declaration)
What this skill creates
An on-* event handler worker — a subscriber that reacts to a domain event and does work directly. No intermediate command dispatch.
This skill does NOT create scheduled jobs — use /create-job for cron-triggered dispatchers.
Inputs to clarify with the user
- Which domain does this worker belong to?
- Which event does it react to? (existing message, or a new one to create)
- What does the handler do? (brief description of the work)
- Does the handler publish events on completion? If yes, which ones?
- Is this an AI-powered worker? If yes, what task? Which entities does it read/write?
- Concurrency and timeout — how many parallel handlers, how long can one take?
- Criticality — what happens if this worker fails? (critical/high/medium/low)
Creation process
Step 1: Verify the triggering event exists
Check if the event message already exists in domains/{domain}/messages/. If not, create it:
- File:
domains/{domain}/messages/{EventName}.yaml(PascalCase, past tense) - Required:
x-message-category: event,x-label(entity + past-tense action),x-version - Action class is inferred from the suffix (handbook §2.2):
*Created→ payload[<aggregateId>, after]*Updated→ payload[<aggregateId>, before, after]*Deleted→ payload[<aggregateId>, before]- anything else → payload
[<aggregateId>, before, after, context?]ANDx-trigger-whenis REQUIRED
- Payload uses snapshot
$refs intodomains/{domain}/events/{Entity}Snapshot.yaml— NEVER raw entity types. One snapshot per entity, reused across all of its events. - Use
$refto existing OpenAPI models for shared types (enums, value objects) — never duplicate schemas - Add to the shared event topic in
async-common/channels/application-events.yaml
Step 2: Create the on-* operation
File: domains/{domain}/async-operations/on-{event-description}.yaml
action: receive
channel:
$ref: '../channels/{aggregate}-events.yaml'
tags:
- name: {Domain} # PascalCase, exactly one
summary: {Brief description}
description: |
{Detailed description of what the handler does.}
This handler does the work directly — no intermediate command dispatch.
traits:
- $ref: '../../../async-common/operation-traits/common.yaml#/reliableDelivery'
messages:
- $ref: '../messages/{EventName}.yaml'
x-worker: # REQUIRED on on-* operations
idempotent: true
inboxDedup: true # default true; set false ONLY for side-effect-free handlers
concurrency: {n}
timeout: {duration}
x-observability:
criticality: {level} # low | medium | high | critical (legacy 'normal' rejected)
sla:
maxProcessingTime: {duration}
alertOnDlq: true
x-subscription: # REQUIRED on event-topic receivers
name: on-{event-description} # MUST equal the operation file stem
# NO 'filter' field — filters are derived from messages: above.
# Generator emits: Label = '{Entity}.{Action}' (or OR-chain for multi-message subscribers)
# AND-merge tenant/channel scoping via requiredHeaders:
# requiredHeaders: { tenantId: '<guid>' } → AND user.tenantId = '<guid>'
# requiredHeaders: { channel: email } → AND user.channel = 'email' (requires x-envelope-promote on snapshot field)Step 3: If the handler publishes events, create emit-* declaration(s)
For each event the handler emits on completion:
3a. Create the emitted event message (if it doesn't exist):
- File:
domains/{domain}/messages/{EmittedEvent}.yaml - Same rules as Step 1
3b. Create the emit-* operation:
File: domains/{domain}/async-operations/emit-{event-description}.yaml
action: send
channel:
$ref: '../channels/{aggregate}-events.yaml'
tags:
- name: {Domain}
summary: Publish {EventName} after {description}
description: |
Publishing declaration for the on-{handler} worker.
{Brief description of when/why this event is published.}
This is NOT a worker — the code generator wires this publish call
into the on-{handler} handler class via x-emits.
messages:
- $ref: '../messages/{EmittedEvent}.yaml'Important: emit-* operations must NOT have x-worker or x-observability. They are declarations, not workers.
3c. Add x-emits to the on-* operation:
x-emits:
- event: {Entity}.{Action} # Matches emitted message's x-label
description: {When/why this event is published}Step 4: Register in asyncapi.yaml
- Add new channels under the correct domain section (if created)
- Add the
on-*operation under the correct domain section - Add the
emit-*operation(s) under the correct domain section (if created) - Add new domain tag to
info.tagsif this is a new async domain
Step 5: If AI-powered, add x-ai and cross-check entities
If the handler uses AI/LLM:
x-ai:
enabled: true
task: {generation|classification|extraction|...}
entities:
reads: [{Entity1}, {Entity2}]
creates: [{Entity3}]
updates: [{Entity4}]MANDATORY cross-check: every entity listed must have the corresponding aiAccess.operations in its OpenAPI x-entity. If not, stop and flag to the user before proceeding.
Step 6: Update OpenAPI operations (if event is API-triggered)
If the triggering event comes from a REST write operation, ensure that operation has x-emits listing the event.
Step 7: Validate
./scripts/specfuse/validate-async-structure.sh
./scripts/specfuse/validate-async-spectral.shFix any errors before presenting the result.
Step 8: Update flow documentation
- Check if a flow doc exists in
api/docs/flows/{domain}/covering this process - If yes: update it to reflect the new worker
- If no: create one using the template in
api/docs/flows/README.md
Checklist (verify before finishing)
-
on-*operation hasaction: receive,x-worker(withinboxDedup),x-subscription(name = file stem, NO authoredfilter),x-observability(criticality from low/medium/high/critical) -
emit-*declarations (if any) haveaction: sendand NOx-worker -
on-*operation hasx-emitslinking to eachemit-*declaration (if any) - Message(s) have
x-message-category: event,x-label,x-version -
x-label.entitymatches a schema withx-entityin OpenAPI - Payloads use
$refto OpenAPI models — no duplication - Event payloads use snapshot
$refs intoevents/{Entity}Snapshot.yamlper action class (*Created/*Updated/*Deleted/transition); never raw entity types - State-transition events carry
x-trigger-whenpredicate;*Created/*Updated/*Deleteddo not - If snapshot includes a property whose source entity field is
x-classification: [pii \| sensitive], snapshot file declaresx-snapshot-pii-acknowledged.{property}with justification ≥ 20 chars - Files follow naming conventions (PascalCase messages, kebab-case operations/channels)
- Root
asyncapi.yamlupdated with new channels and operations - Flow documentation created/updated
- AI workers have
x-ai.entitiescross-checked againstaiAccess - No removed v1 extensions used
- Validation passes with 0 errors