9. Layer 6 — Behavior
Contents of DISL 0.2
Behavior declares what happens in response to user actions, beyond the direct manipulation that the other layers already define: derived updates, cascades, generated names, multi-step operations.
{
"behavior": {
"hooks": [
{ "id": "nameNewState", "on": "create", "for": "State", "when": "!has(self.name)",
"actions": [ { "set": { "name": "'State ' + string(diagram.nodesOfType('State').size())" } } ],
"doc": "New states are numbered automatically." }
],
"operations": {
"makeFinal": {
"label": "Convert to final state", "for": "State", "icon": "flag",
"enabled": "self.outgoing.size() == 0",
"actions": [ { "retype": { "to": "'FinalState'" } } ]
}
},
"deletion": { "State": { "relations": "delete" } },
"clipboard": { "relations": "internal", "ids": "regenerate", "offset": [20, 20] },
"undo": { "mergeWindowMs": 500 }
}
}
Besides hooks (9.2), operations (9.3) and the policies of 9.5, behavior holds the reasons a runtime gives:
| Property | Type | Description |
|---|---|---|
reasons |
map id → NamedReason | A library of named reasons ($defs/NamedReason: {when, message, doc}), so one sentence is declared once and referred to as { "reason": "<id>" } wherever a Reason is accepted (2.3). when is evaluated in the context of the position that refers to it; absent, the reason always applies. |
editGate |
Reason[] | Diagram-wide reasons that withhold every model change while one applies. Context element, with self bound to the diagram. |
messages |
map standard message id → Message | Replaces the sentences a runtime otherwise words itself: std.notApplicable (an action invoked on a selection it does not apply to; context operation, plus operationId), std.readOnly (the diagram is opened read-only; context element on the diagram), std.atStart and std.atEnd (a move up or down at the first or last position). |
The edit gate. While any reason of behavior.editGate applies, a runtime MUST treat every model-changing tool, context tool, operation, form field and gesture as unavailable or refused, with the applicable reason. Gestures that write only view data (moving, resizing, folding) are not model changes and MUST NOT be withheld by the gate. The gate is checked after a notation refusal and before every constraint (8.4).
"behavior": {
"reasons": {
"truncated": { "when": "budget('cards').truncated",
"message": "The diagram shows only the first part of this file, so edits through it are withheld: an edit through a partial view could touch what the view does not show. Edit the file as text instead." }
},
"editGate": [ { "reason": "truncated" } ]
}
| Property | Type | Description |
|---|---|---|
id |
identifier | Required. |
on |
Event or Event[] | create, delete, change, connect, disconnect, reconnect, reparent, move, resize, paste, duplicate, retype, load, save, viewpointOpen. |
for |
TypeRef or TypeRef[] | Element types; absent for diagram-level events (load, save). |
attribute |
path or path[] | For change: only when these attributes change. |
when |
Expression | Condition (context hook, 12.3). |
actions |
Action[] | Executed in order. |
phase |
"before", "after" |
before hooks run inside the gesture transaction before constraints and may abort; after hooks (default) run after the change and before live constraints, in the same undoable transaction. |
order |
int | Ordering among hooks for the same event. |
doc |
Doc |
Hook context variables: self (the element after the change), old (a snapshot before the change, or null for create), event ({kind, attribute, oldValue, newValue, source} where source is "user", "hook", "operation", "layout", "import", "remote").
Termination. Actions of a hook MAY trigger further hooks. Runtimes MUST detect cycles: the same hook MUST NOT run twice for the same element within one transaction, and the total number of hook executions per transaction is bounded by behavior.maxHookDepth (default 100); exceeding it aborts the transaction.
Remote changes. Hooks do not run for changes received from collaborators (event.source == "remote"); those changes were already processed on the originating client.
Derived elements. Hooks MUST NOT fire for derived elements (4.11) appearing, changing or disappearing; a hook on a derived type fires only for an event no recomputation causes, such as viewpointOpen. Changes of viewer state (11.6) are not model events and fire no hook.
Operations are named, documented commands, invoked from tools, context tools, form buttons, quick fixes, keyboard shortcuts or menus.
| Property | Type | Description |
|---|---|---|
label |
Message | Menu and button text; context operation without p. Evaluated for each target it is offered for (2.3). |
doc, icon, shortcut |
||
for |
TypeRef[], "selection", "diagram" |
What the operation applies to. For type lists, self is the target element; for selection, selection is the list. |
params |
map → Attribute | Parameters; if present, the runtime shows a generated dialog (or paramsForm). Available as p in actions. |
paramsForm |
form id | Custom dialog. |
enabled |
Expression | Whether available. |
unavailable |
Reason[] | Why the operation cannot run now; context operation without p. While one applies, it is unavailable with that reason (below). |
confirm |
Message or Confirmation | Question asked before running (9.5). |
actions |
Action[] | Body, executed as one transaction. |
plugin |
{name, args} |
Alternatively implemented by a plugin. |
simulate |
Simulation | Alternatively a simulated run (9.6). Exactly one of actions, plugin and simulate MUST be present. |
Available, unavailable, not offered. An operation is unavailable when its enabled is false, or when a reason in its unavailable list or in behavior.editGate (9.1) applies. Every entry that invokes an operation (a tool, context tool, menu entry, form button, quick fix or shortcut) inherits the operation's enabled and unavailable, so a reason is declared once. A runtime MUST show an unavailable entry disabled with the applicable reason (a tooltip, inline text or a status line), MUST NOT run it, and MUST refuse it with the same reason when it is invoked anyway, by a shortcut or from another surface. An entry that is disabled only by enabled, with no reason applying, is disabled as in 0.1, and the runtime MAY show a generic reason. Whether an entry is offered at all is decided by its own visible (7.2, 7.3) and, for operations, by for: an entry that is not offered is not shown.
"arrange": {
"label": "Arrange diagram", "for": "diagram",
"unavailable": [ { "when": "diagram.nodesOfType('Variable').size() < 2",
"message": "There is nothing to arrange until this diagram has two variables." } ],
"actions": [ { "layout": { "scope": "diagram.nodes", "algorithm": "selfOrganizing" } } ]
}
Actions are a small, closed set of declarative steps. All values in actions are Expressions (2.5d).
| Action | Form | Effect |
|---|---|---|
set |
{ "set": { "attr": expr, … }, "target": expr } |
Assign attributes of target (default self). |
unset |
{ "unset": ["attr"], "target": expr } |
Remove stored values. |
create |
{ "create": { "type": expr, "attributes": {…}, "parent": expr, "slot": expr, "at": expr, "size": expr, "after": expr, "before": expr }, "as": "name" } |
Create a node; the new element is available as name in subsequent actions. after or before places it next to a sibling (below). |
connect |
{ "connect": { "type": expr, "source": expr, "target": expr, "attributes": {…} }, "as": "name" } |
Create a relation. |
delete |
{ "delete": expr } |
Delete an element or list. |
move |
{ "move": { "target": expr, "x": expr, "y": expr, "parent": expr } } |
Change placement (domain values, snapped if applyToProgrammatic). |
resize |
{ "resize": { "target": expr, "width": expr, "height": expr } } |
|
retype |
{ "retype": { "to": expr, "target": expr } } |
Change the type, keeping compatible attributes and relations. |
reparent |
{ "reparent": { "target": expr, "parent": expr, "slot": expr, "after": expr, "before": expr } } |
after or before places it next to a sibling (below). |
reorder |
{ "reorder": { "target": expr, "by": expr } }, or with after or before in place of by |
Change only the element's place among its siblings: by a signed number of places (−1 is one earlier), or directly after or before a sibling. |
view |
{ "view": { "collapsed": expr, "filters": { "<id>": expr }, "viewpoint": expr }, "target": expr } |
Change viewer state (11.6) of target (default self): its collapsed state, the canvas filters' values (6.13.1), or the viewpoint variant shown (3.5). |
let |
{ "let": { "name": expr } } |
Bind a variable for later actions. |
if |
{ "if": expr, "then": [ … ], "else": [ … ] } |
Conditional. |
forEach |
{ "forEach": expr, "as": "item", "do": [ … ] } |
Iterate a list (bounded: lists are finite). |
select |
{ "select": expr } |
Change the selection. |
reveal |
{ "reveal": expr } |
Scroll the canvas to elements. |
highlight |
{ "highlight": expr, "durationMs": 2000 } |
Apply the highlighted state. |
editLabel |
{ "editLabel": { "target": expr, "label": "id" } } |
Start inline editing. |
openForm |
{ "openForm": { "target": expr, "form": "id" } } |
|
notify |
{ "notify": { "message": expr, "severity": "info" } } |
Toast message. |
layout |
{ "layout": { "scope": expr, "algorithm": "id" } } |
Run a layout (section 10). |
abort |
{ "abort": { "message": expr } } |
Cancel the transaction (only in before hooks and operations). A runtime MUST show the message to the user as the refusal of the gesture or command, in the words given. |
call |
{ "call": "operationId", "args": { … } } |
Run another operation. |
plugin |
{ "plugin": "name", "args": { … } } |
Delegate to a plugin action. |
Every action MAY carry when (skip unless true) and doc.
Order of evaluation. Actions MUST run in order against the transaction's working state. Every expression of an action, including its when, MUST see the effects of every action run before it in the same transaction, including those of earlier iterations of an enclosing forEach. The list of a forEach MUST be evaluated once, before its first iteration, and a let value once, where it appears. So a hook that claims closed loops one by one sees, in each iteration, the loops the earlier iterations created: the second loop takes the identifier after the first one's.
Positional create. after and before (at most one of the two) name a sibling element. With either, the new or reparented element MUST be placed directly after or before that sibling in its parent's child order, and the sibling MUST have the same parent (or both be top level); otherwise the transaction is rolled back with an error. The parent's type MUST declare children.ordered: true (4.8), or for top-level elements the diagram's children are ordered: a validator SHOULD warn where it cannot prove this, and a runtime MUST roll back a positional create into an unordered parent. The runtime assigns the stored order between the neighbours in the same transaction (a fractional index under persistence.collaboration.ordering: "fractional-index", otherwise by renumbering the following siblings). Without after or before, a new child is appended last, as in 0.1.
"addSibling": { "label": "Add sibling", "for": ["Node"], "shortcut": "Enter",
"actions": [
{ "create": { "type": "'Node'", "parent": "self.parent", "after": "self",
"attributes": { "text": "siblingName(self.parent.children.map(c, c.text))" } }, "as": "added" },
{ "select": "added" }, { "editLabel": { "target": "added" } } ] }
Reorder. A reorder action MUST change only the order among siblings and is a reorder gesture (8.4), refused by reorder constraints. A by that would move the element past the first or last place is an error that rolls the transaction back; moveUp and moveDown (7.3) are unavailable at the ends instead.
View actions. A view action MUST change viewer state only. It is allowed in read-only mode, is never persisted and never enters the undo history. An operation whose actions are all view, select, reveal and highlight actions MUST be offered in read-only mode and MUST NOT create an undo step. A set action on a transient: "viewer" attribute (4.3) follows the same rules.
"toggleFold": { "label": { "cel": "self.view.collapsed ? 'Expand' : 'Collapse'" }, "for": ["Node"],
"enabled": "self.children.size() > 0",
"actions": [ { "view": { "collapsed": "!self.view.collapsed" } } ] }
Deletion (behavior.deletion, per type):
| Property | Values | Description |
|---|---|---|
children |
"delete" (default), "reparent", "forbid" |
What happens to contained elements. reparent moves them to the deleted node's parent. |
relations |
"delete" (default), "forbid", "reconnect" |
Attached edges. reconnect bridges incoming to outgoing edges of the same type (removing a step from a chain). |
references |
"unset" (default), "delete-referencing", "forbid" |
Reference attributes pointing at the element. |
confirm |
Message or Confirmation | Ask before deleting (below). |
Confirmation ($defs/Confirmation). A confirm in a deletion policy, an operation (9.3) or a form button (7.5) is a Message, the 0.1 form, or a Confirmation:
| Property | Type | Default | Description |
|---|---|---|---|
message |
Message | required | The question. |
title |
Message | the operation's label, or "Delete" for a deletion | Title of the dialog. |
confirmLabel |
Message | the runtime's "OK", or "Delete" for a deletion | Label of the confirming button. |
cancelLabel |
Message | the runtime's "Cancel" | Label of the cancelling button. |
danger |
bool | true for a deletion, false otherwise |
Styles the confirming button as destructive. |
when |
Expression → bool | true |
Ask only when it holds. |
count |
Expression → int | absent | A number the confirmation is about, bound as count in the texts and in when. |
threshold |
int | 0 |
Ask only when count >= threshold. Ignored without count. |
doc |
Doc |
Its expressions are evaluated in the context of what is confirmed, plus count: for a deletion, the delete gesture context (self, selection, diagram, env); for an operation, the operation context (with p once the parameter dialog is answered); for a form button, the form context.
- A runtime MUST ask for confirmation before the transaction when
whenholds and, ifcountis given,count >= threshold; otherwise it MUST proceed without asking. A threshold of0asks every time. - A plain Message in
confirmis a Confirmation with onlymessage, and asks every time, as in 0.1. - One deletion transaction asks at most once. With several elements selected, the runtime uses the confirmation of the first element, in selection order, whose confirmation asks, with
selectionbound to all of them.
"deletion": { "Node": { "children": "delete", "relations": "delete",
"confirm": {
"count": "self.descendants().size()", "threshold": 1,
"title": "Delete branch?",
"message": { "cel": "'Delete \\'' + self.text + '\\' and the ' + string(count) + ' nodes under it? You can undo this.'" },
"confirmLabel": "Delete", "danger": true } } }
A leaf has no descendants, so deleting it asks nothing; a branch asks with its count. Asking only when a deleted element still has connections is the same pattern with "count": "self.incoming.size() + self.outgoing.size()".
Clipboard (behavior.clipboard): relations ("internal" — only edges between copied elements, "all", "none"), ids ("regenerate"), names ("keep", "suffix" — adds " (copy)" to the label attribute), offset (canvas offset on paste; on time axes a duration), crossDocument (bool), formats (clipboard MIME types offered: application/vnd.did.fragment+json (DID, section 7), image/svg+xml, text/plain).
Retyping (behavior.retype): map of type → allowed target types, with attributeMapping (target attr → Expression over old).
Undo (behavior.undo): mergeWindowMs (typing in a label merges into one step), maxSteps, persistHistory (bool, stores history in the DID definition for collaborative review, default false).
A simulation plays a run of the diagram over time, step by step, without changing the model: a job's tasks turning from pending to running to succeeded, a change rippling through a bundle. It is the body of an operation, simulate, in place of actions or plugin. Simulation ($defs/Simulation):
| Property | Type | Req. | Description |
|---|---|---|---|
for |
TypeRef or TypeRef[] | ✓ | The element types that take part. |
state |
attribute name | ✓ | An attribute declared on each of those types with transient: true or transient: "viewer" (4.3); the only thing a simulation writes. |
initial |
Expression | ✓ | Each taking-part element's state at step 0. |
next |
Expression | ✓ | Each element's state at the next step. |
until |
Expression → bool | ✓ | Evaluated after each step, once, with self bound to the diagram; the run finishes when it holds. |
stepMs |
int ≥ 0 | – | The time between steps, in milliseconds. Default 700. |
maxSteps |
int ≥ 1 | – | A hard stop. Default 1000. |
notice |
{ "running": Message, "finished": Message } |
– | The status notice (6.13) shown while the run plays and after it finishes. |
Its expressions are evaluated in the simulation context (12.3), which binds self, state (a map from element id to that element's state at the previous step), step (the number of the step being computed, from 1), p (the operation's parameters), diagram and env.
- A simulation MUST run outside any transaction. Its writes to
stateMUST NOT enter the undo history, mark the diagram modified, be persisted, fire hooks or be seen by constraints. - Every element's
nextMUST be evaluated against the previous step's states, all elements at once (a simultaneous update), and the new states applied together. - Committing any transaction, changing the viewpoint, closing the view or dismissing the notice MUST end the run and clear the states.
- A runtime that honours reduced-motion settings MAY run the steps without delay. Headless validators never run simulations.
- A simulation is offered in read-only mode, like an operation whose actions change only viewer state (9.4).
"simulateRun": {
"label": "Run job (simulated)", "icon": "mdi-play-outline", "for": "diagram",
"params": { "failTaskId": { "type": "string" } },
"simulate": {
"for": ["Task"], "state": "simulated", "stepMs": 700,
"initial": "'pending'",
"next": "state[self.id] == 'running' ? (self.key == p.failTaskId ? 'failed' : 'succeeded') : state[self.id] == 'pending' && upstreamSettled(self, state) ? (mayRun(self, state, p) ? 'running' : 'skipped') : state[self.id]",
"until": "state.all(id, state[id] in ['succeeded', 'failed', 'skipped'])",
"notice": { "running": "Simulated: job run", "finished": "Simulated: job run, finished" }
}
}
A Databricks job plays a mock run wave by wave: a task starts once its upstream tasks have settled and its run_if condition allows, and one chosen task fails. upstreamSettled and mayRun are user functions (3.4).
Illustrated by
- C4 container diagram: 2 items in this layer
- Causal loop diagram: 5 items in this layer
- Databricks job: 6 items in this layer
- Entity–relationship diagram: 1 items in this layer
- Functional decomposition graph: 2 items in this layer
- Gartner hype cycle graph: 3 items in this layer
- Mind map: 9 items in this layer
- RDF graph: 12 items in this layer
- State machine: 9 items in this layer
- Project timeline: 3 items in this layer
Referenced from
- 1. Introduction
- 2. Foundations
- 3. Specification structure, layers and viewpoints
- 4. Layer 1 — Metamodel
- 6. Layer 3 — Notation (visual definition)
- 7. Layer 4 — Toolbox, context tools and forms
- 8. Layer 5 — Constraints
- 11. Layer 8 — Persistence
- 12. The CEL environment
- 14. Processing model
- 15. Conformance
- 16. Security, privacy and robustness
- 17. Complete examples
- Appendix D — Design rationale and open questions