Learn ยท 03 of 5

The language

How documents, identities, references, fields, and flow work.

The document model

A document contains domain catalogs, workshop notation, and workflows. Catalogs define contexts, actors, ownership, Events, aggregates, and field types. Workflows describe behavior. Chapters and hotspots retain workshop context.

After validation, downstream tools use a normalized typed model with derived titles and source-to-target edges.

Identities and derived titles

state_change "add_pet" { }

The identity is add_pet, and its derived display title is Add Pet. Labels use lower_snake_case. Write title only for different wording:

bounded_context "billing" { event "payment_failed" { title = "Card Payment Was Declined" } }

The formatter leaves derived titles implicit. Source order is model order, so keep workflows in business-time order.

Typed references: the most important syntax habit

Quoted values are data. Unquoted traversals are references. For example, to = [event.pet_management.pet_added] resolves a relationship; quoting that traversal turns it into an invalid string.

TargetForm
Bounded contextbounded_context.clinic
Actoractor.clinic_staff
Teamteam.clinic_team
Systemsystem.partner
Workflowworkflow.add_pet
Eventevent.pet_management.pet_added
Aggregateaggregate.pet_management.pet
Local aggregate, inside owning contextaggregate.pet
Field typefield_type.pet_management.pet_id
Local field type, inside owning contextfield_type.pet_id
Local workflow elementcommand.add_pet
Workflow-qualified element, for a hotspotcommand.add_pet.add_pet_command

Bounded contexts own domain contracts

Events, aggregates, and field types belong to a bounded context:

bounded_context "pet_management" { aggregate "pet" { } field_type "pet_id" { type = "Int" id_attribute = true example = 5 } event "pet_added" { aggregate = aggregate.pet field "pet_id" { type = field_type.pet_id } } }

Inside the owning context, references are local: aggregate.pet, field_type.pet_id. Outside it, use the qualified form: aggregate.pet_management.pet, field_type.pet_management.pet_id, event.pet_management.pet_added.

Aggregates

An aggregate is a context-owned consistency boundary:

bounded_context "pet_management" { aggregate "pet" { title = "Pet" description = "Consistency boundary for pet details." } }

Use aggregate_dependencies when behavior spans another consistency boundary.

Reusable field types

Use a field_type when the same ubiquitous-language concept appears in multiple contracts:

bounded_context "owner_management" { field_type "owner_id" { type = "Int" example = 42 id_attribute = true } event "owner_registered" { field "owner_id" { type = field_type.owner_id } } }

Inline one-off fields with a built-in type, such as field "request_id" { type = "UUID" }. The reference lists available types and flags.

Field shorthand

A plain field can infer a same-named field_type. A fields list adds reusable fields when no per-field override is needed:

bounded_context "pet_management" { field_type "pet_id" { type = "UUID" } field_type "pet_name" { type = "String" } event "pet_added" { fields = [field_type.pet_id] field "pet_name" { } } }

The shorthand applies only to field blocks. A field_type must still declare an explicit built-in type.

Lists and structured data

A custom reusable field type can carry nested subfields:

bounded_context "owner_management" { field_type "owner_pets" { type = "Custom" cardinality = "List" example = [{ id = 5 name = "Mochi" birthDate = "2020-01-12" }] subfield "id" { type = "Int" id_attribute = true } subfield "name" { type = "String" } } }

Examples are native HCL literals, and the validator checks that examples match the effective type.

One edge, one canonical declaration

Each edge has one canonical spelling.

Rule 1: workflow-local source. Write to on the source.

bounded_context "pet_management" { event "pet_added" { } } state_change "add_pet" { screen "add_pet_form" { to = [command.add_pet] } command "add_pet" { to = [event.pet_management.pet_added] } }

Rule 2: catalog Event source. Write from on the receiving workflow element.

Here is what happens when a Command reverses the first rule and puts from on itself instead of to on the Screen:

bounded_context "example" { event "pet_created" { } } state_change "create_pet" { screen "form" { to = [command.create] } command "create" { from = [screen.form] to = [event.example.pet_created] } }

The validator rejects the reverse form:

reverse-flow.em.hcl:11:13: Error EM201: Invalid flow reference: command.from has no canonical flow targets in a state_change workflow. You referenced: screen.form.

Quick check: who is the source? Use to on a workflow source. Use from on the receiver when the source is a catalog Event.