install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/events-event-schema" ~/.claude/skills/intense-visions-harness-engineering-events-event-schema && rm -rf "$T"
manifest:
agents/skills/claude-code/events-event-schema/SKILL.mdsource content
Events: Event Schema
Define and evolve event schemas using a schema registry with Avro, Protobuf, or JSON Schema.
When to Use
- Multiple services exchange events and must agree on the data structure
- You need to evolve event schemas without breaking existing consumers
- You're using Kafka and need schema validation at the broker level
- You want compile-time type safety for event payloads across services
- You're building a long-lived event-sourced system where events are stored permanently
Instructions
JSON Schema + Zod (TypeScript-native, no registry needed for small teams):
import { z } from 'zod'; // Define schemas with Zod — generates TypeScript types automatically const OrderCreatedEventSchema = z.object({ eventId: z.string().uuid(), eventType: z.literal('ORDER_CREATED'), schemaVersion: z.literal(1), aggregateId: z.string(), aggregateType: z.literal('Order'), occurredAt: z.string().datetime(), payload: z.object({ orderId: z.string().uuid(), userId: z.string().uuid(), items: z.array( z.object({ productId: z.string(), quantity: z.number().int().positive(), unitPrice: z.number().positive(), }) ), totalAmount: z.number().positive(), currency: z.string().length(3), // ISO 4217 }), }); type OrderCreatedEvent = z.infer<typeof OrderCreatedEventSchema>; // Envelope schema for all events const EventEnvelopeSchema = z.object({ eventId: z.string().uuid(), eventType: z.string(), schemaVersion: z.number().int().positive(), aggregateId: z.string(), occurredAt: z.string().datetime(), correlationId: z.string().uuid().optional(), causationId: z.string().uuid().optional(), }); // Validate incoming events function parseOrderEvent(raw: unknown): OrderCreatedEvent { return OrderCreatedEventSchema.parse(raw); // throws ZodError if invalid } // Create events with correct structure function createOrderEvent(order: Order, correlationId?: string): OrderCreatedEvent { return { eventId: crypto.randomUUID(), eventType: 'ORDER_CREATED', schemaVersion: 1, aggregateId: order.id, aggregateType: 'Order', occurredAt: new Date().toISOString(), payload: { orderId: order.id, userId: order.userId, items: order.items, totalAmount: order.total, currency: 'USD', }, }; }
Schema versioning strategy:
// Version 1 const UserCreatedV1 = z.object({ schemaVersion: z.literal(1), userId: z.string(), email: z.string().email(), }); // Version 2 — backward compatible (added optional field) const UserCreatedV2 = z.object({ schemaVersion: z.literal(2), userId: z.string(), email: z.string().email(), name: z.string().optional(), // new optional field — safe addition }); // Discriminated union for versioned parsing const UserCreatedEvent = z.discriminatedUnion('schemaVersion', [UserCreatedV1, UserCreatedV2]); function parseUserCreated(raw: unknown) { const event = UserCreatedEvent.parse(raw); // Normalize to latest version if (event.schemaVersion === 1) { return { ...event, schemaVersion: 2 as const, name: undefined }; } return event; }
Event registry for type-safe dispatch:
// Central event registry type EventRegistry = { 'order.created': OrderCreatedEvent; 'order.shipped': OrderShippedEvent; 'user.created': UserCreatedEvent; 'payment.failed': PaymentFailedEvent; }; type EventType = keyof EventRegistry; // Schema map for validation const eventSchemas: { [K in EventType]: z.ZodType<EventRegistry[K]> } = { 'order.created': OrderCreatedEventSchema, 'order.shipped': OrderShippedEventSchema, 'user.created': UserCreatedEventSchema, 'payment.failed': PaymentFailedEventSchema, }; function validateEvent<T extends EventType>(type: T, raw: unknown): EventRegistry[T] { return eventSchemas[type].parse(raw) as EventRegistry[T]; }
Details
Schema compatibility rules (Avro / Confluent conventions apply to JSON Schema too):
| Change | Backward Compatible | Forward Compatible |
|---|---|---|
| Add optional field | Yes | Yes |
| Add required field | No | Yes |
| Remove optional field | Yes | No |
| Remove required field | No | No |
| Rename field | No | No (use aliases) |
| Change field type | No | No |
Safe evolution pattern: Never remove or rename fields. Add fields as optional. Bump
schemaVersion. Keep old schemas in the registry for producers still on v1.
Confluent Schema Registry (for Kafka): Stores Avro/Protobuf/JSON schemas. Producers register schemas; consumers validate against them. Enforces compatibility rules at publish time — prevents breaking changes from reaching consumers.
Anti-patterns:
- Using
or untyped JSON as event payloads — schema drift becomes undetectableany - Breaking schema changes without a version bump — consumers parse incorrectly in silence
- Storing schema versions only in docs, not in the event envelope — impossible to know how to parse at runtime
Event ID for idempotency: Always include
eventId (UUID). Consumers use it to deduplicate redelivered events.
Source
microservices.io/patterns/data/event-sourcing.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.