Skip to content

Process Representations

A process is one model — Chapter 7 covers the steps, the branching, the sagas. But the audience reading it changes. A new hire wants a story: who calls whom, in order, start to finish. An API reviewer wants request/response pairing made explicit — lifelines, not prose. Ops, laning a BPMN diagram for a runbook, wants to know which team owns which band of the diagram, and which teams’ work groups into one pool.

None of that is three different processes. It’s the same Checkout, rendered three ways. A flow view (from Chapter 8) picks the mode; the process underneath never changes.

flow <ProcessRef> [sequence | bpmn] takes a mode token. Plain (no token) is the flow walkthrough — the shape from Chapter 7, top to bottom. sequence renders the same control flow as a UML-style sequence diagram: one lifeline per participant, messages as arrows between them — the form an API reviewer already reads fluently, with request/response pairing visually explicit in a way the walkthrough leaves implicit. bpmn renders a BPMN collaboration: performers sorted into horizontal swimlanes, lanes optionally grouped into pools — the form ops and business stakeholders expect from a process diagram.

Every mode renders resolved steps, so the line glyphs of Chapter 7 leave no trace of their own: a | fan is drawn as a second edge off the shared head, a \ snake as the run’s next hop (the tail node isn’t repeated), and each written line keeps exactly one row in the tree and sidebar.

Mode is presentation truth — you write the mode you intend the view to ship in, the same way you write show/hide for a board view:

service Orders {
aspect {
team: "Commerce"
domain: "Storefront"
}
}
service Payments {
aspect {
team: "Payments"
domain: "Backend"
}
}
service Inventory {
aspect {
team: "Fulfillment"
domain: "Backend"
}
}
service Notifications {
aspect {
team: "Platform"
domain: "Backend"
}
}
process Checkout {
Customer > Orders.createOrder
Orders > Inventory.reserve
Orders > Payments.authorize
Payments if "flagged" {
Ops "manual fraud review"
}
Orders > Notifications.sendEmail
}
view CheckoutWalkthrough {
"Checkout for the onboarding deck — the flow walkthrough, plain and linear"
flow Checkout
}
view CheckoutApiReview {
"Checkout for an API review — lifelines make request/response pairing explicit"
flow Checkout sequence
}
view CheckoutOps {
"Checkout as a BPMN collaboration, laned by team and pooled by domain"
flow Checkout bpmn {
lane by @@team
pool by @@domain
}
}

Three views, one process Checkout. Change a step in the process and all three re-render from it — there’s nothing to keep in sync, because nothing but the mode differs.

Each mode is one of the four representation clauses a view can carry (board, table, matrix, flowChapter 8), so a flow view still can’t also declare table or matrix; the ref is written explicitly (flow Checkout, never inferred from show), and it names a process or a subprocess — anything with a renderable flow.

The bpmn mode’s optional body sorts performers — the owners of owned actions, Chapter 7 — into lanes. A callee reached only as a target (never an owner) stays task-label text; it doesn’t get its own band.

lane <selector> takes any selector from the selector language and gives one lane per matched performerlane service on the model above would open four lanes, one each for Orders, Payments, Inventory, Notifications. Prefix the selector with a title and several matches merge into one lane instead: lane "Backend": Payments or Inventory or Notifications.

Claims are first-wins, in document order — a performer already placed by an earlier lane clause isn’t picked up by a later one, even if its selector would otherwise match:

service Orders {}
service Payments {}
service Inventory {}
service Notifications {}
process Checkout {
Customer > Orders.createOrder
Orders > Inventory.reserve
Orders > Payments.authorize
Orders > Notifications.sendEmail
}
view CheckoutOps {
"first-wins: Payments is claimed by the first clause, so the titled
lane below picks up only the two performers still unclaimed"
flow Checkout bpmn {
lane Orders or Payments // two lanes: Orders, Payments
lane "Backend": Payments or Inventory or Notifications // Payments already claimed — only Inventory + Notifications join
}
}

