Powered by AppSignal & Oban Pro

Choreo Sequence Diagrams: Walkthrough

sequence_walkthrough.livemd

Choreo Sequence Diagrams: Walkthrough

Mix.install([
  {:choreo, "~> 0.14.1"},
  # {:choreo, path: Path.expand("../..", __DIR__), force: true},
  {:kino, "~> 0.19.0"},
  {:kino_vizjs, "~> 0.9.0"}
])

Section

Rendering diagrams: This livebook uses Choreo.Lab.Siren to render Mermaid sequence diagrams inline. Sequence diagrams are Mermaid's native strength; Choreo also provides a best-effort DOT renderer for static image or PDF pipelines.


What is a Sequence Diagram?

A sequence diagram shows how participants (actors, systems, components) interact with each other over time. It is one of the most effective ways to document:

  • API call flows
  • Authentication handshakes
  • Database transactions
  • Event-driven choreography
  • Error handling and retry paths

Unlike other Choreo modules, Choreo.Sequence leans heavily into Mermaid's native sequenceDiagram syntax. GraphViz has no native sequence-diagram concept, so the DOT renderer produces a readable, timeline-style approximation rather than a formal UML sequence layout.

Choreo provides two ways to work with sequence diagrams:

  1. Programmatic Pipe API (Choreo.Sequence) — A stable, pipe-first interface ideal for dynamic builders, tracing middleware, and automated log conversion.
  2. Lab DSL (Choreo.Lab.DSL.Sequence) — A concise, Livebook-friendly syntax for sketching event choreography, API handshakes, and failure modes.

The introductory example below uses the explicit pipe-first syntax. All subsequent examples throughout this guide demonstrate the Lab DSL.


Building a Basic Sequence

alias Choreo.Sequence
alias Choreo.Sequence.Analysis
import Choreo.Lab.DSL.Sequence

# Initialize a basic login sequence using pipe syntax
login =
  Sequence.new()
  |> Sequence.add_actor(:user, label: "User")
  |> Sequence.add_participant(:web, label: "Web App")
  |> Sequence.add_participant(:api, label: "API")
  |> Sequence.add_participant(:db, label: "Database")
  |> Sequence.message(:user, :web, label: "Enter credentials")
  |> Sequence.message(:web, :api, label: "POST /login")
  |> Sequence.activate(:api)
  |> Sequence.message(:api, :db, label: "SELECT user_hash")
  |> Sequence.return(:db, :api, label: "hash + salt")
  |> Sequence.message(:api, :api, label: "Verify password")
  |> Sequence.return(:api, :web, label: "200 + JWT")
  |> Sequence.deactivate(:api)
  |> Sequence.message(:web, :user, label: "Redirect to dashboard")

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(login)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(login)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(login))
)

Message Types

Choreo.Sequence supports several message styles:

Type Mermaid Arrow Use case
:sync ->> Synchronous call (default)
:async -) Asynchronous / fire-and-forget
:return -->> Response message
:self ->> Message to self (auto-detected)
messages =
  sequence do
    user = actor("User")
    api = participant("API")
    queue = participant("Queue")

    user ~> api |> call("Submit order")
    async api ~> queue, "Enqueue job"
    reply api ~> user, "Order accepted"
    queue ~> queue |> call("Retry on failure")
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(messages)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(messages)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(messages))
)

Activation Boxes

Use activate/1 and deactivate/1 in the DSL to show when a participant lifeline is actively processing.

activation =
  sequence do
    client = actor("Client")
    server = participant("Server")
    cache = participant("Cache")

    client ~> server |> call("GET /items/42")
    activate server
    server ~> cache |> call("GET items:42")
    reply cache ~> server, "cached JSON"
    reply server ~> client, "200 OK"
    deactivate server
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(activation)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(activation)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(activation))
)

Notes

Add explanatory notes anywhere in the diagram.

notes =
  sequence do
    user = actor("User")
    api = participant("API")

    user ~> api |> call("Sign up")
    over api, "Rate limit: 5 req/min"
    reply api ~> user, "201 Created"
    right user, "User receives welcome email"
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(notes)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(notes)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(notes))
)

Note positions in Lab DSL:

  • over participant, "text"
  • left participant, "text"
  • right participant, "text"
  • between a, b, "text"

Fragments: Loops, Alts, and Options

Use fragments to model control flow.

Loop

loop_example =
  sequence do
    user = actor("User")
    api = participant("API")

    loop "until page empty" do
      api ~> api |> call("Fetch next page")
    end

    reply api ~> user, "All pages"
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(loop_example)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(loop_example)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(loop_example))
)

Alt / Else

alt_example =
  sequence do
    user = actor("User")
    api = participant("API")

    user ~> api |> call("GET /account")

    alt "account exists" do
      reply api ~> user, "200 OK"
      otherwise "account not found"
      reply api ~> user, "404 Not Found"
    end
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(alt_example)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(alt_example)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(alt_example))
)

Optional

