Model file format
There is one JSON file per model, at models/<model-id>/model.json. The UI, the CLI and Claude all edit this file, and the UI reloads live when it changes on disk. The types are defined in src/core/types.ts, which is the source of truth for this document.
Identity is by name, never by numeric id. Entities, relationships and hierarchies are referenced by their name.
Top level
{
"format": "entitymodeler",
"format_version": 1,
"source": "https://entitymodel.com", // where the file was made; written automatically
"name": "acme_orders", // internal slug
"title": "Acme Orders",
"organization": "Acme Corp",
"description": "Scope and purpose…",
"version": "0.1",
"author": "",
"status": "draft", // draft | in review | approved
"created_at": "ISO-8601", "updated_at": "ISO-8601",
"entities": [ /* Entity */ ],
"relationships": [ /* Relationship */ ],
"hierarchies": [ /* Hierarchy */ ],
"areas": [ /* subject-area rectangles on the canvas */ ],
"notes": [ /* sticky notes on the canvas */ ],
"tab": { "name": "Physical", "kind": "physical", "dialect": "postgres" }, // optional: the primary tab's name and kind
"tabs": [ /* further diagram tabs, see Tabs below */ ]
}
Entity
{
"name": "customer", // ^[a-z][a-z0-9_]*$ — internal name
"display_name": "Customer",
"entity_type": "standard", // standard | reference
"description": "…",
"color_code": "#2563EB",
"area": "Sales", // optional subject-area name
"attributes": [ /* Attribute */ ],
"struct_types": [ /* StructType */ ],
"dependent_reference": null, // reference only: { "parent": "country", "linked_column": "country_code" }
"reference_values": [ // reference only: the list's values, keyed by attribute name
{ "country_code": "US", "country_name": "United States", "iso3": "USA" }
], // dependent children also carry the linked column, e.g. { "country_code": "US", "state_code": "CA", ... }
"reference_values_source": "ISO 3166-1",
"notes": "design notes, not shown in the docs",
"derived_from": "tbl_customers", // set by tab derivation: the entity's name in the source tab
"ui": { "x": 120, "y": 80 } // canvas position; omit it and the UI lays the entity out.
// "hidden": true keeps it in the model but off the diagram
// "folded": ["billing_address"] collapses those STRUCT attributes' fields
// into a single row on the diagram (docs and exports still list them)
}
- standard entities are the business things the model is about: Customer, Order, Product, Location.
- reference entities are controlled code lists that other entities point at: country, currency, status, type codes.
Attribute
{ "name": "customer_id", "label": "Customer ID", "type": "STRING",
"is_primary_key": true, "is_label": false, "is_array": false,
"description": "",
// REFERENCE_ENTITY only:
"reference": { "entity": "country", "output_attribute": "country_name" },
// STRUCT only:
"struct_type": "address",
// documentation metadata:
"required": true, "pii": false, "example": "CUS-0001", "notes": "" }
- Types:
STRING BIGINT INT SMALLINT TINYINT DECIMAL DOUBLE FLOAT BOOLEAN DATE TIMESTAMP STRUCT REFERENCE_ENTITY. DECIMALtakesprecisionandscale(default 18, 2). Use it for money and other exact numbers.unique: truemarks values unique across rows. It becomes a UNIQUE constraint in SQL and auniquetest in dbt.is_labelmarks the primary display attribute. Each entity has one.- STRUCT and array attributes cannot be primary keys.
output_attributeon a reference is the attribute of the reference entity whose value is stored on the referencing record.exampleappears in the UI, on hover, and in the Example column of the attribute dictionary. A reference attribute with no example shows the first values of its reference list instead.
Reference values
A reference entity can carry its full list in reference_values: one object per value, keyed by attribute name, with every value a string.
The values are used for:
- hover hints on the attributes that reference the list
- the Reference values doc tables
- CSV download, with attribute names as the headers
Validation checks for blank or duplicate keys. It also checks that every dependent child's linked-column value exists in the parent's values; that match is exact and case-sensitive.
Load values from a file with node bin/em.mjs values <id> <entity> <file.csv>.
Dependent reference
dependent_reference constrains a reference list to a parent list, as in Country → State → City.
parentis the parent reference entity.linked_columnis the parent attribute that each child value carries, usually the parent's primary key.- The linked column is added to the child automatically, so it must not also be declared as an attribute on the child.
StructType
{ "name": "address", "description": "",
"fields": [ { "name": "line1", "type": "STRING" },
{ "name": "country_code", "type": "REFERENCE_ENTITY", "reference": { "entity": "country", "output_attribute": "country_code" } } ] }
Fields are scalars or REFERENCE_ENTITY only; structs cannot nest. For repeating groups, set is_array: true on the STRUCT attribute.
Relationship
{ "name": "placed_by", // ^[a-z][a-z0-9_]*$
"source": "order", "target": "customer",
"cardinality": "N:1", // 1:1 | 1:N | N:1 | N:N (1:N = each source has many targets)
"forward_label": "Placed By", "reverse_label": "Placed", "bidirectional": true,
"description": "",
"foreign_key": "customer_id", // optional: child attribute holding the parent's key (see below)
"attributes": [ { "name": "channel", "label": "Channel", "type": "STRING" } ] }
foreign_key names the attribute on the child side that holds the parent's primary key. SQL and DBML export use it.
- N:1 and 1:1: the child is the source.
- 1:N: the child is the target.
- N:N: there is no foreign key; export writes a junction table named after the relationship.
When foreign_key is unset, it is inferred from the names: the parent's PK name, then <parent>_<pk>, then <parent>_id, and for self-relationships parent_<pk>. If nothing matches, the export adds a key column. Set it explicitly when the column name is unusual, e.g. parent_supplier_id.
Hierarchy
{ "name": "customer_ownership", "display_name": "Customer Ownership", "root": "customer",
"status": "draft", "max_depth": 6, "allow_multi_parent": false,
"levels": [
{ "level": 2, "branches": [ { "name": "Subsidiaries", "entity": "customer", "relationship": "subsidiary_of", "direction": "forward", "parent_branch": "Root" } ] } ] }
- Level 1 is the root, and its branch name is
Root. - Levels run contiguously from 2.
forwardmeans the relationship's source is the child.
Canvas extras
"areas": [ { "id": "area_x1", "name": "Sales", "color": "#3B82F6", "x": 0, "y": 0, "w": 900, "h": 600, "description": "" } ],
"notes": [ { "id": "note_x1", "text": "Open question: …", "x": 0, "y": 700, "w": 220, "color": "#FEF3C7" } ]
Tabs
A model can hold several diagrams, shown as tabs: a conceptual view, a logical model, one physical model per platform, versions and what-if variants. The top-level entities, relationships, hierarchies, areas and notes are the primary tab; tab names it (default { "name": "Main", "kind": "logical" }). Every other tab is an entry in tabs with the same five arrays plus its own metadata:
"tabs": [
{ "id": "logical", // stable id; [a-z0-9_]
"name": "Logical", // unique across tabs (case-insensitive)
"kind": "logical", // conceptual | logical | physical | variant
"description": "",
"dialect": "snowflake", // physical tabs: target platform (default SQL dialect)
"derived_from": "main", // id of the tab this one was derived or copied from
"derived_at": "ISO-8601",
"source": { "platform": "databricks", "catalog": "main", "schema": "sales", "pulled_at": "ISO-8601" }, // tabs from `em pull`
"entities": [], "relationships": [], "hierarchies": [], "areas": [], "notes": [] }
]
- Tabs are independent: the same entity name can exist in several tabs with different attributes and positions. References and relationships only point within their own tab.
- Each tab is validated on its own. Conceptual tabs may have entities without attributes.
derived_fromon a tab, and on its entities and attributes, is lineage:em diffandem migrate(and Compare in the UI) use it to match renamed things, e.g.tbl_cust.cust_nm↔customer.customer_name.- Make derived tabs with
node bin/em.mjs derive <id> --kind logical|physical|conceptual --from <tab>rather than by hand. - Single-tab tools (docs, export, values, import) act on the primary tab unless given
--tab.
Mappings
mappings records where each target attribute's data comes from, across tabs (an ETL spec):
"mappings": [
{ "id": "map_x1", "target": { "tab": "logical", "entity": "customer", "attribute": "customer_name" },
"sources": [ { "tab": "main", "entity": "tbl_customers", "attribute": "cust_nm" } ],
"transform": "initcap(src.cust_nm)", "status": "draft", "notes": "" } ]
tabis a tab id (mainfor the primary tab).sourcescan be empty (a constant) or list several columns. An emptytransformmeans a direct copy.- Entity and attribute renames update mappings automatically; deleting a target removes its mapping.
- Validation warns about ends that no longer exist (
map.dangling) and attributes mapped twice.
Glossary terms and comments
"glossary_snapshot": [ { "id": "term_x1", "name": "Customer", "definition": "…", "synonyms": ["Client"], "status": "approved", "updated_at": "ISO-8601" } ],
"comments": [ { "id": "c_x1", "author": "Ana", "text": "Is this the legal name?", "created_at": "ISO-8601", "resolved": false,
"target": { "tab": "main", "kind": "entity", "name": "customer", "attribute": "customer_name" },
"replies": [ { "id": "r_x1", "author": "Claude", "text": "Yes; renamed.", "created_at": "ISO-8601" } ] } ]
termon an entity or attribute holds a term id.- The shared glossary lives in
models/_glossary.json(em glossary).glossary_snapshotcarries the terms the model uses, so the file stands alone. - Comment
target.kindismodel,entityorrelationship. Renames keep comments attached.
Naming standards
standards holds optional rules per tab kind. Validation checks them, and Issues offers fixes for the mechanical ones.
"standards": {
"logical": { "attribute_case": "snake", "entity_number": "singular", "abbreviations": "expand", "pk_suffix": "_id",
"require_entity_description": true, "require_attribute_description": true, "require_pii_review": true },
"physical": { "attribute_case": "upper_snake", "max_length": 255, "reference_prefix": "ref_" }
}
- Fields:
attribute_case(snake,camel,upper_snake),entity_number(singular,plural),entity_prefix,reference_prefix,pk_suffix,abbreviations(expand),max_length,require_entity_description,require_attribute_description,require_pii_review. - A kind with no entry is not checked.
Older files
normalizeModel (in src/core/model.ts) migrates files from earlier versions of the format when they load:
clientbecomesorganization- any non-reference
entity_typebecomesstandard - fields the model no longer carries are dropped
The cleaned file is written back on the next save, or immediately with node bin/em.mjs fmt <id>.