Skip to content

4. Layer 1 — Metamodel

Contents of DISL 0.6

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.

4.1 Overview

{
  "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.

4.2 Primitive types

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.

4.3 Attributes

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 with message, for a drag before it is released (8.4).
  • std.facets and std.axisBounds (8.7) keep their severity and enforcement; outOfRange only 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 on lists that changes one of attributes or either attribute of between, a runtime MUST clamp the present values of attributes, in order, so that each lies at least gap after the previous anchor and at least gap (times the number of anchors still to come) before the second attribute of between, where an anchor is a present value or, with absent: "spread", the evenly spread position of an absent one. The first attribute of between is the anchor before the first.
  • Only the first inner attributes take part as anchors; the others are only clamped inside between.
  • 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 between leaves too little room for every anchor gap apart, 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 where gap itself 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).

4.4 Data types

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.

4.5 Enumerations

{
  "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.

4.6 Node types

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.

4.7 Inheritance

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, label or doc, set required: true, narrow a reference type to a subtype, or set fixed (a constant is the tightest facet; default is 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, allowSelfLoops or allowParallel MUST 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: allowSelfLoops and allowParallel from true to false. It MUST NOT state true for allowSelfLoops or allowParallel where a direct supertype's value is false, and MUST NOT state a directed that differs from a direct supertype's; a supertype's value is its value by the lookup above, the default included, so directed: false under a supertype that states nothing differs from its true. 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) abstract and derived make 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.

4.8 Containment and references

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).

4.9 Relation types

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.

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.

4.10 Ports

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).

4.11 Derived elements

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).

4.11.1 Stored and derived elements (informative)

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.

4.11.2 Derived node types

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 derived MUST NOT be instantiated by a user, a tool, a template or an action. create, delete, retype and reparent actions 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 derived expression (4.3) is evaluated on the derived element in the element context, 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 derived or sources, 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.

4.11.3 Derived relations

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 id is absent, the id MUST be <Type>:<source.id>-><target.id>, with #2, #3, … appended to repeats in from order, as for the expression form (4.9).
  • The ends MUST satisfy the relation type's source and target declarations (4.9). An item whose ends do not is dropped, and one std.derivedEnds finding 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.

4.11.4 Provenance, findings and edits

  • Every derived element has derived == true and a list of sources in CEL (12.2); a stored element has derived == false and 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, unless edits maps the gesture to an operation. A mapped gesture MUST run that operation, as one transaction, with self bound to the derived element; the operation reaches the stored elements through self.sources, and the derived element then follows from recomputation. rename is the inline edit of the label, and attribute:<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.move instead 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" } ]
}

4.11.5 Computation

  • 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 the derived relation types, in the order of metamodel.relations. Each from sees the stored elements and the derived elements of every type computed before it. 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 in diagram.nodes, nodesOfType and 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.derivedId finding 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 from yields no elements of its type and one std.derivedFailed finding; 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.
  • from is subject to language.limits.celCost per evaluation; every other derived expression per item.

Illustrated by

Referenced from