Skip to content

9. Layer 6 — Behavior

Contents of DISL 0.4

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.

9.1 Overview

{
  "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).
placements map id → ParentPlacement (DISL 0.3) Named parent placements, which choose the parent of a new element by geometry; referred to by a drop's or a create action's place (below).

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" } ]
}

Parent placements (DISL 0.3). A drop (7.2) or a create action (9.4) normally names its parent: the element under the pointer, or a fixed parent, after or before. A parent placement ($defs/ParentPlacement) chooses the parent instead by geometry, among elements that need not be under the pointer, and the index among that parent's children:

Property Type Default Description
candidates Expression → list(Element) required The elements that may become the parent.
as SimpleId candidate The name each candidate is bound to in rank.
rank Expression → number required Evaluated per candidate; the lowest wins.
index Expression → int after the last child The new element's index among the chosen parent's children, with parent bound to it.
whenEmpty "root", "refuse" "refuse" What happens when there is no candidate.
refusal Reason the runtime's sentence The refusal when no parent is chosen.
doc Doc
  • A placement is evaluated in the parentPlacement context (12.3): the create context with position bound to the drop point, or to the point where the context menu was opened.
  • A runtime MUST evaluate candidates, bind each one in turn as as, and choose as parent the candidate with the lowest rank, the first in model order on a tie.
  • With a parent chosen, the runtime MUST evaluate index with parent bound to it, and MUST create the element as that parent's child at that index (clamped to the number of children). The parent's type MUST declare children.ordered: true (4.8), as for a positional create.
  • With no candidate, the runtime MUST create the element at the top level when whenEmpty is "root" and the diagram has no top-level element of a type candidates could return, and MUST otherwise refuse with refusal.
  • A drop with place MUST NOT consult targets or elsewhere (7.2). A create action with place MUST NOT also give parent, after or before.
"behavior": {
  "placements": {
    "underNearestAbove": {
      "candidates": "diagram.nodes.filter(n, takesAnotherChild(n) && n.view.bounds.center()[1] < position.y)",
      "as": "candidate",
      "rank": "math.abs(position.x - candidate.view.bounds.center()[0]) + 2.0 * (position.y - candidate.view.bounds.center()[1])",
      "index": "parent.children.filter(c, c.view.bounds.center()[0] < position.x).size()",
      "whenEmpty": "root",
      "refusal": "Drop the node below the node it belongs under: a Do in order, a Try in order, a Do together, or a Retry, Repeat until, Only while or Ask approval before that has no child yet."
    }
  }
}

An agent behavior model takes as parent the node above the drop point that can take another child and is nearest by |x - cx| + 2 (y - cy), and puts the new node before the first of its children whose centre lies right of the drop point. An empty tree takes the node as its root, and a drop with no node above it is refused. What is stored is the node's place in the tree; its position on the canvas follows from the layout (10.3).

9.2 Hooks

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.

9.3 Operations

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" } } ]
}

9.4 Actions

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, "place": "id" }, "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). (DISL 0.3) place names a parent placement (9.1) that chooses its parent and index, with position the point the operation was invoked at; it excludes parent, after and before.
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, "pinned": expr, "groups": { "<key>": expr } }, "target": expr } Change view data (11.6) of target (default self): its collapsed state, the canvas filters' values (6.13.1), the viewpoint variant shown (3.5), and (DISL 0.4) whether it is pinned (7.3) and the collapse state of its groups by group key (6.9).
open { "open": expr } (DISL 0.4) Open a link (below).
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", "refusals": { "nothingDrawn": Message, "unchanged": Message } } } Run a layout (section 10). (DISL 0.3) refusals.nothingDrawn refuses the action while its scope holds nothing the algorithm can place (for rows, no element with a drawable extent, 10.2); refusals.unchanged refuses it when the layout would change nothing (10.2).
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. (DISL 0.4) A view action that changes a kind of stored view data (pinned, groups, or a filter with persist, 11.6) is an edit: it is written, enters the undo history with the operation it belongs to, and is refused in read-only mode. Setting pinned to true pins the target at its current position; setting it to false removes the pin and the stored position. Setting a group's state to its default removes the stored state (6.9). The rest of this paragraph is the 0.3 rule for viewer state. 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.

Open (DISL 0.4). open takes a link, a string, and MUST treat it as follows, never following a link the user did not activate:

  • An http or https URI is opened in the platform's browser, in a new tab or window, without giving the opened page access to the runtime (no opener, no referrer).
  • A path (4.2, paths) is resolved against the folder of the diagram's file when relative. When it lies inside the workspace the host shows, the host reveals it there, and opens it when it is a file the host has a tool for. Otherwise the host shows the path, says why it was not opened, and offers to copy it.
  • Anything else is not opened; the action is refused with a reason that names the link.

open changes no model, no view data and no history, and is allowed in read-only mode.

"openLink": { "label": "Open link", "for": ["Item"], "enabled": "self.link != ''",
  "actions": [ { "open": "self.link" } ] }
"toggleFold": { "label": { "cel": "self.view.collapsed ? 'Expand' : 'Collapse'" }, "for": ["Node"],
  "enabled": "self.children.size() > 0",
  "actions": [ { "view": { "collapsed": "!self.view.collapsed" } } ] }

9.5 Deletion, clipboard, retyping and undo policies

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 when holds and, if count is given, count >= threshold; otherwise it MUST proceed without asking. A threshold of 0 asks every time.
  • A plain Message in confirm is a Confirmation with only message, 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 selection bound 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). (DISL 0.3) An entry's when (Expression → bool, with self the element and target the candidate type) is optional and defaults to true, so that without it every type in to is allowed whatever the element's state. When given, a retype action (9.4) and a type form item (7.5) MUST NOT retype an element to a type whose when is false, and a type item without options does not offer it. A retype that to allows but that the element's state forbids, such as a type that holds no children for an element that has some, is refused by whoever applies the change, typically the writer of the persistence format; a specification states those refusals, in the writer's sentences, as the refusals of its type item (7.5), and keeps the choices free of them with that item's options, rather than with when. A type an entry's to does not list, or an element that has no entry, is refused with the runtime's sentence.

"retype": { "Sequence": { "to": ["Fallback", "Parallel", "Retry", "Check", "Do"],
  "attributeMapping": { "attempts": "has(old.attempts) ? max(old.attempts, 3) : 3" } } }

An agent behavior model lists every other kind in to and gives no when: a Sequence with two children may be listed as becoming a Do, and the Markdown writer refuses it with "Do" holds no children, and this node has 2 children., the sentence of the Kind item's first refusal (7.5).

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

9.6 Simulations

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 state MUST NOT enter the undo history, mark the diagram modified, be persisted, fire hooks or be seen by constraints.
  • Every element's next MUST 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

Referenced from