Lesson 6 of 6 · 6 min read · Governance

Flows

Flows describe end-to-end business interactions — "place an order", "open an account", "settle a trade" — as ordered sequences over the relationships you've already declared. They're how you document what actually happens at runtime, not just what's connected to what.

In this lesson
  1. Why flows exist
  2. Anatomy of a flow
  3. Transitions and direction
  4. Worked example · place an order
  5. Cross-repo flows
  6. Visualizing in the CALM Visualizer
  7. What ArchRails enforces

Why flows exist

Nodes and relationships tell you the topology — what can talk to what. They don't tell you the story: which calls happen in which order to satisfy a given business outcome. A trader places an order — what's the path through your system?

Without flows, that knowledge lives in tribal docs, Confluence pages, or nowhere. With flows, the sequence is declared in CALM, validated on every PR (so it can't reference an edge that no longer exists), and surfaced in the dashboard's visualizer for new joiners.

Flows reference, they don't redeclare. Every transition in a flow points at a relationship's unique-id — the relationship itself is declared once in the relationships array. Removing the relationship breaks the flow (and ArchRails fires AR-FLOW-001 on the PR that introduces the break).

Anatomy of a flow

Flows live in the top-level flows array of a CALM file (same level as nodes and relationships). Minimum shape:

{
  "unique-id":   "place-an-order",
  "name":        "Place an order",
  "description": "Customer submits an order via the portal, which is validated and persisted.",
  "transitions": [
    {
      "relationship-unique-id": "portal-to-orders-server",
      "sequence-number":        1,
      "direction":              "source-to-destination"
    },
    {
      "relationship-unique-id": "orders-server-to-orders-db",
      "sequence-number":        2,
      "direction":              "source-to-destination"
    }
  ]
}

Four fields:

Transitions and direction

Each transition references one relationship by its unique-id, gives it a place in the sequence, and declares which way the call goes:

relationship-unique-id   The relationship being traversed in this step.
                         Must match a relationship declared in this CALM
                         file (or, with federation, in a linked one).

sequence-number          1-indexed integer. Gaps are tolerated;
                         duplicate sequence numbers are allowed only when
                         steps happen in parallel.

direction                One of:
                         "source-to-destination" — call goes the way the
                                                  relationship was declared.
                         "destination-to-source" — call goes the other way
                                                  (response, callback, reverse SSE).

Most transitions are source-to-destination — you declared the relationship in the direction calls actually flow. Use destination-to-source when the same edge carries both directions (websocket, gRPC streaming, server-sent events) and the flow needs to model the reverse leg explicitly.

Worked example · place an order

Assume an architecture with these relationships already declared:

customer-clicks-portal      interacts:  customer  → customer-portal
portal-to-orders-server     connects:   customer-portal → orders-server
orders-to-risk              connects:   orders-server  → risk-engine
orders-to-db                connects:   orders-server  → orders-db
orders-to-cache             connects:   orders-server  → orders-cache

The "place an order" flow stitches them together:

{
  "unique-id":   "place-an-order",
  "name":        "Place an order",
  "description": "Customer submits, risk approves, order persists, cache primed.",
  "transitions": [
    {
      "relationship-unique-id": "customer-clicks-portal",
      "sequence-number": 1,
      "direction":       "source-to-destination"
    },
    {
      "relationship-unique-id": "portal-to-orders-server",
      "sequence-number": 2,
      "direction":       "source-to-destination"
    },
    {
      "relationship-unique-id": "orders-to-risk",
      "sequence-number": 3,
      "direction":       "source-to-destination"
    },
    {
      "relationship-unique-id": "orders-to-db",
      "sequence-number": 4,
      "direction":       "source-to-destination"
    },
    {
      "relationship-unique-id": "orders-to-cache",
      "sequence-number": 4,
      "direction":       "source-to-destination"
    }
  ]
}

Notice that the DB write and cache prime share sequence-number: 4 — they happen in parallel after risk approval. That's the model: each sequence-number is one "step," and a step can contain multiple parallel transitions.

Cross-repo flows

Most non-trivial flows cross repo boundaries — the portal is in one repo, orders in another, risk in a third. Two ways to model this:

Either way, the relationships the flow references need to be reachable through federation — either declared in this CALM file or in a federated one with a discoverable repository field on the relevant node or endpoint.

Visualizing in the CALM Visualizer

The dashboard's CALM Visualizer renders each declared flow as a numbered sequence overlay on the graph — useful for onboarding new engineers ("here's what happens when a customer places an order") and for incident review ("the page came from this hop and propagated this far"). Open the visualizer from Dashboard → CALM Visualizer and pick a flow from the side panel.

What ArchRails enforces

Flows are validated on every PR:

Flows can also carry their own controls block — useful for requirements that apply across the whole transition sequence ("every flow that touches PII must complete within 30 seconds and emit one audit log per transition"). See lesson 5 for the controls syntax.


You've completed the foundations track. Open a real PR in your own repo and watch how ArchRails reads the architecture you've just learned to author. Questions? Reach the team via the Dashboard → Contact Us button.