opt_example =
  sequence do
    user = actor("User")
    api = participant("API")

    user ~> api |> call("Place order")

    opt "promo code provided" do
      api ~> api |> call("Apply discount")
    end

    reply api ~> user, "Order confirmed"
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(opt_example)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(opt_example)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(opt_example))
)

Complete Example: OAuth2 Authorization Code Flow

oauth =
  sequence do
    user = actor("User")
    browser = participant("Browser")
    client = participant("Client App")
    auth = participant("Auth Server")
    resource = participant("Resource Server")

    # Step 1: User initiates login
    user ~> browser |> call("Click 'Sign in with OAuth'")
    browser ~> client |> call("Request authorization")
    client ~> browser |> call("Redirect to /authorize")
    browser ~> auth |> call("GET /authorize?client_id=...")

    # Step 2: User authenticates and consents
    activate auth
    auth ~> browser |> call("Render login + consent")
    browser ~> user |> call("Prompt credentials")
    user ~> browser |> call("Submit credentials")
    browser ~> auth |> call("POST consent")
    reply auth ~> browser, "Redirect with code"
    deactivate auth

    # Step 3: Exchange code for tokens
    browser ~> client |> call("Callback with code")
    activate client
    client ~> auth |> call("POST /token (code + secret)")
    reply auth ~> client, "access_token + refresh_token"
    deactivate client

    # Step 4: Use access token
    activate client
    client ~> resource |> call("GET /profile (Bearer)")
    reply resource ~> client, "User profile"
    deactivate client
    reply client ~> browser, "Render dashboard"
    browser ~> user |> call("Show dashboard")
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(Sequence.to_mermaid(oauth)),
  Graphviz: Kino.VizJS.render(Sequence.to_dot(oauth)),
  Sketch: Choreo.Lab.Sketch.new(Sequence.to_mermaid(oauth))
)

Analysis

Choreo.Sequence includes analysis helpers to catch common diagram problems.

problematic =
  sequence do
    user = actor("User")
    api = participant("API")
    _ghost = participant("Ghost Service", id: :ghost)

    user ~> api
    activate api
  end

Analysis.validate(problematic)

Available checks:

  • missing_labels/1 — messages without labels
  • unknown_participants/1 — messages referencing undefined participants
  • isolated_participants/1 — participants with no messages
  • unbalanced_activations/1 — activate without deactivate, or vice versa
  • unclosed_fragments/1 — fragments opened but never ended
  • validate/1 — runs all checks and returns a list of {severity, message} tuples

DOT Fallback

If you need a static image, PDF, or GraphViz pipeline, use to_dot/2. It renders a timeline-style digraph rather than a formal sequence diagram.

Sequence.to_dot(login) |> IO.puts()

Cheat Sheet

Lab DSL Syntax

Syntax Description
sequence do ... end Define a sequence diagram
user = actor("User") Declare an external actor participant
api = participant("API") Declare a system/service participant
service("Auth", id: :auth) Declare participant with custom id
`user ~> api > call("GET /items")`
`api ~> worker > async("process")`
reply db ~> api, "rows" Return / response message
`api ~> api > call("verify")`
edge api ~> db, "query", type: :sync Explicit edge with type option
activate api / deactivate api Activation lifeline boxes
over api, "Validates token" Note centered over a participant
left user, "External caller" Note to the left of a participant
right api, "Logs request" Note to the right of a participant
between api, db, "mTLS connection" Note spanning between two participants
loop "label" do ... end Repeated loop fragment
opt "condition" do ... end Optional fragment
alt "condition" do ... otherwise "else" ... end Alternative branching fragment
par "parallel tasks" do ... end Concurrent / parallel fragment
critical "atomic step" do ... end Critical region fragment

Programmatic Pipe API & Analysis

Task / Feature Command
Create Sequence Diagram Sequence.new/1
Add Actor Sequence.add_actor/3 (Opts: :label, :description)
Add Participant Sequence.add_participant/3 (Opts: :label, :description)
Synchronous Message Sequence.message/4 (Opts: :label, type: :sync)
Asynchronous Message Sequence.async/4 (Opts: :label)
Return Message Sequence.return/4 (Opts: :label)
Self Message Sequence.self_message/3 (Opts: :label)
Lifeline Activation Sequence.activate/2, Sequence.deactivate/2
Add Note Sequence.note/3 ({:over, id}, {:left, id}, {:between, a, b})
Start Fragment Sequence.fragment/3 (:loop, :opt, :alt, :else, :par, etc.)
End Fragment Sequence.end_fragment/1
Render Native Mermaid Sequence.to_mermaid/2
Render Timeline DOT Graphviz Sequence.to_dot/2 (Opts: :theme)
Theme Helper Sequence.theme/2 (:default, :dark, :ocean, :warm, :forest)
Full Integrity Validation Analysis.validate/1
Find Missing Labels Analysis.missing_labels/1
Find Unknown Participants Analysis.unknown_participants/1
Find Isolated Participants Analysis.isolated_participants/1
Find Unbalanced Activations Analysis.unbalanced_activations/1
Find Unclosed Fragments Analysis.unclosed_fragments/1