Learn · 03 of 5

The language

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

The model

A model contains domain catalogs (lists of shared definitions), workshop notation, and workflows. Catalogs define contexts, actors, ownership, Events, aggregates, and field types. Workflows describe behavior. Chapters and hotspots keep workshop context.

Specification v0.4.0 defines a model as one .em.hcl file or a folder of member files. A folder includes only direct .em.hcl files whose names do not start with a dot. It does not include subfolders.

Each file holds whole top-level blocks and must parse on its own. References resolve across member files without changing ID scopes. Each bounded context and its domain contracts stay in one file. See Folder models.

After validation, downstream tools use a normalized typed model that has 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 and formats one file at a time. A one-file model uses source order, so keep workflows in business-time order.

With two or more member files, model order is bytewise file-name order, then source order. Chapters must stay in one file. Chapter order, then each chapter's workflows list, sets workflow order. Workflows outside chapters follow in model order and produce EM407. A folder with one member file keeps the one-file rules.

Typed references

Quoted values are data. Unquoted traversals are references. For example, to = [event.pet_management.pet_added] resolves a relationship. If you quote that traversal, it becomes 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, for example aggregate.pet and field_type.pet_id. Outside the context, use the qualified form: aggregate.pet_management.pet, field_type.pet_management.pet_id, event.pet_management.pet_added.

Aggregates

An aggregate is a consistency boundary that a context owns:

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 business term appears in several 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 } } }

For a one-off field, write a built-in type inline, for example field "request_id" { type = "UUID" }. The reference lists the available types and flags.

Field shorthand

A plain field can infer the field_type with the same name. A fields list adds reusable fields if you need no per-field override:

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. 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 if a Command reverses the first rule. The Command 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.

To check an edge, find the source. Use to on a workflow source. Use from on the receiver if the source is a catalog Event.