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.Sirento 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:
- Programmatic Pipe API (
Choreo.Sequence) — A stable, pipe-first interface ideal for dynamic builders, tracing middleware, and automated log conversion. - 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 labelsunknown_participants/1— messages referencing undefined participantsisolated_participants/1— participants with no messagesunbalanced_activations/1— activate without deactivate, or vice versaunclosed_fragments/1— fragments opened but never endedvalidate/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 |