13. Sequence

A sequence reads a diagram on a time axis: layout: sequence places named participants across the top, drops a lifeline from each, and lays messages — ordinary links — top-to-bottom in source order, so the order you write the wires is the order they happen. It adds no grammar: participants are nodes, messages are links (SPEC 9), frames and notes are nodes — only the engine, six type names, and two properties (place, activation) are new, and it lowers to primitives like any layout-owning engine (SPEC 11, seam 3).

The container & its children

layout: sequence on the root ({ layout: sequence }) or any node makes a sequence; the |sequence| template is the preset over |block|. Its children split by role, recognised by type — every other box is a participant (an open fallback, unlike a chart’s closed series set):

ChildIs aDrawn
a box (|box|, |cyl|, |icon|, …)participanta header at the top + a lifeline down
a link (a -> b)messagea time-row arrow between two lifelines
|loop| / |opt| / |alt|framea labelled rectangle around a span of messages
|else|separatora guarded compartment divider inside an |alt|
|note|notea callout over / beside lifelines

Nodes and links interleave in source order (SPEC 9), so a frame (a node) sits among the messages (links) around it.

One scope. Every message resolves its endpoints against the sequence’s participants, whatever frame it sits in: a frame’s [ ] groups messages for layout but opens no new scope — it declares no participants and auto-creates none, overriding the sealed-body and body-auto-create rules (SPEC 3, SPEC 9) inside a sequence. So |alt| [ db --> api … ] wires the outer db / api lifelines, never frame-local boxes.

gap sets spacing: its column part the space between participants, its row part the gap between message rows (gap: row col; default 32 32). A label wider than its span widens it — adjacent lifelines sit max(gap-column, widest message label between them + margin) apart, text measured at compile time. width / height size the whole frame and distribute any surplus; unset, it sizes to its content.

Participants & lifelines

A participant is an ordinary node; its smart label is its header, placed by its type (SPEC 3) — centred text for a box, the symbol for an |icon|. Participants sit across the top in declaration order (left to right), each dropping a lifeline and sharing a common foot at the last row. An undeclared endpoint auto-creates a participant — a -> b with neither declared draws two — appended in first-use order, so a quick sequence needs no header:

{ layout: sequence }
user   -> server "login"     // two auto-created participants, one message
server --> user  "token"
user server login token

Declare a participant (with an #id, so messages can name it) to fix its order, type, or paint: |cyl#db| "Store", or |icon#user| "user" for an actor glyph. A participant lends its paint to its apparatus: its lifeline and activation bars take its own fill / stroke / stroke-width, so colouring or weighting a participant carries down its whole timeline. Being an ordinary box, it also honours the box model on its own content (SPEC 11).

Messages

A message is a link (SPEC 9) read on the time axis: its operator picks the look, its label rides above the arrow, its order is its row.

WriteMeans
a -> b "x"a call — solid arrow, a to b
a --> b "x"a return — dashed arrow
a ~> b "x"an async message — wavy arrow
a -> a "x"a self-message — a hook on a’s own lifeline, label to the right

Every operator, marker, class, and { } is the link’s own; only the placement differs, so a message’s label sits centred above its arrow and along: has no role. A chain a -> b -> c is two messages on two rows; a fan a -> b & c likewise expands to two, in expansion order. A forced side (a:left) and routing have no meaning on a time-row arrow and are ignored. Call vs. return is read from the operator (-> vs -->), not a stroke-style: override.

Activations

A participant is active while it handles a call. By default (activation: auto) a call (->) opens an activation bar on its target’s lifeline; the next return (-->) from that target closes its most recent open bar; nested calls stack (each bar offset outward), and an unclosed bar runs to that participant’s last row. The bar stack is sequence-global — a call inside a frame may close outside it. A self-message (a -> a) and an async (~>) open none, and a return with no open bar just draws its arrow. activation: none on the sequence draws no bars. (Explicit per-message control is deferred — SPEC 24.)

Frames & notes

A frame is a node whose [ ] holds its messages, drawn as a dashed rectangle spanning the lifelines those messages touch (plus a small inset) over the rows they occupy. A top-left tab names the operator; the frame’s smart label is its guard, drawn as the first compartment’s condition. Frames nest and draw behind the lifelines (a fill tints the region without hiding the wires):

FrameMeans
|loop| "guard"the messages repeat (drawn once, not unrolled)
|opt| "guard"the messages happen only if the guard holds (an if)
|alt| "guard"one of several alternatives (an if/else)

An |alt| holds two or more compartments split by |else| "guard" — a separator valid only inside an |alt|, its label that branch’s guard; the first compartment’s guard is the |alt|’s own label:

api -> db "query"
|alt| "found" [
  db --> api "row"
  |else| "missing"
  db --> api "404"
]
api --> user "done"

A |note| is a callout placed at its time row (source order), bound to lifelines by place: — a mode, then its lifeline id(s): { place: over api } a box over one lifeline, { place: over api db } a box spanning those (and any between), { place: left api } / { place: right api } a box beside one. One mode per note. Its smart label is the text; a multi-line or styled note rides the [ ] like any box. place: is valid only in a sequence. par and other fragments are deferred (SPEC 24).

Defaults

The five sequence types are bundles over |block|, tuned to read with no styling; the cascade overrides any of it, and they reuse the scene’s role variables — no new ones. (|note| is the core template, compacted here by its built-in scoped rule — SPEC 8.)

TypeDefaults over |block|
|sequence|layout: sequence; gap: 32 32 (a root { layout: sequence } gets the same gap)
|loop| / |opt| / |alt|fill: none; stroke: --group-stroke; stroke-style: dashed; stroke-width: 1; radius: 4; padding: 24; font-size: 12
|else|fill: none; stroke: --group-stroke; stroke-style: dashed; stroke-width: 1; font-size: 12

The engine resolves in the layout phase — a message’s x-ends are the lifelines’ positions (fixed once participants are placed) and its y is its row — placing participants, walking messages/frames/notes in source order, and lowering headers → |block| + text, lifelines and arrows → |line|, activations/frames/notes → |block| (SPEC 19). The orthogonal router never sees these links.