4. Layer 1 — Metamodel
Contents of DISL 0.8
The metamodel defines the abstract syntax of the language: the kinds of elements that can exist, their attributes, how they nest, how they connect, and which ports they expose. It says nothing about appearance.
{
"metamodel": {
"diagram": { "attributes": { "title": { "type": "string" } } },
"dataTypes": { "Email": { "base": "string", "format": "email" } },
"enums": { "Priority": { "values": { "low": {}, "normal": {}, "high": {} }, "ordered": true } },
"types": { "Task": { "attributes": { "title": { "type": "string", "required": true } } } },
"relations": { "DependsOn": { "source": "Task", "target": "Task" } }
}
}
| Property | Type | Description |
|---|---|---|
diagram |
{attributes, doc} |
Attributes of the diagram root element (title, author, version, settings). |
dataTypes |
map → DataType | Named value types with facets, or structs (4.4). |
enums |
map → Enum | Enumerations (4.5). |
types |
map → NodeType | Node types — everything that is drawn as a node, including containers, lanes and annotations (4.6). |
relations |
map → RelationType | Relation types — everything that is drawn as an edge (4.9). |
doc |
Doc | Documentation of the metamodel. |
| Type | CEL type | JSON representation | Facets |
|---|---|---|---|
string |
string |
string | minLength, maxLength, pattern, format |
text |
string |
string (multi-line) | as string, plus markup: "plain" | "markdown" |
int |
int |
integer | min, max, step, unit |
number |
double |
number | min, max, exclusiveMin, exclusiveMax, step, precision, unit |
bool |
bool |
boolean | – |
date |
timestamp |
"YYYY-MM-DD" |
min, max (as dates) |
datetime |
timestamp |
RFC 3339 string | min, max, timezone (stored zone policy: "utc", "preserve", IANA name), writtenPrecision, newPrecision (below) |
yearMonth |
int (month index) |
"±YYYY-MM" |
min, max (as yearMonth strings) |
time |
duration since midnight |
"HH:MM[:SS]" |
min, max, step |
duration |
duration |
ISO 8601 duration | min, max, calendar (allow P1M/P1Y) |
color |
string |
CSS color string | palette (list of allowed colors), alpha (bool) |
uri |
string |
URI string | schemes (allowed schemes), paths (DISL 0.4: a path is accepted too, below) |
expression |
string |
CEL source | context (name of the CEL context it will be evaluated in), resultType |
json |
dyn |
any JSON | schema (inline JSON Schema) |
binary |
bytes |
base64 string | mediaTypes, maxBytes |
Paths in a uri (DISL 0.4). With paths: true, a uri value MAY also be a path: an absolute path of the platform it was written on (C:/git/repo, /home/user/repo), or a relative path, resolved against the folder of the file the value is stored in. A value is a URI when it starts with a scheme followed by : and is not a drive letter (C:), and a path otherwise; schemes restricts URIs only. A value that is a URI with a scheme outside schemes MUST be kept as written and reported by std.facets (8.7). Without paths, a uri is a URI, as in 0.3.
"link": { "type": "uri", "schemes": ["http", "https"], "paths": true }
A task's link may be https://github.com/org/repo/pull/150, docs/plan.md beside the diagram's file, or C:/git/repo/.claude/worktrees/kd; ftp://host/file is kept and reported.
Unit facets (unit) are informational strings ("h", "kg", "EUR"); runtimes SHOULD show them as field suffixes.
Year and month. A yearMonth value is a month without a day, in astronomical year numbering: year 0000 is 1 BCE and -3200 is 3201 BCE. Its JSON form is an ISO 8601 expanded year of four to six digits, with a leading - for years before 0000, a hyphen and a two-digit month ("2026-09", "-3200-01"). In CEL it is an int, the month index year × 12 + (month − 1), so "0000-01" is 0 and "-0001-12" is −1; arithmetic on it is integer arithmetic, and yearMonth(), formatYearMonth() and parseYearMonth() (12.4) convert. A timestamp cannot hold these values, because CEL timestamps span the years 0001 to 9999. Writers MUST write at least four year digits and a - for negative years (DID, section 3).
Written precision. Two facets of datetime attributes govern the precision a value is written with:
| Facet | Values | Default | Description |
|---|---|---|---|
writtenPrecision |
"preserve", "date", "minute", "second", "millisecond" |
none (0.1: the writer's precision) | The precision values are written with. preserve keeps, per value, the precision it was read with. |
newPrecision |
"date", "minute", "second", "millisecond" |
"second" |
The precision of values the tool creates. |
With writtenPrecision: "preserve", a runtime MUST record for each stored value the precision it was read with (date when it has no time part, otherwise the finest written field that is not zero) and MUST write a changed value in that precision, rounding half away from zero; a date-only value is the start of that day. precisionOf(self, attr) (12.4) reads the recorded precision, so a snap rule can depend on it, and samePrecisionAs (4.3) with std.mixedPrecision (8.7) keeps two attributes of one element alike.
"begin": { "type": "datetime", "required": true, "timezone": "floating", "writtenPrecision": "preserve", "newPrecision": "date" }
A timeline bar read as 2026-01-05 is written back as 2026-01-05 after a drag, and one read as 2026-01-05T09:30 keeps its minutes.
An attribute describes one named value on a node, relation, port or the diagram.
| Property | Type | Description |
|---|---|---|
type |
TypeName | Primitive, enum, data type, or node/relation type (a reference, 4.8). Required. |
label |
LocalizedText | Display name. |
doc |
Doc | Shown as field help. |
required |
bool | Must have a non-empty value. Enforced as a built-in constraint with severity error (section 8.7). Default false. |
default |
literal or {cel} |
Initial value on creation. A CEL default is evaluated in the create context (12.3). |
many |
bool | The attribute holds a list. Default false. |
minItems, maxItems, uniqueItems |
int, int, bool | List facets when many. |
readOnly |
bool | Not editable by users (may still be set by behavior). |
readOnlyReasons |
Reason[] | Why the attribute cannot be edited, in priority order; context element. While one applies the attribute is read-only for users (below). |
absentText |
Message | Shown instead of a value when the attribute is absent (below); context element. |
emptyText |
Message | Shown instead of a value when the attribute is empty (below); context element. |
derived |
Expression | Computed, never stored; evaluated in the element context. Implies readOnly. |
fixed |
literal | The attribute always has this value: read-only, never persisted (below). Exclusive with derived and default. |
transient |
bool or "viewer" |
true: editable but not persisted (for example UI-only flags). "viewer": held per viewer (below). |
outOfRange |
OutOfRange | Whether a value past min or max is clamped or refused, per kind of edit (below). |
unique |
"diagram", "parent", "type" |
Value must be unique within the scope. Enforced as a built-in constraint. |
samePrecisionAs |
attribute name | The value must be written with the same precision as the named attribute of the same element (both dates, or both with a time); checked by std.mixedPrecision (8.7). |
key |
bool | Part of the element's natural key (used for natural ids, 11.5, and for display). |
secret |
bool | Masked in forms and excluded from exports and logs. |
facets |
object | Primitive facets from 4.2 (min, max, pattern, …); may also be written directly on the attribute. |
group |
string | Default form section when a form is generated. |
order |
int | Default position in generated forms. |
x-* |
any | Extensions. |
{
"attributes": {
"name": { "type": "string", "required": true, "maxLength": 80, "unique": "parent",
"doc": "Unique within the enclosing region." },
"priority": { "type": "Priority", "default": "normal" },
"effort": { "type": "number", "min": 0, "step": 0.5, "unit": "h" },
"tags": { "type": "string", "many": true, "uniqueItems": true },
"slug": { "type": "string", "derived": "self.name.lowerAscii().replace(' ', '-')" },
"owner": { "type": "Person", "doc": "Reference to a person node in the same diagram." },
"createdAt": { "type": "datetime", "readOnly": true, "default": { "cel": "env.now" } }
}
}
Read-only reasons. A user cannot edit an attribute when it is readOnly or derived, when it is bound to a computed placement, or when a reason of its readOnlyReasons or of behavior.editGate (9.1) applies. The reason a runtime shows is the first applicable one in this order: the form field's readOnlyReasons (7.5), the attribute's readOnlyReasons, then the edit gate; a {reason} entry that names a gate reason places that reason where it is listed instead of last. A runtime at level Standard or above MUST show the applicable reason with the field (beside it, or as its explanation), and MUST refuse a write to a read-only attribute by a user with the same reason, whatever the write's origin (a form, an inline label edit, a paste, a plugin). When an attribute is read-only and no reason applies, the runtime SHOULD show its doc.summary.
Absent and empty. An attribute is absent for an element when it has no stored value and no default (DID omits it); it is empty when its stored value is "", [] or {}. A runtime MUST show absentText for an absent value and emptyText for an empty one, and MUST NOT show the two the same way unless neither is set. In CEL, absence stays !has(self.attr). A validator SHOULD warn about absentText on an attribute that has a default, which is never absent.
"description": { "type": "text", "readOnly": true,
"readOnlyReasons": [ "Published by the package's author and read from the local NuGet cache; nothing in this workspace defines it." ],
"absentText": "Not available - this package is not in the local NuGet cache" }
Viewer attributes. An attribute with transient: true keeps its 0.1 meaning: editable, not persisted, and with undo behaviour left to the runtime. An attribute with transient: "viewer" is viewer state (11.6): its value is held per viewer and starts at default each time a viewer opens the view; it MUST NOT be persisted, MUST NOT enter the undo history, MUST be settable in read-only mode, and MUST NOT be read in the deterministic contexts of 12.5, where a validator MUST reject such a read. A set action on it is a viewer-state change, with the rules of the view action (9.4).
"showAmbientPackages": { "type": "bool", "default": false, "transient": "viewer", "label": "Show ambient packages" }
Fixed attributes. An attribute with fixed always has that value, which MUST be a valid value of its type. It is read-only, and it is never persisted: a writer (DID or a format binding) MUST NOT write it, and a reader that finds a stored value MUST ignore it and SHOULD report a warning when it differs from the fixed value. In CEL, self.attr reads the fixed value and has(self.attr) is false. A subtype MAY narrow an inherited attribute to a fixed one (4.7).
"FdgElement": { "abstract": true, "attributes": { "height": { "type": "number", "min": 1 } } },
"UiElement": { "extends": "FdgElement", "attributes": { "height": { "type": "number", "fixed": 48 } } }
A functional decomposition graph stores a height only for comments; every other element is 48 high and writes nothing.
Out of range. min and max of an attribute MAY be a {cel} value evaluated in the element context (a timeline bar's end bounded by its begin: "min": { "cel": "self.begin" }). OutOfRange ($defs/OutOfRange) chooses what happens to an edit that would put the value past them:
| Property | Values | Default | Description |
|---|---|---|---|
drag |
"clamp", "refuse" |
"refuse" |
A gesture (move, resize, handle drag) that would put the value past the bound. |
typed |
"refuse", "clamp" |
"refuse" |
A value entered in a form, an inline label or pasted. |
message |
Message | the runtime's std.facets or std.axisBounds sentence |
The refusal; context gesture:change (self, attribute, oldValue, newValue) plus min and max. |
- With
clamp, a runtime MUST stop the value at the bound and MUST NOT refuse the edit. The bound MUST be applied after snapping: when the snapped value lies past the bound, the bound wins, even when it is not on the snap grid. - With
refuse, a runtime MUST refuse the edit withmessage, for a drag before it is released (8.4). std.facetsandstd.axisBounds(8.7) keep their severity and enforcement;outOfRangeonly chooses between clamping and refusing a prevented edit. The defaults are those of 0.1.
"maturity": { "type": "Fraction", "required": true, "step": 0.01,
"outOfRange": { "drag": "clamp", "typed": "refuse",
"message": { "cel": "string(newValue) + ' is off the map; maturity runs from 0 to 1.'" } } }
Neighbour-aware bounds (DISL 0.3). min and max bound one attribute at a time. Ordered attributes that must stay a distance apart, such as the boundaries between a trend's phases, are bounded together by the type-level property bounds.neighbour of a node type (4.6) or relation type (4.9), beside attributes. NeighbourBounds ($defs/NeighbourBounds):
| Property | Type | Default | Description |
|---|---|---|---|
attributes |
attribute name[] (at least one) | required | The ordered attributes kept apart, in their order. |
between |
[attribute name, attribute name] | required | The two attributes every value of attributes lies between, the lower first. |
gap |
number or {cel} |
required | The least distance between two neighbouring anchors, in the attributes' units (months for yearMonth); {cel} in the element context. |
inner |
int or {cel} |
all of them | How many of attributes, from the first, take part as anchors; the others are only clamped inside between. |
absent |
"spread", "ignore" |
"ignore" |
spread: an absent attribute among the anchors takes part at its evenly spread position between its present neighbours; ignore: it does not take part. |
on |
("change", "handle", "create")[] |
all three | The edits after which the clamp runs: a typed or programmatic set, a shape handle drag (6.8), a creation. |
pinned |
"edited", "none" |
"none" |
edited: the attribute the transaction set is clamped first, and the others around it. |
doc |
Doc |
- After any transaction of a kind
onlists that changes one ofattributesor either attribute ofbetween, a runtime MUST clamp the present values ofattributes, in order, so that each lies at leastgapafter the previous anchor and at leastgap(times the number of anchors still to come) before the second attribute ofbetween, where an anchor is a present value or, withabsent: "spread", the evenly spread position of an absent one. The first attribute ofbetweenis the anchor before the first. - Only the first
innerattributes take part as anchors; the others are only clamped insidebetween. - With
pinned: "edited", the attribute the transaction set is clamped first and the others around it. - The clamp is part of the transaction and MUST NOT be a separate undo step. When
betweenleaves too little room for every anchorgapapart, no clamp satisfies the bounds, and the transaction MUST be refused (8.4) rather than clamped. - The clamp applies after snapping and after
outOfRange(above), so it can move a value off the snap grid only wheregapitself is off it.
"Trend": {
"attributes": { "start": { "type": "yearMonth" }, "stop": { "type": "yearMonth" }, "phases": { "type": "int" },
"peakEnd": { "type": "yearMonth" }, "troughEnd": { "type": "yearMonth" }, "slopeEnd": { "type": "yearMonth" } },
"bounds": { "neighbour": { "attributes": ["peakEnd", "troughEnd", "slopeEnd"], "between": ["start", "stop"],
"gap": 1, "inner": { "cel": "visiblePhases(self) - 1" }, "absent": "spread", "on": ["change", "handle"], "pinned": "edited" } } }
A trend from 2000-01 to 2000-05 with four phases keeps every phase at least a month long: dragging the Trough boundary to 2000-02 lands it at 2000-03, because Peak needs a month before it; a later resize of the trend to 2000-01..2000-04 is refused (too short) rather than clamped.
Unset values. Reading an attribute that has no stored value yields its default when one is declared, otherwise the zero value of its CEL type ('', 0, 0.0, false, [], {}, null for references, the Unix epoch for timestamps, 0s for durations). The CEL macro has(self.attr) tests whether a value is explicitly stored. Persistence omits unset values (11.8).
A data type is either a constrained primitive or a struct.
{
"dataTypes": {
"Percentage": { "base": "number", "min": 0, "max": 100, "unit": "%", "doc": "0–100 %" },
"Iso4217": { "base": "string", "pattern": "^[A-Z]{3}$", "doc": "Currency code" },
"Money": {
"fields": {
"amount": { "type": "number", "required": true },
"currency": { "type": "Iso4217", "default": "EUR" }
},
"display": { "cel": "string(value.amount) + ' ' + value.currency" }
}
}
}
| Property | Type | Description |
|---|---|---|
base |
primitive | For constrained primitives. |
| facets | Facets of the base primitive. | |
fields |
map → Attribute | For structs; fields may themselves be structs, lists or references. |
display |
Bindable<string> | How a value is summarised in labels and list cells (value is the struct). |
label, doc |
Struct values are CEL maps; fields are accessed as self.budget.amount.
{
"enums": {
"Priority": {
"ordered": true,
"values": {
"low": { "label": "Low", "color": "#639922", "icon": "arrow-down" },
"normal": { "label": "Normal" },
"high": { "label": "High", "color": "#E24B4A", "icon": "arrow-up",
"doc": "Must be scheduled in the current iteration." }
}
}
}
}
| Property | Type | Description |
|---|---|---|
values |
map → EnumValue (ordered) | Value ids are simple identifiers and are what is stored. Map order is the display order. |
ordered |
bool | Values have a meaningful order; enables <, > comparison via ordinal(value, 'Priority'). |
extensible |
bool | Users may enter values not listed (combobox). |
label, doc |
EnumValue: value, label, doc, color, icon, deprecated. The optional color and icon are hints that notations may use ({ "cel": "enumColor('Priority', self.priority)" }).
Colours from theme tokens (DISL 0.3). An EnumValue's color is a Color or a TokenRef ({ "token": … }, 6.2). A token is resolved in the current theme mode wherever the colour is used: a legend entry (6.13) for an enum value whose color is a token MUST resolve the token in the current theme mode, and enumColor() (12.4) returns the resolved colour.
"Phase": { "ordered": true, "values": {
"peak": { "label": "Peak", "color": { "token": "ghg.peak" } },
"trough": { "label": "Trough", "color": { "token": "ghg.trough" } } } }
With ghg.peak declared as #f7e7a1 and, in the dark mode, #6b5a17, the hype cycle's legend paints the Peak swatch light or dark with the theme.
Stored form. An enum value's key is its name in the specification and in CEL; value (a non-empty string, default: the key) is its stored form, how it is written in a DID definition and the spelling a format binding maps it to by default. Stored forms MUST be unique within an enumeration. A reader MUST map a stored form back to its key; an unknown stored form is kept as written for an extensible enum and reported by std.facets otherwise. CEL always sees the key, so a stored form need not be an identifier:
"TaskType": { "values": {
"notebook": { "label": "Notebook" },
"run_job": { "label": "Run job", "value": "run-job" },
"for_each": { "label": "For each", "value": "for-each" } } }
A Databricks task of type run-job is read as run_job, compared in CEL as self.taskType == 'run_job', and written back as run-job.
A node type describes a kind of element that is drawn as a node.
| Property | Type | Description |
|---|---|---|
label, doc, icon |
Display name, documentation, default icon. | |
abstract |
bool | Cannot be instantiated; exists for inheritance and type references. |
extends |
TypeRef or TypeRef[] | Supertypes (4.7). |
attributes |
map → Attribute | Own attributes. |
bounds |
{neighbour} |
Bounds that involve several attributes at once: neighbour keeps ordered attributes a gap apart (4.3). (DISL 0.3) |
ports |
map → PortType | Connection points (4.10). |
children |
Containment | What may be nested inside (4.8). |
multiplicity |
{min, max} |
Number of instances allowed per diagram (for example exactly one Start). Enforced as a built-in constraint. |
derived |
DerivedNode | The type's elements are computed from the model and never stored (4.11). |
viewOnly |
bool | The element has no model meaning and lives only in a view: notes, free text, frames, images, annotations. View-only elements are stored in view data (11.6) and are excluded from diagram.nodes unless includeViewOnly is used (12.4). |
labelAttribute |
attribute name | The attribute used as the element's name in lists, finding messages, reference pickers and default labels. Default: the first key attribute, else the first required string attribute, else the first string attribute. |
tags |
string[] | Free classification, usable in CEL as self.isTagged('x'). |
x-* |
Extensions. |
A type inherits all attributes, ports, containment rules and tags from its supertypes, and is accepted wherever a supertype is expected (in relation ends, containment, references, constraint scopes, notation lookups).
- Multiple inheritance is allowed. The set of supertypes MUST form a directed acyclic graph.
- A subtype MAY redeclare an inherited attribute to narrow it: tighten facets, change
default,labelordoc, setrequired: true, narrow a reference type to a subtype, or setfixed(a constant is the tightest facet;defaultis then dropped). It MUST NOT change the base type or relax facets. Conflicting inherited declarations of the same attribute from two supertypes are a specification error unless the subtype redeclares it. - Relation flags. A relation type that does not state
directed,allowSelfLoopsorallowParallelMUST be read with the value of the nearest type in its C3 linearisation that states it, and with the default of section 4.9 when none does; this is the value notation, forms and constraints use. A relation type MAY state a flag its supertypes have only to narrow it:allowSelfLoopsandallowParallelfromtruetofalse. It MUST NOT statetrueforallowSelfLoopsorallowParallelwhere a direct supertype's value isfalse, and MUST NOT state adirectedthat differs from a direct supertype's; a supertype's value is its value by the lookup above, the default included, sodirected: falseunder a supertype that states nothing differs from itstrue. A relation type whose direct supertypes have different values of a flag by that lookup MUST state that flag itself, as for conflicting inherited attributes. A relation type that breaks either rule is a specification error. (informative)abstractandderivedmake no difference to the lookup or to these rules, and a supertype that states a flag with its default value changes nothing. - Notation, forms and constraints are looked up along the linearised supertype chain (C3 linearisation, as in Python): the most specific declaration wins, and a node type without its own notation uses its nearest supertype's notation.
- Relation types inherit from relation types only; node types from node types only.
Containment makes nodes nest: a pool contains lanes, a lane contains tasks, a table contains columns. A contained node has exactly one parent; deleting the parent deletes its contents (unless behavior says otherwise, 9.5).
{
"Lane": {
"attributes": { "name": { "type": "string" } },
"children": {
"allowed": ["Task", "Event", "Gateway"],
"min": 0,
"max": null,
"ordered": true,
"slots": {
"header": { "allowed": ["LaneHeader"], "max": 1 },
"content": { "allowed": ["Task", "Event", "Gateway"] }
}
}
}
}
| Property | Type | Description |
|---|---|---|
allowed |
TypeRef[] | Types that may be direct children. |
min, max |
int / null | Child count bounds (null = unbounded). |
perType |
map TypeRef → {min, max} |
Per-type bounds. |
ordered |
bool | Child order is meaningful and persisted (for example stacked compartments, table columns). |
slots |
map → {allowed, min, max, doc} |
Named sub-containers when a node has several distinct regions; each child records its slot. Notation maps slots to compartments or regions. |
acrossViews |
bool | Whether a child may be shown outside its parent in other viewpoints. Default false. |
A node type without children cannot contain other nodes. Top-level nodes have the diagram as parent (parent == null in CEL, self.owner == diagram).
References are attributes whose type is a node or relation type. They store the target's id, are resolved to elements in CEL, and are not containment: deleting the target sets the reference to null (or deletes the referencing element when behavior declares onDelete: cascade-references, 9.5). References are drawn as edges only if a notation says so (6.10, edges for reference attributes).
A relation type describes a kind of connection, drawn as an edge.
{
"relations": {
"Transition": {
"doc": "A change from one state to another, fired by a trigger.",
"source": { "types": ["State"], "max": null },
"target": { "types": ["State"], "exclude": ["InitialState"] },
"directed": true,
"allowSelfLoops": true,
"allowParallel": true,
"attributes": {
"trigger": { "type": "string" },
"guard": { "type": "expression", "context": "x-guard" }
}
}
}
}
| Property | Type | Description |
|---|---|---|
label, doc, icon |
||
abstract, extends |
As for node types. | |
source, target |
TypeRef, TypeRef[] or RelationEnd | Allowed endpoints. Required. |
directed |
bool | Default true. Inherited (4.7). Undirected relations treat source/target as unordered for allowParallel and cycle checks. |
allowSelfLoops |
bool | Source and target may be the same element. Default false. Inherited (4.7). |
allowParallel |
bool | More than one relation of this type between the same pair. Default true. Inherited (4.7). |
acyclic |
bool | Shorthand for a built-in constraint (std.acyclic) forbidding cycles in the graph formed by all relations whose type is this type or one of its subtypes (below). |
attributes |
map → Attribute | Relation attributes. |
bounds |
{neighbour} |
As for node types (4.3). (DISL 0.3) |
connectsRelations |
bool | Endpoints may be relations, not only nodes (for example UML association classes, annotations pointing at edges). Default false. |
derived |
Expression or DerivedRelation | Derived relations are computed and drawn read-only: an Expression returning a list of maps (below), or the object form of 4.11.3. |
viewOnly |
bool | As for node types — connectors between notes, for instance. |
RelationEnd
| Property | Type | Description |
|---|---|---|
types |
TypeRef[] | Allowed element types. |
exclude |
TypeRef[] | Subtypes excluded from types. |
ports |
string[] | Port types the end must attach to (4.10). If set, the end MUST be a port of that type. |
min, max |
int / null | How many relations of this type an element may have at this end (outgoing for source, incoming for target). |
role |
string | Role name, usable in CEL and labels (for example "parent" / "child"). |
optional |
bool | On the target end only: a relation of this type MAY have no target. Default false. |
doc |
Doc |
Connectivity rules that depend on attributes or on the pair of endpoints are written as connect constraints (8.4).
Flags over subtypes. A relation type inherits directed, allowSelfLoops and allowParallel from its supertypes, and may only narrow them (4.7). acyclic forbids cycles in the graph formed by all relations whose type is this type or one of its subtypes; on an abstract relation type it therefore covers any mix of its subtypes. A subtype of an acyclic type is acyclic and MUST NOT set acyclic: false. This is the 0.1 meaning made explicit, since a subtype is accepted wherever its supertype is (4.7).
"Owns": { "abstract": true, "extends": "FdgRelation", "acyclic": true },
"UiChild": { "extends": "Owns", "source": "UiElement", "target": { "types": ["UiElement"], "max": 1 } },
"OwnsData": { "extends": "Owns", "source": ["UiElement", "Action", "DataElement"], "target": { "types": ["DataElement"], "max": 1 } }
In a functional decomposition graph, no mix of UiChild, OwnsData and the other ownership relations may close a loop.
(informative) Which relations allowParallel counts is unchanged by its inheritance: they are relations of the same type between the same pair, so two relations of two different subtypes of one type on one pair are not parallel to each other, while two of one subtype that inherits allowParallel: false are.
Relations without a target. When the target end is optional, a relation of the type MAY be stored without a target; self.target is then null, the relation is drawn as its edge notation's stub (6.10), and std.endpoints (8.7) MUST NOT report the missing target. A connect gesture never creates a relation without a target; reconnecting the free end to an element is a connect gesture. DID stores such a relation without its target (DID, section 3).
"UsesRole": { "source": "Play", "target": { "types": ["Role"], "optional": true } }
An Ansible play that names a role the folder does not contain keeps its relation, drawn as a short stub labelled with the role as written.
Ends on derived nodes. (DISL 0.8) An end of a stored relation MAY resolve to a derived node (4.11.2). When the end as stored or read names no stored element, it MUST resolve to the first derived node, in computation order (4.11.5), of a type that end allows, whose id is the end as written, or the end as written under the prefix of the node type's natural id rule (11.5). self.source or self.target is then that node, the relation is drawn like any other (6.1), and neither std.references nor std.endpoints reports the end; a rule tells it from a stored end by derived (4.11.4, 12.2). The stored end does not change: the relation is written with the end as written, never with the derived node's id (4.11.5). An end that names neither a stored element nor such a derived node is what it was: unset, as this section and 11.2 say.
"UnresolvedTask": {
"extends": "Task",
"derived": {
"from": "diagram.relationsOfType('Dependency').filter(d, !has(d.source))",
"key": "item.upstreamKey",
"id": "'task:' + item.upstreamKey",
"attributes": { "key": "item.upstreamKey" }
}
}
A Databricks job whose task depends on a task nobody declares draws a placeholder for the key, under the id a task of that key would have, with an edge from it to each task waiting on it. The specifications beside this one show it in placeholder-tasks.dis.
Derived relations, expression form. The 0.1 form of derived is an Expression, in the derive context (12.3), returning a list of maps. Each map MUST have source and target (elements) and yields one relation. Any other key that names an attribute of the relation type gives that attribute's value, and the keys id, sources and owner have the meanings of the object form (4.11.3); other keys are a specification error where statically known and are otherwise ignored. When id is absent, the id MUST be <Type>:<source.id>-><target.id>, where <Type> is the relation type's name, with #2, #3, … appended to the second and later maps that give the same id, in list order. The 0.1 meaning of the form is unchanged: this only fills what 0.1 left open (Changes from 0.1). The object form of 4.11.3 adds merging, provenance, ownership and edits.
Ports are named connection points on nodes: pins of a circuit component, input/output handles of a data-flow block, the sockets of a UML component.
{
"Block": {
"attributes": { "name": { "type": "string" } },
"ports": {
"in": { "direction": "in", "multiplicity": { "min": 1, "max": 8 }, "dynamic": true,
"attributes": { "name": { "type": "string" }, "dataType": { "type": "string" } } },
"out": { "direction": "out", "multiplicity": { "min": 1, "max": 1 } }
}
}
}
| Property | Type | Description |
|---|---|---|
label, doc |
||
direction |
"in", "out", "inout" |
Allowed edge direction at this port. Default "inout". |
multiplicity |
{min, max} |
Number of port instances of this type per node. Default {min: 1, max: 1} (one fixed port). |
dynamic |
bool | Users may add and remove instances (within multiplicity). |
maxConnections |
int / null | Edges per port instance. |
accepts |
TypeRef[] | Relation types that may attach. Default: all whose ends allow this node type. |
attributes |
map → Attribute | Attributes of each port instance. |
Port instances are elements with kind == "port", an id, a type of the form Block.in, and an owner (the node). They are persisted with their owner (11.4).
A derived element is an element the runtime computes from other elements of the model: it groups, merges, splits, lifts or filters them, or its ends or parent are themselves derived. It has no stored record of its own and is never written. A stored element is one that is read from storage: a record of a DID definition, or an element a format binding reads from one entry of a foreign file. A node type becomes derived with derived (4.6, a DerivedNode); a relation type with derived in the object form of 4.11.3, or in the 0.1 expression form (4.9).
FBL, the Format Binding Language, reads a foreign file into stored elements, one per entry of the file (a triple, a list entry, a statement, a project reference), each with its source location and with or without a rule to write it back. DISL computes everything else. The line between them is drawn by the entry: an element that corresponds to exactly one entry, and whose ends and parent are stored, is a stored element of the binding; an element that groups several entries (an RDF resource card is every triple sharing a subject), merges them (one SKOS hierarchy edge stated by both broader and narrower), splits one (an OWL IRI that is both a class and an individual), lifts or filters them for a view (C4 membership and lifted relationships), or whose ends or parent are derived (a SPARQL variable placed in its shallowest scope) is a DISL derived element.
So a format binding produces the fine-grained stored records and leaves the drawn projection to DISL. Stored records that are never drawn are ordinary node types that the viewpoint excludes (3.5): they stay in the model and are validated. Elements that the binding reads but cannot write back, such as unknown kinds or unresolved template references, are FBL's "not written back" elements; they are stored, not derived, and differ from DISL's fixed attributes (4.3), which are never written at all.
A derived element MUST NOT have a source entry of its own; where it needs a source location, it borrows that of its first source (4.11.4). An element that corresponds to exactly one source entry, and whose ends and parent are stored, SHOULD be a stored element produced by the format binding rather than a derived element.
DerivedNode ($defs/DerivedNode):
| Property | Type | Req. | Context | Description |
|---|---|---|---|---|
from |
Expression → list |
✓ | derive |
The items. One element is derived per item, or per distinct key. |
key |
Expression | – | deriveItem |
Items with equal keys (CEL ==) yield one element, with group bound to all of them. Default: every item is its own element. |
id |
Expression → string |
✓ | deriveItem |
The element's id. It MUST be unique among all elements of the diagram (4.11.5). |
attributes |
map attribute → Expression | – | deriveItem |
Values of the type's attributes. An attribute not listed has its default. |
parent |
Expression → Element? |
– | deriveItem |
Computed containment (below). Default null: top level. |
slot |
Expression → string |
– | deriveItem |
The slot in the parent (4.8). |
sources |
Expression → list(Element) |
– | deriveItem |
The provenance (4.11.4). Default: group when the items are elements, else []. |
reason |
Reason | – | element |
Why a gesture on the element is refused (4.11.4). Default: the runtime's sentence saying that the element is computed from the model and cannot be edited here. |
edits |
map gesture → operation id | – | – | Gestures carried out by an operation instead of being refused (4.11.4). Keys: delete, reparent, move, connect, rename, attribute:<name>. |
doc |
Doc | – | – |
The derive context binds diagram and env; the deriveItem context binds item (the first item of the group), group (all items with this key, in from order), index (the position of the group in from order), diagram and env (12.3). A key MUST be a string, number, bool or list of those; any other key is a specification error where it is statically known.
- A node type with
derivedMUST NOT be instantiated by a user, a tool, a template or an action.create,delete,retypeandreparentactions whose target is a derived element are a specification error where that is statically known, and a run-time error that rolls the transaction back otherwise. - Every attribute of a derived node type is read-only. An attribute with its own
derivedexpression (4.3) is evaluated on the derived element in theelementcontext, as for stored elements. - A derived node type MAY extend stored or abstract node types, and inherits their notation, forms and constraints (4.7). A stored type MUST NOT extend a derived one. A derived type MUST NOT declare attributes named
derivedorsources, which name its provenance in CEL (12.2). - One stored element MAY be a source of any number of derived elements, of one type or several; an IRI that is both an OWL class and an individual is two derived types over the same triples, whose ids differ by prefix.
Computed containment. The parent of a derived node is the value of parent. It MAY be a stored node or a derived node of a type computed earlier (4.11.5), and MUST be allowed by that type's children (4.8); otherwise the element is drawn at top level and one std.containment finding is reported. A stored node's parent is always stored: DISL does not compute containment for stored nodes.
"Triple": {
"doc": "One triple of the file, as the format binding reads it. Never drawn.",
"attributes": { "s": { "type": "string" }, "p": { "type": "uri" }, "o": { "type": "string" }, "oKind": { "type": "TermKind" } }
},
"Resource": {
"extends": "Term",
"attributes": { "iri": { "type": "uri" }, "types": { "type": "uri", "many": true } },
"derived": {
"from": "diagram.nodesOfType('Triple').map(t, t.oKind == 'iri' && t.p != RDF_TYPE ? [[t.s, t], [t.o, t]] : [[t.s, t]]).flatten()",
"key": "item[0]",
"id": "'res:' + item[0]",
"attributes": {
"iri": "item[0]",
"types": "group.map(g, g[1]).filter(t, t.s == item[0] && t.p == RDF_TYPE && t.oKind == 'iri').map(t, t.o)"
},
"sources": "group.map(g, g[1]).distinct()",
"reason": "Resources are read from the triples; add, rename or remove them through the context menu.",
"edits": { "delete": "removeResource", "rename": "renameResource" }
}
}
An RDF graph derives one resource card per IRI, from every triple that names it as subject or as the object of a non-type triple; the Triple records are excluded from the viewpoint ("exclude": ["Triple"]). RDF_TYPE stands for the IRI of rdf:type, so a class used only as a type is a badge on its instances, not a card.
DerivedRelation ($defs/DerivedRelation) is the object form of a relation type's derived. It has the properties of DerivedNode except parent and slot, with id optional, plus:
| Property | Type | Req. | Context | Description |
|---|---|---|---|---|
source |
Expression → Element |
✓ | deriveItem |
The source end. |
target |
Expression → Element |
✓ | deriveItem |
The target end. |
owner |
Expression → Element? |
– | deriveItem |
The element the relation belongs to (below). |
- When
idis absent, the id MUST be<Type>:<source.id>-><target.id>, with#2,#3, … appended to repeats infromorder, as for the expression form (4.9). - The ends MUST satisfy the relation type's
sourceandtargetdeclarations (4.9). An item whose ends do not is dropped, and onestd.derivedEndsfinding is reported per relation type, not per item. - The ends MAY be stored or derived elements of types computed earlier (4.11.5).
Owner. The owner of a relation is the element it belongs to: it is drawn in the owner's layer (6.16), hidden when the owner is collapsed or not drawn, and counted among the owner's contents by layout. Without owner, and for every stored relation, the owner is the nearest common ancestor of its two ends, or the diagram when they have none. CEL reads it as self.owner (12.2).
"functions": {
"liftTo": {
"params": [{ "name": "e", "type": "Element" }], "returns": "Element", "uses": ["diagram"],
"cel": "([e] + e.ancestors()).filter(a, inView(a)).first().orValue(null)"
}
},
"relations": {
"DrawnRelationship": {
"source": "Element", "target": "Element",
"attributes": { "description": { "type": "string" }, "technology": { "type": "string" } },
"derived": {
"from": "diagram.relationsOfType('Relationship').filter(r, liftTo(r.source) != null && liftTo(r.target) != null && liftTo(r.source) != liftTo(r.target))",
"key": "[liftTo(item.source).id, liftTo(item.target).id]",
"id": "'rel:' + liftTo(item.source).id + '->' + liftTo(item.target).id",
"source": "liftTo(item.source)",
"target": "liftTo(item.target)",
"attributes": { "description": "item.description", "technology": "item.technology" },
"edits": { "attribute:description": "describeRelationship", "delete": "deleteRelationships" }
}
}
}
A C4 view lifts each stored relationship to the nearest ancestor of each end that the view draws (ancestors() lists the parent first, 12.2), and draws several relationships that land on one pair as one line carrying the first one's description; the stored Relationship type is excluded from the viewpoint, so every drawn line is the derived one.
- Every derived element has
derived == trueand a list ofsourcesin CEL (12.2); a stored element hasderived == falseand no sources. - Constraints MAY scope derived types. A finding on a derived element is keyed by its id, and its location, where the constraint declares none, MUST be that of the first of its sources that has one (8.6).
- A suppression MUST NOT name a derived element whose id is ephemeral (11.5.3); one with a stable id MAY be suppressed like any element.
- Hooks (9.2) MUST NOT fire for derived elements appearing, changing or disappearing.
- A gesture that would change a derived element in the model (delete, reparent, connect, a rename, an attribute write) MUST be refused with
reason, as the refusal of 8.4, unlesseditsmaps the gesture to an operation. A mapped gesture MUST run that operation, as one transaction, withselfbound to the derived element; the operation reaches the stored elements throughself.sources, and the derived element then follows from recomputation.renameis the inline edit of the label, andattribute:<name>a write of that attribute from a form or label. - Moving or resizing a derived node writes view data like any node's, when its id is not ephemeral;
edits.moveinstead carries the move out by an operation (for example one that writes a bound attribute of the sources).
"removeResource": {
"for": ["Resource"],
"label": { "cel": "self.sources.size() > 1 ? 'Remove (with ' + string(self.sources.size()) + ' statements)' : 'Remove'" },
"actions": [ { "delete": "self.sources" } ]
}
- A runtime MUST compute derived elements when a model is loaded (14.2) and in the "recompute derived" step of every transaction (14.4), in declaration order: first the derived node types, in the order of
metamodel.types, then, (DISL 0.8) once, the ends of stored relations that name a derived node (4.9), then the derived relation types, in the order ofmetamodel.relations. Eachfromsees the stored elements and the derived elements of every type computed before it; that of a derived node type sees those ends unset. A reference to a derived type computed later is a specification error. - The elements of one derived type are ordered by the position of their group's first item in
from; that order is the type's order indiagram.nodes,nodesOfTypeand the other lists of 12.2 (12.5). - Two derived elements with the same id, or a derived id equal to a stored id, are a run-time error: the later one in computation order MUST NOT be drawn, and one
std.derivedIdfinding names both. - Derived elements MUST NOT be written, to a DID definition or by a format binding, and MUST NOT enter the undo history; undoing a change to their sources recomputes them. A runtime MAY recompute incrementally, but the result MUST equal a full recomputation.
- A failing
fromyields no elements of its type and onestd.derivedFailedfinding; a failing per-item expression drops that item, with one finding per type and error message (2.5). The rest of the diagram is drawn. - Derived ids are stable unless the id rule of their type makes them ephemeral (11.5.3). View data and style overrides MAY be stored for derived elements with stable ids and MUST NOT be stored for the others; a stored view key that names no element once derived elements are computed is kept and ignored, not reported as an unresolved reference (DID, section 5).
- Derived elements are drawn at step 2 of the drawing order (6.1) and count toward budgets (3.2.1) in their computation order.
fromis subject tolanguage.limits.celCostper evaluation; every other derived expression per item.
Illustrated by
- C4 container diagram: 8 items in this layer
- Causal loop diagram: 5 items in this layer
- Databricks job: 10 items in this layer
- Entity–relationship diagram: 5 items in this layer
- Functional decomposition graph: 15 items in this layer
- Gartner hype cycle graph: 8 items in this layer
- Mind map: 4 items in this layer
- Placeholder tasks: 3 items in this layer
- RDF graph: 12 items in this layer
- Service calls: 6 items in this layer
- State machine: 11 items in this layer
- Project timeline: 8 items in this layer
- Work board: 8 items in this layer
Referenced from
- 2. Foundations
- 3. Specification structure, layers and viewpoints
- 5. Layer 2 — Coordinate systems, placement and snapping
- 6. Layer 3 — Notation (visual definition)
- 7. Layer 4 — Toolbox, context tools and forms
- 8. Layer 5 — Constraints
- 9. Layer 6 — Behavior
- 11. Layer 8 — Persistence
- 12. The CEL environment
- 14. Processing model
- 17. Complete examples
- Appendix B — Built-in catalogues
- Appendix C — Glossary
- Cover of DISL 0.8