Orders or Payments is untitled, so it opens two lanes, not one. Payments is gone by the time the titled "Backend" clause runs, so that lane ends up holding only Inventory and Notifications — the two performers still up for grabs.

For the common case where lanes should track an aspect rather than be hand-picked, lane by <getter> opens one lane per distinct value automatically — that’s what lane by @@team did in the flagship example above: one lane per team, with no need to enumerate the performers by hand as teams come and go.

Zero lane clauses is the laneless render — one lane per performer, same as lane <sort> over everything. With at least one lane clause, an implicit default lane catches whatever’s left unclaimed: untitled, rendered last, and shown only when it’s non-empty. Unowned work — a step whose owner never resolved — lands there too, so unassigned work is visibly unplaced rather than silently dropped.

A persona actor — an owner name that only ever shows up on a note step or a control-flow prefix, never as a real caller — carries no aspects, because it isn’t a modeled element. lane by @@team can never match it: aspect atoms, by getters, and pool queries are fail-closed against personas. The one way to place a persona in a lane is a bare ref or an or-union of refs:

service Orders {}
service Payments {}
process Checkout {
Customer > Orders.createOrder
Orders > Payments.authorize
Payments if "flagged" {
Ops "manual fraud review"
}
}
view CheckoutOps {
"@@team never matches Ops — it's a persona, not a modeled service, so it
falls into the default lane unless claimed by bare ref"
flow Checkout bpmn {
lane by @@team
}
}
view CheckoutOpsClaimed {
flow Checkout bpmn {
lane by @@team
lane Ops
}
}

CheckoutOps lanes Orders and Payments by team and drops Ops into the default lane — Ops carries no team aspect to match against. CheckoutOpsClaimed adds lane Ops, an explicit bare-ref claim, and Ops gets its own band instead.

A pool groups lanes by a query over their performers, with one rule: a lane joins a pool exactly when every performer in the lane matches the pool’s query. That one rule covers the default lane too — it joins a pool exactly when everything unclaimed happens to match. The first matching pool wins, in declaration order; a pool that ends up with no lanes is dropped from the render.

Three ways to write the query:

  • pool <Ref> — sugar for <Ref> or in <Ref>. A space or module is a natural pool: every lane whose performer is that container, or sits inside it, joins.
  • pool "Title": <selector> — an arbitrary query, not tied to any container. The title is required here (an untitled pool always takes an element ref).
  • pool by <getter> — one pool per distinct value, auto-derived, mirroring lane by.
system Backend {
service Payments {
aspect {
team: "Payments"
domain: "backend"
}
}
service Inventory {
aspect {
team: "Fulfillment"
domain: "backend"
}
}
service Notifications {
aspect {
team: "Platform"
domain: "backend"
}
}
}
service Orders {
aspect {
team: "Commerce"
domain: "storefront"
}
}
process Checkout {
Customer > Orders.createOrder
Orders > Inventory.reserve
Orders > Payments.authorize
Orders > Notifications.sendEmail
}
view CheckoutBySpace {
"Untitled pool sugar — pool Backend groups every lane whose performer sits inside the Backend system"
flow Checkout bpmn {
lane by @@team
pool Backend
}
}
view CheckoutByQuery {
"Titled pool — an arbitrary query over performers, not tied to a container"
flow Checkout bpmn {
lane by @@team
pool "Backoffice": @@domain:"backend"
}
}
view CheckoutByDomain {
"Auto pools — one per distinct @@domain value"
flow Checkout bpmn {
lane by @@team
pool by @@domain
}
}

All three land the same result here — Payments, Inventory, and Notifications pool together, Orders stands alone — by three different routes: structural containment, a hand-written query, and automatic derivation. Reach for pool <Ref> when a real container already exists, pool by when the grouping is one aspect away, and a titled query pool for anything in between.

A reversible block’s unwind chain (Chapter 7) reads under its forward steps regardless of laning — compensation bands render globally, below every lane band, never nested inside one team’s row:

