name: preferences-event-catalog-qlerify description: Qlerify to EventCatalog transformation workflow for converting event models to catalog artifacts. Load when transforming Qlerify exports to EventCatalog MDX.
Qlerify to EventCatalog transformation
Purpose
This document provides a self-contained workflow for transforming Qlerify Event Modeling JSON exports into EventCatalog-compatible MDX artifacts with JSON Schema. The transformation preserves algebraic structure from Event Modeling (events as free monoid elements, commands as validated functions, aggregates as Deciders) while generating discoverable documentation. Use this document as automatically loaded context when working with Qlerify exports or invoke it as a slash command when beginning transformation work.
Relationship to other documents
Cross-reference json-querying.md for duckdb and jaq query patterns used during structure discovery and extraction.
The transformation phase uses both tools: duckdb for analytical queries (aggregations, counts, relationship analysis) and jaq for structural extraction and JSON reshaping.
Cross-reference event-catalog-tooling.md for EventCatalog concepts, algebraic foundations (Decider pattern, free monoid events, module algebra services), and anti-patterns to avoid when generating catalog entries.
The output of this transformation workflow populates an EventCatalog that documents the algebraic structure discovered through Event Modeling.
Cross-reference schema-versioning.md for JSON Schema evolution strategies when extending generated schemas beyond initial transformation.
Qlerify JSON structure
Qlerify exports Event Modeling boards as JSON with the following top-level structure:
{
"id": "export-uuid",
"name": "Board Name",
"version": "1.0.0",
"domainEvents": [], // event nodes with embedded cards
"schemas": [], // Command/Query/Entity schemas
"lanes": [], // swim lanes (service boundaries)
"boundedContexts": [], // typically empty in current exports
"groups": [] // visual grouping metadata
}
Each domainEvent represents a node in the Event Modeling flow and contains:
{
"id": "event-uuid",
"description": "Event Past Tense Name",
"type": "bpmn:Task",
"laneId": "lane-uuid",
"parents": ["parent-event-uuid"], // temporal dependencies
"cards": [ // embedded colored cards
{
"cardType": {
"domainModelRole": "Command|AggregateRoot|ReadModel|GivenWhenThen|null",
"color": "#hex"
},
"text": "Card title",
"schemaId": "schema-uuid-or-null"
}
]
}
Card types by domain model role:
| Role | Color | Purpose | Schema Type | EventCatalog Target |
|---|---|---|---|---|
| Command | Blue | Imperative action request | Command schema | Command |
| AggregateRoot | Yellow | Consistency boundary | Entity schema | Entity (aggregateRoot: true) |
| ReadModel | Green | Query projection | Query schema | Query |
| GivenWhenThen | Purple | Behavior specification | none | Flow step summary (markdown) |
| null (UserStory) | Pink | Actor context | none | Flow step actor |
Schema objects provide type definitions:
{
"id": "schema-uuid",
"name": "SchemaName",
"type": "Command|Query|Entity",
"entityId": "card-uuid",
"boundedContext": null,
"fields": [
{
"name": "fieldName",
"dataType": "uuid|timestamp|int|boolean|string|enum|null",
"primaryKey": true,
"cardinality": "one-to-one|one-to-many|many-to-one|many-to-many",
"relatedEntityId": "entity-schema-uuid-or-null",
"exampleData": ["value1", "value2"] // for enums
}
]
}
Lanes define service boundaries:
{
"id": "lane-uuid",
"name": "LaneName",
"offset": 0
}
Parent-child relationships in the parents[] array establish temporal flow, enabling sequential ordering of events into flow steps.
D2 diagrams as complementary input
D2 diagrams provide visual structure that complements Qlerify JSON data. When Event Modeling sessions produce both Qlerify JSON exports and D2 diagrams, the two artifacts inform EventCatalog transformation from different perspectives: Qlerify provides detailed schemas and relationships, while D2 provides visual organization and service boundaries.
D2 can be generated from Qlerify JSON using automated transformation scripts, or created alongside Qlerify exports during Event Modeling sessions. In either case, D2 swimlanes map directly to EventCatalog service or domain organization, and D2 visual layout informs EventCatalog navigation structure.
D2 to EventCatalog mapping
D2 structural elements map to EventCatalog artifacts:
| D2 Element | EventCatalog Target | Notes |
|---|---|---|
| Container (actor/system) | Domain or Service directory | Swimlanes become service boundaries |
| Event node | Event MDX file | Past tense event name |
| Command node | Command MDX file | Imperative command name |
| Read Model node | Query documentation | Projection of event-sourced state |
| Cross-container arrows | Producer/consumer relationships | Service sends/receives cross-references |
| Node ordering (left-to-right) | Flow step sequence | Temporal ordering |
| Container grouping | Domain boundaries | Multiple containers may belong to one domain |
D2 swimlanes (containers) represent service boundaries in Event Modeling diagrams. Each swimlane maps to a service in EventCatalog, with the swimlane name converted to PascalCase for the service ID. When D2 diagrams group multiple swimlanes under a parent container, that parent becomes a domain grouping multiple services.
D2 event nodes within a swimlane map to events owned by the corresponding EventCatalog service. D2 command nodes map to commands consumed by the service containing them. D2 read model nodes map to queries produced by the owning service.
Cross-container arrows in D2 indicate inter-service communication.
An arrow from a command in Service A to an event in Service B means Service B produces that event in response to the command.
An arrow from an event in Service B to a command in Service C means Service C consumes that event and may trigger the command as a consequence.
These arrows directly populate the sends and receives arrays in EventCatalog service frontmatter.
Node ordering in D2 diagrams (left-to-right flow) corresponds to temporal event sequences. This ordering complements the parent-child relationships in Qlerify JSON, providing visual confirmation of flow steps. When D2 and Qlerify agree on ordering, transformation confidence is high. When they differ, the Qlerify parent-child relationships take precedence as they encode explicit temporal dependencies.
Workflow integration with D2
When both Qlerify JSON and D2 diagrams are available, integrate them during transformation:
Event Modeling Session
↓
├─→ Qlerify JSON Export (detailed schemas, relationships)
└─→ D2 Diagram(s) (visual structure, service boundaries)
↓
Discovery Phase: Cross-validate structure
├─→ Map D2 swimlanes to Qlerify lanes
├─→ Verify event ordering matches across artifacts
└─→ Identify discrepancies (pause for manual resolution)
↓
Transformation to EventCatalog MDX
The discovery phase queries (see "Discovery queries" section) should be augmented with D2 validation queries:
- Extract swimlane names from D2 and compare with Qlerify lane names
- Verify event nodes in D2 match domainEvents in Qlerify JSON (by name or description)
- Map D2 arrows to Qlerify parent-child relationships and flag inconsistencies
D2 diagrams are particularly valuable for:
- Clarifying ambiguous service boundaries when Qlerify lanes are unclearly named
- Visualizing cross-service dependencies that may be implicit in Qlerify JSON
- Confirming intended temporal ordering when parent-child relationships are complex (multiple parents, branching)
- Providing initial service grouping into domains before bounded context analysis
D2 structural queries
When D2 diagrams are available as .d2 files, extract structure using text processing:
# Extract container (swimlane) names
rg '^([a-zA-Z0-9_-]+):\s*\{' event-model.d2 -o -r '$1' | sort -u
# Extract event nodes (past tense patterns, capitalized)
rg '([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\s*:' event-model.d2 -o -r '$1' |
grep -E '(Created|Updated|Deleted|Reserved|Confirmed|Cancelled|Sent|Received)$'
# Extract command nodes (imperative patterns, often "Create", "Update", etc.)
rg '([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s*:' event-model.d2 -o -r '$1' |
grep -E '^(Create|Update|Delete|Reserve|Confirm|Cancel|Send|Receive)\s'
# Extract cross-container arrows (service dependencies)
rg '([a-zA-Z0-9_-]+)\.\S+\s*->\s*([a-zA-Z0-9_-]+)\.\S+' event-model.d2 -o -r '$1 -> $2' |
sort -u
These patterns extract D2 structure for comparison with Qlerify JSON during discovery. Exact regex patterns depend on D2 diagram conventions used during Event Modeling sessions.
D2 diagrams should be stored alongside Qlerify JSON exports in the source directory, referenced in transformation logs, and archived as input artifacts for traceability.
Discovery queries
Use these queries to understand the structure of an unknown Qlerify export before beginning transformation.
duckdb exploration
Inspect overall structure and relationships:
-- Count entities by type
SELECT
s.type,
COUNT(*) as count
FROM read_json_auto('export.json') AS root,
unnest(root.schemas) AS s
GROUP BY s.type
ORDER BY count DESC;
-- Analyze event flow depth (parent chains)
WITH RECURSIVE events AS (
SELECT
e.id::VARCHAR as event_id,
e.description::VARCHAR as description,
len(e.parents) as parent_count
FROM read_json_auto('export.json') AS root,
unnest(root.domainEvents) AS e
)
SELECT
parent_count,
COUNT(*) as events_with_this_depth
FROM events
GROUP BY parent_count
ORDER BY parent_count;
-- Find orphaned schemas (no card reference)
SELECT
s.id::VARCHAR as schema_id,
s.name::VARCHAR as schema_name,
s.type::VARCHAR as schema_type
FROM read_json_auto('export.json') AS root,
unnest(root.schemas) AS s
WHERE s.entityId::VARCHAR NOT IN (
SELECT c.schemaId::VARCHAR
FROM unnest(root.domainEvents) AS e,
unnest(e.cards) AS c
WHERE c.schemaId IS NOT NULL
);
-- Lane distribution (events per service)
SELECT
l.name::VARCHAR as lane_name,
COUNT(e.id) as event_count
FROM read_json_auto('export.json') AS root,
unnest(root.lanes) AS l
LEFT JOIN unnest(root.domainEvents) AS e ON e.laneId::VARCHAR = l.id::VARCHAR
GROUP BY l.name
ORDER BY event_count DESC;
-- Schema field complexity (average fields per schema type)
SELECT
s.type::VARCHAR as schema_type,
COUNT(DISTINCT s.id) as schema_count,
AVG(len(s.fields)) as avg_fields,
MAX(len(s.fields)) as max_fields
FROM read_json_auto('export.json') AS root,
unnest(root.schemas) AS s
GROUP BY s.type;
jaq exploration
Inspect structure and extract metadata:
# Top-level keys to understand export format version
jaq 'keys' export.json
# Card type distribution by role
jaq '[.domainEvents[].cards[].cardType.domainModelRole] |
group_by(.) |
map({role: .[0], count: length})' export.json
# Schema types and names
jaq '.schemas | map({type: .type, name: .name}) |
group_by(.type) |
map({type: .[0].type, schemas: map(.name)})' export.json
# Lane names (future service names)
jaq '.lanes | map(.name)' export.json
# Events with no parents (flow entry points)
jaq '[.domainEvents[] | select(.parents | length == 0) | .description]' export.json
# Events with multiple parents (flow joins)
jaq '[.domainEvents[] | select(.parents | length > 1) |
{event: .description, parent_count: (.parents | length)}]' export.json
# Schema fields requiring type inference (null dataType)
jaq '.schemas[] |
select(.fields[] | .dataType == null) |
{schema: .name, fields_needing_inference: [.fields[] | select(.dataType == null) | .name]}' export.json
# Enum fields with example data
jaq '.schemas[].fields[] |
select(.dataType == "enum") |
{field: .name, values: .exampleData}' export.json
Entity mapping rules
Map Qlerify elements to EventCatalog types according to this consolidated specification:
| Qlerify Element | EventCatalog Type | Location Pattern | Notes |
|---|---|---|---|
| Lane | Service | domains/{domain}/services/{service-id}/ |
One service per lane, PascalCase ID from lane name |
| domainEvent.description | Event name | services/{service-id}/events/{event-id}/ |
Past tense, extract from containing event |
| Command card + schema | Command | services/{service-id}/commands/{command-id}/ |
Blue card, schema.type = "Command" |
| AggregateRoot card + schema | Entity | domains/{domain}/entities/{entity-id}/ |
Yellow card, schema.type = "Entity", aggregateRoot: true |
| ReadModel card + schema | Query | services/{service-id}/queries/{query-id}/ |
Green card, schema.type = "Query" |
| GivenWhenThen card | Flow step summary | Embedded in flow step markdown | Purple card, no schema |
| UserStory card (role: null) | Flow step actor | Flow step actor field | Pink card, no schema |
| parent-child chain | Flow steps | domains/{domain}/flows/{flow-id}/ |
Sequential ordering via parents[] array |
| export root | Domain | domains/{domain-id}/ |
Single domain per export |
Naming conventions for generated IDs:
| Element | ID Convention | Example |
|---|---|---|
| Domain | kebab-case from name | data-ingestion |
| Service | PascalCase from lane.name + "Service" | AutomationService |
| Event | PascalCase from domainEvent.description | InventoryReserved |
| Command | PascalCase from schema.name | ReserveInventory |
| Query | PascalCase from schema.name | GetInventoryStatus |
| Entity | PascalCase from schema.name | Inventory |
| Flow | kebab-case from description | reserve-and-confirm-flow |
Schema translation rules
Translate Qlerify schema fields to JSON Schema draft-07 according to these condensed rules from schema-translation-quick-reference.md:
Type mapping
| Qlerify dataType | JSON Schema type | JSON Schema format | Notes |
|---|---|---|---|
| uuid | string | uuid | High-confidence identifier |
| timestamp | string | date-time | ISO 8601 timestamp |
| int | integer | - | Whole numbers |
| boolean | boolean | - | true/false |
| string | string | - | Text data |
| enum | string | - | Plus enum: [] array from exampleData |
| null (missing) | (inferred) | (inferred) | Apply field name pattern matching |
Field name inference patterns
When dataType: null, infer types from field name patterns:
| Pattern | Inferred Type | Format | Confidence |
|---|---|---|---|
*Id, *ID, id |
string | uuid | high |
*At, *Date, *Time, created*, updated* |
string | date-time | high |
is*, has*, can*, *Flag |
boolean | - | high |
*Count, *Quantity, *Number, *Index |
integer | - | medium |
*Amount, *Price, *Total, *Rate |
number | - | medium |
email, *Email |
string | high | |
url, *Url, *URL, *Link |
string | uri | high |
phone, *Phone |
string | - | medium |
status, state, *Status, *State |
string | - | low (check for enum) |
Generate warnings for medium/low confidence inferences and include them in schema descriptions.
Required field determination
| Condition | Required? | Notes |
|---|---|---|
primaryKey: true |
yes | Always required |
cardinality: "one-to-one" |
yes | Exactly one relation |
cardinality: "one-to-many" |
yes | At least one relation |
cardinality: "many-to-one" |
no | Optional foreign key |
cardinality: "many-to-many" |
no | Join table handles |
| Field name contains "optional" | no | Explicit marker |
| No metadata (default) | yes | Conservative default |
JSON Schema template
Generate schemas using this structure:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "urn:qlerify:schema:{type}:{kebab-case-name}",
"title": "{Original Schema Name}",
"description": "{Type} schema generated from Qlerify Event Modeling export",
"type": "object",
"properties": {
"{fieldName}": {
"type": "{json-type}",
"format": "{format-if-applicable}",
"description": "{field-description} (source: {confidence-note})",
"examples": ["{example-value}"]
}
},
"required": ["{required-field-names}"],
"additionalProperties": false
}
Include confidence notes in descriptions:
- Explicit type from Qlerify:
"(source: explicit Qlerify dataType)" - High-confidence inference:
"(source: inferred from field name pattern)" - Medium-confidence inference:
"(source: inferred with medium confidence - verify)" - Low-confidence inference:
"(source: low confidence inference - manual review required)"
Enum handling
For fields with dataType: "enum", extract values from exampleData array:
{
"status": {
"type": "string",
"enum": ["pending", "confirmed", "cancelled"],
"description": "Order status (source: enum values from Qlerify examples)",
"examples": ["pending"]
}
}
Generate high-severity warning if dataType: "enum" but exampleData is empty or missing.
Relationship handling
For fields with relatedEntityId populated:
{
"customerId": {
"type": "string",
"format": "uuid",
"description": "References Customer entity (Qlerify entityId: {uuid})"
}
}
Build entity lookup table during transformation to resolve UUIDs to entity names for richer descriptions.
Transformation workflow
Execute transformation in sequential phases, each building on previous phase outputs.
Phase 1: Discover structure
Run discovery queries from the "Discovery queries" section to understand:
- How many schemas of each type exist
- Lane distribution and service boundaries
- Event flow depth and complexity
- Fields requiring type inference
- Orphaned schemas or missing relationships
If D2 diagrams are available, run D2 structural queries from "D2 diagrams as complementary input" section and cross-validate:
- Compare D2 swimlane names with Qlerify lane names (should align)
- Verify D2 event node names match Qlerify domainEvent descriptions
- Map D2 cross-container arrows to Qlerify parent-child relationships
- Flag discrepancies for manual review before proceeding
Generate a summary report showing counts, warnings, structural observations, and D2 validation results before proceeding to extraction.
Phase 2: Extract domain
Create the root domain structure.
Since Qlerify exports typically have boundedContexts: [], use a single domain derived from export metadata or user input.
Extract domain metadata:
# Get export name and version
jaq '{name: .name, version: .version, id: .id}' export.json
Generate domains/{domain-id}/index.mdx:
---
id: {kebab-case-name}
name: {Export Name}
version: 1.0.0
summary: Event-driven domain model generated from Qlerify Event Modeling export
owners: []
---
# {Domain Name}
Generated from Qlerify export `{export.name}` version `{export.version}`.
## Services
This domain contains {lane-count} services corresponding to Event Modeling swim lanes.
## Flow
The primary flow consists of {event-count} events organized into sequential steps.
Phase 3: Extract services from lanes
Each lane becomes a service. Extract lane data and generate service MDX files.
Query for lane-to-event mapping:
-- duckdb query to map lanes to their events and cards
SELECT
l.id::VARCHAR as lane_id,
l.name::VARCHAR as lane_name,
COUNT(DISTINCT e.id) as event_count,
list(DISTINCT c.cardType.domainModelRole::VARCHAR) as card_roles
FROM read_json_auto('export.json') AS root,
unnest(root.lanes) AS l
LEFT JOIN unnest(root.domainEvents) AS e ON e.laneId::VARCHAR = l.id::VARCHAR
LEFT JOIN unnest(e.cards) AS c ON true
GROUP BY l.id, l.name;
For each lane, generate domains/{domain-id}/services/{service-id}/index.mdx:
---
id: {PascalCaseName}Service
name: {Lane Name} Service
version: 0.0.1
summary: Service handling {lane-name} operations in Event Modeling flow
owners: []
sends: [] # populated in later phases
receives: [] # populated in later phases
entities: [] # populated when linking schemas
---
# {Service Name}
Service boundary corresponding to Event Modeling lane "{lane.name}".
## Responsibilities
This service handles {event-count} events in the overall flow.
Phase 4: Extract events from domainEvents
Each domainEvent becomes an Event entity.
Events are named from domainEvent.description (past tense).
Query to extract events with their lane associations:
jaq '.domainEvents | map({
id: .id,
name: .description,
laneId: .laneId,
parents: .parents
})' export.json
For each domainEvent, determine owning service from laneId, then generate domains/{domain-id}/services/{service-id}/events/{event-id}/index.mdx:
---
id: {PascalCaseDescription}
name: {domainEvent.description}
version: 0.0.1
summary: Event produced when {contextual-description}
producers:
- {ServiceId}
consumers: [] # populated from parent-child flow analysis
schemaPath: schema.json
---
# {Event Name}
This event is produced by {Service} when {trigger-description}.
## Temporal context
This event follows: {parent-event-names}
This event precedes: {child-event-names}
Generate minimal schema.json (events typically carry state deltas, not full schemas in Qlerify):
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "urn:qlerify:schema:event:{kebab-event-name}",
"title": "{EventName}",
"description": "Event schema (generated, extend as needed)",
"type": "object",
"properties": {
"eventId": {
"type": "string",
"format": "uuid",
"description": "Unique event identifier"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "Event occurrence timestamp"
}
},
"required": ["eventId", "timestamp"],
"additionalProperties": true
}
Note: Events in Event Modeling boards often don't have explicit schemas in Qlerify exports. Generate placeholder schemas and mark them for manual extension based on aggregate state requirements.
Phase 5: Extract commands from Command cards
Filter schemas with type: "Command" and their associated cards.
Query to extract command schemas:
jaq '.schemas | map(select(.type == "Command")) |
map({
id: .id,
name: .name,
entityId: .entityId,
fields: .fields
})' export.json
For each command schema, determine owning service from the card's domainEvent.laneId, then generate domains/{domain-id}/services/{service-id}/commands/{command-id}/index.mdx:
---
id: {SchemaName}
name: {Humanized Schema Name}
version: 0.0.1
summary: Command to {action-description}
producers: [] # inferred from flow (who triggers this command)
consumers:
- {ServiceId} # service in lane containing this command's card
schemaPath: schema.json
---
# {Command Name}
Imperative command validated and processed by {Service}.
## Validation
Validation rules enforced before command acceptance (extend based on business rules).
Generate schema.json using type translation rules:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "urn:qlerify:schema:command:{kebab-command-name}",
"title": "{SchemaName}",
"description": "Command schema generated from Qlerify",
"type": "object",
"properties": {
// Map each field from schema.fields[] using type mapping table
},
"required": [
// Fields marked required per required field determination rules
],
"additionalProperties": false
}
Phase 6: Extract queries from ReadModel cards
Filter schemas with type: "Query" and their associated ReadModel cards.
Query pattern identical to commands but filtering for type: "Query":
jaq '.schemas | map(select(.type == "Query")) |
map({
id: .id,
name: .name,
entityId: .entityId,
fields: .fields
})' export.json
Generate domains/{domain-id}/services/{service-id}/queries/{query-id}/index.mdx:
---
id: {SchemaName}
name: {Humanized Schema Name}
version: 0.0.1
summary: Query to retrieve {data-description}
producers:
- {ServiceId} # service providing the read model
consumers: [] # services consuming this query
schemaPath: schema.json
---
# {Query Name}
Read model query exposing projection of event-sourced state.
## Projection
This query returns data projected from events: {related-events}.
Generate schema.json using identical type translation as commands.
Phase 7: Extract entities from AggregateRoot cards
Filter schemas with type: "Entity" and their associated AggregateRoot cards.
Query to extract entity schemas:
jaq '.schemas | map(select(.type == "Entity")) |
map({
id: .id,
name: .name,
entityId: .entityId,
fields: .fields
})' export.json
Generate domains/{domain-id}/entities/{entity-id}/index.mdx:
---
id: {SchemaName}
name: {Schema Name}
version: 0.0.1
summary: Aggregate root representing {domain-concept}
owners: []
aggregateRoot: true
identifier: {primary-key-field-name}
properties:
- name: {field.name}
type: {json-type}
required: {true|false}
summary: {field-description}
---
# {Entity Name}
Aggregate root establishing consistency boundary for {related-operations}.
## Decider pattern
This aggregate implements decide and evolve functions:
- `decide`: Validates commands against current state, produces events
- `evolve`: Applies events to update aggregate state
## State reconstruction
Aggregate state is reconstructed by replaying events in temporal order.
Generate schema.json if properties array is insufficient for complex types:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "urn:qlerify:schema:entity:{kebab-entity-name}",
"title": "{SchemaName}",
"description": "Entity schema for aggregate root",
"type": "object",
"properties": {
// Map fields using type translation with special handling for primaryKey
},
"required": [
// Always include primaryKey field
],
"additionalProperties": false
}
Identify primary key from field with primaryKey: true or infer from patterns (id, {entityName}Id, first uuid field).
Phase 8: Generate flow from parent-child chains
Reconstruct temporal flow from domainEvent parent-child relationships.
Query to extract flow topology:
-- duckdb query to map event sequences
WITH event_graph AS (
SELECT
e.id::VARCHAR as event_id,
e.description::VARCHAR as event_name,
e.laneId::VARCHAR as lane_id,
unnest(e.parents)::VARCHAR as parent_id
FROM read_json_auto('export.json') AS root,
unnest(root.domainEvents) AS e
)
SELECT * FROM event_graph ORDER BY parent_id NULLS FIRST;
Identify flow entry points (events with empty parents[] array) and traverse forward, generating flow steps in sequence.
For each card type in a domainEvent, create flow step elements:
| Card Type | Flow Step Field | Mapping |
|---|---|---|
| UserStory (null role) | actor.name | card.text |
| GivenWhenThen | summary | card.text (markdown formatted) |
| Command | message.id | schema.name (PascalCase) |
| Event | (implicit) | domainEvent.description becomes step transition |
Generate domains/{domain-id}/flows/{flow-id}/index.mdx:
---
id: {flow-name-from-context}
name: {Flow Display Name}
version: 0.0.1
summary: End-to-end flow from {entry-point} to {terminal-event}
owners: []
steps:
- id: step-1
title: {domainEvent.description}
summary: {GivenWhenThen.text}
actor:
name: {UserStory.text}
message:
id: {CommandOrEvent.name}
version: "0.0.1"
service:
id: {ServiceId}
version: "0.0.1"
next_step:
id: step-2
label: "on success"
# ... subsequent steps following parent-child chain
---
# {Flow Name}
Sequential flow reconstructed from Event Modeling parent-child relationships.
## Flow visualization
This flow represents the temporal ordering discovered during Event Modeling sessions.
Handle branching flows (events with multiple children) by using next_steps array instead of next_step:
steps:
- id: step-n
# ... step fields
next_steps:
- id: step-success
label: "validation passed"
- id: step-failure
label: "validation failed"
Phase 9: Cross-reference and validate
Link all generated artifacts through cross-references.
Update service sends and receives arrays:
- A service
sendsan event if that event's domainEvent.laneId matches the service's lane - A service
receivesa command if that command's card appears in a domainEvent within the service's lane - A service
receivesan event if that event is a parent of any domainEvent in the service's lane
Update service entities arrays:
- Link entities whose schemas have
entityIdpointing to AggregateRoot cards in domainEvents within the service's lane
Update event consumers arrays:
- An event is consumed by services whose lanes contain domainEvents listing this event in their
parents[]array
Validation checklist:
- All schemas referenced in MDX frontmatter exist as
schema.jsonfiles - All service cross-references (
sends,receives) point to existing commands/events/queries - All entity cross-references use correct PascalCase IDs
- All flow steps reference valid messages and services
- All inferred types have confidence notes in schema descriptions
- All enum fields have populated
enumarrays - All aggregate roots have
aggregateRoot: trueandidentifierfields - Primary keys are included in
requiredarrays
Generate validation report listing warnings by severity (error, high, medium, low).
EventCatalog output structure
Complete directory tree for transformed artifacts:
domains/
{domain-id}/
index.mdx # Domain overview
services/
{service-id}/
index.mdx # Service overview
commands/
{command-id}/
index.mdx # Command metadata
schema.json # Command JSON Schema
events/
{event-id}/
index.mdx # Event metadata
schema.json # Event JSON Schema
queries/
{query-id}/
index.mdx # Query metadata
schema.json # Query JSON Schema
entities/
{entity-id}/
index.mdx # Entity metadata with properties
schema.json # Entity JSON Schema (optional)
flows/
{flow-id}/
index.mdx # Flow with steps array
File naming conventions:
- Directory names: kebab-case for domains/flows, PascalCase for services/entities
- MDX files: always
index.mdx - Schema files: always
schema.json
MDX frontmatter templates
Domain
---
id: {kebab-case-id}
name: {Display Name}
version: 1.0.0
summary: {Description}
owners: []
---
Service
---
id: {PascalCaseId}
name: {Display Name}
version: 0.0.1
summary: {Description}
owners: []
sends:
- id: {EventId}
version: "0.0.1"
receives:
- id: {CommandOrEventId}
version: "0.0.1"
entities:
- id: {EntityId}
version: "0.0.1"
---
Event
---
id: {PascalCaseId}
name: {Display Name}
version: 0.0.1
summary: {Description}
producers:
- {ServiceId}
consumers:
- {ServiceId}
schemaPath: schema.json
---
Command
---
id: {PascalCaseId}
name: {Display Name}
version: 0.0.1
summary: {Description}
producers:
- {ServiceId}
consumers:
- {ServiceId}
schemaPath: schema.json
---
Query
---
id: {PascalCaseId}
name: {Display Name}
version: 0.0.1
summary: {Description}
producers:
- {ServiceId}
consumers:
- {ServiceId}
schemaPath: schema.json
---
Entity
---
id: {PascalCaseId}
name: {Display Name}
version: 0.0.1
summary: {Description}
owners: []
aggregateRoot: true
identifier: {primaryKeyFieldName}
properties:
- name: {fieldName}
type: {jsonSchemaType}
required: {true|false}
summary: {description}
---
Flow
---
id: {kebab-case-id}
name: {Display Name}
version: 0.0.1
summary: {Description}
owners: []
steps:
- id: {step-id}
title: {Step Title}
summary: {Optional GivenWhenThen text}
actor:
name: {UserStory actor name}
message:
id: {CommandOrEventId}
version: "0.0.1"
service:
id: {ServiceId}
version: "0.0.1"
next_step: # single transition
id: {next-step-id}
label: {transition-label}
# OR next_steps for branching
next_steps:
- id: {step-id}
label: {branch-label}
---
Algebraic structure preservation
The transformation must preserve algebraic relationships discovered during Event Modeling.
Events form a free monoid under concatenation. The parent-child chains in Qlerify exports encode this temporal ordering. Flow steps preserve sequential composition, and the flow visualization communicates that events are elements in a temporal sequence, not isolated messages.
Commands are validated functions producing events. Command MDX should describe validation rules (preconditions) and the events produced on success versus failure. Use railway-oriented composition language: "validation rules enforced before command acceptance" rather than "command validation".
Aggregates implement the Decider pattern.
Entity MDX for AggregateRoot cards must reference the Decider structure explicitly: decide function (command → state → events) and evolve function (state → event → state).
Do not document aggregates as "entities with behavior" (object-oriented framing).
Services are module boundaries with explicit effects. Service MDX should list effects at the boundary (event store writes, external calls) and emphasize that internal logic is pure. Following Debasish Ghosh's module algebra pattern, services expose signatures (abstract interfaces), algebras (implementations), and interpreters (effect handlers).
Avoid anti-patterns from event-catalog-tooling.md:
- Do not organize by "entities" (organize by services and aggregates)
- Do not frame commands as "requests" and events as "responses"
- Do not imply class hierarchies in event documentation
- Do not document events without Decider context
- Do not hide effects in implicit descriptions
Validation and quality checks
After completing transformation, validate output quality.
Structural validation
Run these checks programmatically:
# Verify all schema.json files are valid JSON Schema draft-07
fd -e json -x jaq 'if ."$schema" == "http://json-schema.org/draft-07/schema#"
then "valid" else "invalid: " + . end' {} \;
# Verify all MDX files have required frontmatter fields
fd index.mdx -x sh -c 'echo "=== {} ===" && head -20 {} | grep "^id:"'
# Count generated artifacts by type
echo "Services: $(fd -t d . domains/*/services | wc -l)"
echo "Events: $(fd index.mdx domains/*/services/*/events | wc -l)"
echo "Commands: $(fd index.mdx domains/*/services/*/commands | wc -l)"
echo "Queries: $(fd index.mdx domains/*/services/*/queries | wc -l)"
echo "Entities: $(fd index.mdx domains/*/entities | wc -l)"
echo "Flows: $(fd index.mdx domains/*/flows | wc -l)"
Semantic validation
Check for completeness and correctness:
- Every schema file has a corresponding MDX file in the same directory
- Every service
sendsreference points to an existing event or command - Every service
receivesreference points to an existing command or event - Every service
entitiesreference points to an existing entity - Every flow step
message.idpoints to an existing command, event, or query - Every flow step
service.idpoints to an existing service - Every entity marked
aggregateRoot: truehas anidentifierfield - Every entity schema includes the identifier field in
requiredarray - All inferred types (medium/low confidence) are documented in schema descriptions
- All enum fields have non-empty
enumarrays
Quality metrics
Generate quality report:
# Type inference statistics
jaq '.schemas[].fields[] |
select(.dataType == null) |
.name' export.json | wc -l
# Report: {count} fields required type inference
# Schema complexity distribution
jaq '.schemas | map({name, field_count: (.fields | length)}) |
sort_by(.field_count) | reverse' export.json
# Report: Most complex schemas for review
# Flow coverage
# Compare: number of domainEvents vs number of flow steps
# Report: {coverage}% of events incorporated into flows
Future extensions
This workflow can be extended to handle additional Qlerify features as they become available.
Bounded context support: When Qlerify exports include populated boundedContexts arrays, extend Phase 2 to generate multiple domains with context mapping documentation per bounded-context-design.md.
Channel extraction: EventCatalog supports channels (message buses, topics, queues) as first-class entities. Currently we generate placeholder channels (command-bus-channel, event-bus-channel). Future versions could extract channel metadata from Qlerify if exports include infrastructure details.
AsyncAPI integration: Generate AsyncAPI specifications alongside EventCatalog MDX for machine-readable event contracts that support runtime validation.
Schema registry integration: Export generated JSON Schemas to Confluent Schema Registry, AWS Glue Schema Registry, or EventCatalog's schema versioning features per schema-versioning.md.
Decider code generation: Use entity schemas and command/event mappings to generate Decider skeleton code in target languages (Rust, TypeScript, Haskell) following patterns from domain-modeling.md and language-specific preferences.