process PlaceOrderSaga {
dist reversible on error {
Customer > Order.create unwind Customer > Order.cancel
Order > TicketInventory.reserve unwind Order > TicketInventory.release
TicketInventory > Payment.charge unwind TicketInventory > Payment.refund
Payment > Badge.issue // terminal grant — nothing after to undo
}
}
view PlaceOrderSagaOps {
"Compensation flow, laned by participant — the refund chain reads under
its forward steps regardless of which lane is pooled"
flow PlaceOrderSaga bpmn {
lane Customer
lane Order
// TicketInventory and Payment stay unclaimed — the default lane
}
}

A choreographed saga (dist reversible) has no single coordinator — each participant is its own lane, Customer and Order claimed explicitly here, TicketInventory and Payment falling to the default lane. Whichever pool a lane joins, the compensation band underneath it stays put: a refund chain crossing team lines is exactly the case a laned diagram exists to make visible, so the render never hides it inside one team’s row.

Everything about flow from Chapter 8 still holds. show/hide compose only for the side panel and context — the canvas always renders the whole referenced process, never a filtered slice of it. style/lens target what’s actually in the selector universe: participating modules and interfaces, and the derived edges — never steps, which aren’t model elements and so aren’t stylable. Persistent commentary on a step (a note, a call comment) lives on the process itself, versioned with it, not on the view. on <plane> is a targeted diagnostic in a flow view — a process already carries its own plane, so there’s nothing for on to set. pin is likewise a diagnostic (layout is flow-determined); rename still applies.

The bpmn body is legal only under the bpmn token — written on plain or sequence flow, it’s inert with a targeted diagnostic. Plain and sequence lanes are callee identity (who’s on which side of a call), a different axis from the performer bands bpmn builds.

A declared flow view — one with a name, like CheckoutOps above — renders by name from every headless surface, the same resolver the live tab uses:

  • MCP: arch_render with viewName: "CheckoutOps".
  • Server: GET /api/view/flow.svg?view=CheckoutOps (or .png).
  • CLI: archlang render --out=checkout-ops.svg --view-name=CheckoutOps.

A knobbed view composes the same way an instance does anywhere else (Chapter 8) — bind $target to a process, and the flow ref and its bpmn body both read the knob:

service Orders {}
service Payments {}
process Checkout {
Customer > Orders.createOrder
Orders > Payments.authorize
}
view TeamOps {
knob process target
knob module actor
flow $target bpmn {
lane $actor
}
}
view TeamOps PaymentsOps {
target: Checkout
actor: Payments
}

knob process target is the parameterized-walkthrough idiom — one flow $target view, instantiated once per process it’s asked to render; lane $actor binds an element-kind knob straight into the lane selector slot. TeamOps PaymentsOps ships pre-bound: --view-name=PaymentsOps renders Checkout, laned around Payments, with nothing left to pick.

  • One process, three modes: plain (flow walkthrough), sequence (UML lifelines), bpmn (laned collaboration) — flow <ProcessRef> [sequence | bpmn]. Mode is presentation truth; the process underneath never changes.
  • The bpmn body sorts performers into lanes (lane <selector> per-match, lane "Title": <selector> merged, lane by <getter> auto) and groups lanes into pools (pool <Ref> sugar, pool "Title": <selector> query, pool by <getter> auto) — a lane joins a pool exactly when every one of its performers matches the pool’s query.
  • Claims are first-wins in document order; the implicit default lane catches whatever’s unclaimed and shows only when non-empty; persona actors (owners absent from the model) are claimable only by bare ref, never by aspect or pool query.
  • Compensation bands from a reversible saga render globally, under every lane, regardless of how the lanes pool.
  • A flow view is still a view: show/hide scope the side panel only, style/lens target modules and edges (never steps), and on/pin are diagnostics — the process carries its own plane and layout.
  • A declared flow view renders headlessly by name — MCP viewName, the server’s /api/view/flow.svg|png?view=, the CLI’s --view-name — and a knobbed instance ships pre-bound.

Chapter 34: Latency Analysis → — the same process graph, folded into a system/business time estimate.