Choreo is an Elixir visual modeling library. Write declarative, domain-specific graph definitions and render diagrams using Mermaid or Graphviz, backed by semantic static analysis.
Each tab shows the Elixir declaration above its rendered diagram output.
alias Choreo.FSM
fsm =
FSM.new()
|> FSM.add_initial_state(:idle)
|> FSM.add_state(:processing)
|> FSM.add_state(:awaiting_payment)
|> FSM.add_state(:cancelled)
|> FSM.add_final_state(:completed)
|> FSM.add_transition(:idle, :processing, label: "submit_order")
|> FSM.add_transition(:processing, :awaiting_payment, label: "invoice_generated")
|> FSM.add_transition(:awaiting_payment, :completed, label: "payment_received")
|> FSM.add_transition(:awaiting_payment, :cancelled, label: "payment_timeout")
|> FSM.add_transition(:processing, :cancelled, label: "cancel")
# Static analysis
FSM.unreachable_states(fsm) # => []
FSM.dead_end_states(fsm) # => [:cancelled]
FSM.shortest_path(fsm, :idle, :completed)
alias Choreo.Sequence
seq =
Sequence.new()
|> Sequence.add_actor(:user, label: "User")
|> Sequence.add_participant(:frontend, label: "Frontend")
|> Sequence.add_participant(:api, label: "API Gateway")
|> Sequence.add_participant(:payment, label: "Payment Service")
|> Sequence.add_participant(:db, label: "Database")
|> Sequence.message(:user, :frontend, label: "Click checkout")
|> Sequence.message(:frontend, :api, label: "POST /orders")
|> Sequence.activate(:api)
|> Sequence.message(:api, :payment, label: "charge_card(amount)")
|> Sequence.activate(:payment)
|> Sequence.return(:payment, :api, label: "charge_id")
|> Sequence.deactivate(:payment)
|> Sequence.message(:api, :db, label: "INSERT order")
|> Sequence.return(:db, :api, label: "order_id")
|> Sequence.deactivate(:api)
|> Sequence.return(:api, :frontend, label: "201 Created")
|> Sequence.return(:frontend, :user, label: "Order confirmed ✓")
alias Choreo.Workflow
workflow =
Workflow.new()
|> Workflow.add_start(:order_received)
|> Workflow.add_task(:validate, label: "Validate Order", timeout_ms: 1000)
|> Workflow.add_task(:reserve_stock, label: "Reserve Stock", timeout_ms: 3000)
|> Workflow.add_task(:charge, label: "Charge Card", timeout_ms: 5000)
|> Workflow.add_task(:ship, label: "Ship Order", timeout_ms: 10_000)
|> Workflow.add_compensation(:release_stock, for: :reserve_stock)
|> Workflow.add_compensation(:refund, for: :charge)
|> Workflow.add_end(:fulfilled)
|> Workflow.connect(:order_received, :validate)
|> Workflow.connect(:validate, :reserve_stock)
|> Workflow.connect(:reserve_stock, :charge)
|> Workflow.connect(:charge, :ship)
|> Workflow.connect(:ship, :fulfilled)
# Analysis
Workflow.critical_path(workflow) # latency bottlenecks
Workflow.missing_compensations(workflow)
alias Choreo.ERD
erd =
ERD.new()
|> ERD.add_table(:users)
|> ERD.add_column(:users, :id, :uuid, primary_key: true)
|> ERD.add_column(:users, :email, :string, nullable: false)
|> ERD.add_column(:users, :name, :string)
|> ERD.add_table(:orders)
|> ERD.add_column(:orders, :id, :uuid, primary_key: true)
|> ERD.add_column(:orders, :user_id, :uuid, foreign_key: {:users, :id})
|> ERD.add_column(:orders, :total, :decimal, nullable: false)
|> ERD.add_table(:line_items)
|> ERD.add_column(:line_items, :id, :uuid, primary_key: true)
|> ERD.add_column(:line_items, :order_id, :uuid, foreign_key: {:orders, :id})
|> ERD.add_column(:line_items, :product, :string)
|> ERD.add_column(:line_items, :qty, :integer)
|> ERD.connect(:users, :orders, label: "places", relationship: :one_to_many)
|> ERD.connect(:orders, :line_items, label: "contains", relationship: :one_to_many)
alias Choreo.UML
uml =
UML.new()
|> UML.add_class(:account, label: "Account",
fields: ["id: UUID", "email: String", "role: atom"],
methods: ["authenticate/2", "deactivate/1"])
|> UML.add_class(:profile, label: "Profile",
fields: ["avatar_url: String", "bio: String"])
|> UML.add_class(:session, label: "Session",
fields: ["token: String", "expires_at: DateTime"])
|> UML.add_class(:audit_log, label: "AuditLog",
fields: ["action: String", "timestamp: DateTime"])
|> UML.connect(:account, :profile, label: "has_one", type: :composition)
|> UML.connect(:account, :session, label: "has_many", type: :aggregation)
|> UML.connect(:account, :audit_log, label: "generates", type: :dependency)
# Analysis
UML.circular_associations(uml) # => []
alias Choreo.C4
system =
C4.new("E-Commerce Platform")
|> C4.add_person(:customer, label: "Customer")
|> C4.add_person(:admin, label: "Admin")
|> C4.add_system(:shop_app, label: "Shop Application", boundary: :internal)
|> C4.add_system(:payment_gw, label: "Payment Gateway", boundary: :external)
|> C4.add_system(:email_svc, label: "Email Service", boundary: :external)
|> C4.add_container(:api, label: "REST API", tech: "Phoenix/Elixir")
|> C4.add_container(:web, label: "Web UI", tech: "LiveView")
|> C4.add_container(:db, label: "Database", tech: "PostgreSQL")
|> C4.connect(:customer, :web, label: "Browses")
|> C4.connect(:web, :api, label: "HTTP/JSON")
|> C4.connect(:api, :db, label: "Ecto queries")
|> C4.connect(:api, :payment_gw, label: "Charge card")
|> C4.connect(:api, :email_svc, label: "Send receipt")
|> C4.connect(:admin, :api, label: "Manages")
# Analysis
C4.shared_databases(system) # detect coupling
C4.external_interfaces(system)
alias Choreo.MindMap
map =
MindMap.new()
|> MindMap.set_root(:elixir, label: "Elixir")
|> MindMap.add_topic(:concurrency, label: "Concurrency")
|> MindMap.add_topic(:ecosystem, label: "Ecosystem")
|> MindMap.add_topic(:tooling, label: "Tooling")
|> MindMap.add_subtopic(:processes, label: "Processes", under: :concurrency)
|> MindMap.add_subtopic(:genservers, label: "GenServers", under: :concurrency)
|> MindMap.add_subtopic(:beam, label: "BEAM VM", under: :ecosystem)
|> MindMap.add_subtopic(:hex, label: "Hex.pm", under: :ecosystem)
|> MindMap.add_subtopic(:mix, label: "Mix", under: :tooling)
|> MindMap.add_subtopic(:livebook, label: "Livebook", under: :tooling)
|> MindMap.branch(:elixir, :concurrency)
|> MindMap.branch(:elixir, :ecosystem)
|> MindMap.branch(:elixir, :tooling)
alias Choreo.Planner
project =
Planner.new("Launch v1.0")
|> Planner.add_milestone(:design_complete, title: "Design Complete")
|> Planner.add_milestone(:beta_release, title: "Beta Release")
|> Planner.add_task(:wireframes, title: "Wireframes", status: :done, estimate_hours: 12)
|> Planner.add_task(:ui_design, title: "UI Design", status: :done, estimate_hours: 20)
|> Planner.add_task(:api_impl, title: "API Impl", status: :in_progress, estimate_hours: 40)
|> Planner.add_task(:frontend, title: "Frontend", status: :in_progress, estimate_hours: 32)
|> Planner.add_task(:testing, title: "QA Testing", status: :backlog, estimate_hours: 16)
|> Planner.add_task(:deploy, title: "Deploy", status: :backlog, estimate_hours: 8)
|> Planner.depends_on(:frontend, :ui_design)
|> Planner.depends_on(:api_impl, :wireframes)
|> Planner.depends_on(:testing, :api_impl)
|> Planner.depends_on(:testing, :frontend)
|> Planner.depends_on(:deploy, :testing)
# Analysis
Planner.critical_path(project) # longest dependency chain
Planner.ready_to_start(project) # tasks with all deps met
alias Choreo.ThreatModel
tm =
ThreatModel.new()
|> ThreatModel.add_actor(:user, label: "End User")
|> ThreatModel.add_actor(:attacker, label: "Attacker", external: true)
|> ThreatModel.add_process(:web_server, label: "Web Server")
|> ThreatModel.add_process(:auth_service, label: "Auth Service")
|> ThreatModel.add_datastore(:user_db, label: "User DB", sensitive: true)
|> ThreatModel.add_datastore(:session_cache, label: "Redis Cache")
|> ThreatModel.add_trust_boundary(:internet, label: "Internet Boundary")
|> ThreatModel.add_trust_boundary(:dmz, label: "DMZ")
|> ThreatModel.connect(:user, :web_server, label: "HTTPS/443")
|> ThreatModel.connect(:web_server, :auth_service, label: "gRPC")
|> ThreatModel.connect(:auth_service, :user_db, label: "SQL")
|> ThreatModel.connect(:auth_service, :session_cache, label: "SET/GET")
ThreatModel.cross_boundary_flows(tm) # flows crossing trust zones
ThreatModel.sensitive_assets(tm) # datastores marked sensitive
alias Choreo.Dependency
deps =
Dependency.new()
|> Dependency.add_module(:core, label: "Core Business", layer: :domain)
|> Dependency.add_module(:accounts, label: "Accounts", layer: :domain)
|> Dependency.add_module(:orders, label: "Orders", layer: :domain)
|> Dependency.add_module(:auth, label: "Auth", layer: :application)
|> Dependency.add_module(:repo, label: "Repo", layer: :infrastructure)
|> Dependency.add_module(:mailer, label: "Mailer", layer: :infrastructure)
|> Dependency.depends_on(:accounts, :core)
|> Dependency.depends_on(:orders, :core)
|> Dependency.depends_on(:orders, :accounts)
|> Dependency.depends_on(:auth, :accounts)
|> Dependency.depends_on(:repo, :core)
|> Dependency.depends_on(:mailer, :orders)
# Analysis
Dependency.cyclic_imports(deps) # => []
Dependency.impact_of_change(deps, :core) # all dependents
alias Choreo.Dataflow
pipeline =
Dataflow.new()
|> Dataflow.add_source(:kafka, label: "Kafka Topic")
|> Dataflow.add_source(:webhook, label: "Webhook")
|> Dataflow.add_transform(:router, label: "Event Router")
|> Dataflow.add_transform(:parser, label: "JSON Parser")
|> Dataflow.add_transform(:enricher, label: "Enricher")
|> Dataflow.add_transform(:validator, label: "Schema Validator")
|> Dataflow.add_sink(:postgres, label: "Postgres")
|> Dataflow.add_sink(:s3, label: "S3 Archive")
|> Dataflow.add_sink(:dlq, label: "Dead Letter Queue")
|> Dataflow.connect(:kafka, :router)
|> Dataflow.connect(:webhook, :router)
|> Dataflow.connect(:router, :parser)
|> Dataflow.connect(:parser, :enricher)
|> Dataflow.connect(:enricher, :validator)
|> Dataflow.connect(:validator, :postgres)
|> Dataflow.connect(:validator, :s3)
|> Dataflow.connect(:validator, :dlq, label: "on_error")
Dataflow.bottlenecks(pipeline) # single-input sinks
Dataflow.strictly_acyclic?(pipeline)
alias Choreo.DecisionTree
tree =
DecisionTree.new()
|> DecisionTree.add_root(:is_premium, question: "Is user premium?")
|> DecisionTree.add_node(:high_usage, question: "Monthly usage > 10k req?")
|> DecisionTree.add_node(:has_discount, question: "Active discount code?")
|> DecisionTree.add_leaf(:enterprise_price, label: "Enterprise Tier", value: 499)
|> DecisionTree.add_leaf(:pro_price, label: "Pro Tier", value: 99)
|> DecisionTree.add_leaf(:discounted, label: "Discounted", value: 29)
|> DecisionTree.add_leaf(:free_tier, label: "Free Tier", value: 0)
|> DecisionTree.branch(:is_premium, true, :high_usage)
|> DecisionTree.branch(:is_premium, false, :has_discount)
|> DecisionTree.branch(:high_usage, true, :enterprise_price)
|> DecisionTree.branch(:high_usage, false, :pro_price)
|> DecisionTree.branch(:has_discount, true, :discounted)
|> DecisionTree.branch(:has_discount, false, :free_tier)
# Analysis
DecisionTree.incomplete_branches(tree) # => []
DecisionTree.reachable_leaves(tree) # all terminal outcomes
embed/4Nest any Choreo diagram — Dataflow, Workflow, C4, ERD — as a labelled cluster inside a parent system. Nodes are prefixed to avoid ID collisions; edges carry their original metadata.
# Each child diagram is a stand-alone Choreo model
ingestion =
Choreo.Dataflow.new()
|> Dataflow.add_source(:kafka, label: "Kafka")
|> Dataflow.add_transform(:router, label: "Router")
|> Dataflow.add_sink(:db, label: "DB Writer")
|> Dataflow.connect(:kafka, :router)
|> Dataflow.connect(:router, :db)
orchestration =
Choreo.Workflow.new()
|> Workflow.add_start(:trigger)
|> Workflow.add_task(:process, label: "Process")
|> Workflow.add_end(:done)
|> Workflow.connect(:trigger, :process)
|> Workflow.connect(:process, :done)
platform =
Choreo.new()
# Create named cluster boundaries
|> Choreo.add_cluster("ingestion", label: "Data Ingestion")
|> Choreo.add_cluster("orchestration", label: "Orchestration")
# Embed with unique prefixes — nodes become :ing_kafka, :orc_trigger …
|> Choreo.embed(ingestion, "ingestion", prefix: "ing_")
|> Choreo.embed(orchestration, "orchestration", prefix: "orc_")
# Connect across cluster boundaries
|> Choreo.connect(:ing_db, :orc_trigger, label: "triggers")
# Render the unified diagram
Choreo.to_mermaid(platform)
Declare semantic links between nodes in different diagram schemas. Choreo.trace/4 records a typed relationship (:reads, :writes, :triggers, …) that Choreo.Analysis.Tracing can walk transitively to answer “if this table changes, what workflows break?”
alias Choreo
alias Choreo.Analysis.Tracing
# Embed ERD schema + Workflow into one parent system
platform =
Choreo.new()
|> Choreo.add_cluster("schema", label: "Data Schema")
|> Choreo.add_cluster("checkout", label: "Checkout Workflow")
|> Choreo.embed(schema, "schema", prefix: "db_")
|> Choreo.embed(checkout, "checkout", prefix: "wf_")
# Declare semantic cross-diagram traces
platform =
platform
|> Choreo.trace(:wf_auth, :db_users, type: :reads)
|> Choreo.trace(:wf_charge, :db_orders, type: :writes)
# Impact analysis — what is downstream of :db_users?
Tracing.impact_analysis(platform, :db_users)
# => [:wf_auth]
# Cross-diagram execution path
Tracing.trace_path(platform, :wf_auth, :db_orders)
# => {:ok, [:wf_auth, :wf_charge, :db_orders]}
# Render with trace arrows visible
Choreo.to_dot(platform, show_traces: true)
Traces are stored as a separate edge layer — they are invisible by default so they don’t clutter normal diagrams. Pass show_traces: true to any renderer to make them appear as dashed red arrows.
Each trace carries a :type tag (:reads, :writes, :triggers, :depends_on) that the analysis engine uses to build the dependency graph.
| Function | Returns |
|---|---|
| impact_analysis/2 | All nodes downstream of a change |
| trace_path/3 | Cross-diagram execution path |
| to_dot/2 show_traces: | Renders trace arrows in DOT / Mermaid |
Choreo.View.filter/2 keeps only nodes that match a predicate. Use it to remove internals for stakeholder decks, or isolate specific node types across any diagram module.
alias Choreo.MindMap
alias Choreo.View
map =
MindMap.new()
|> MindMap.set_root(:elixir, label: "Elixir")
|> MindMap.add_topic(:concurrency, label: "Concurrency")
|> MindMap.add_topic(:ecosystem, label: "Ecosystem")
|> MindMap.add_subtopic(:processes, label: "Processes")
|> MindMap.add_subtopic(:beam, label: "BEAM VM")
|> MindMap.add_note(:history, label: "Created 2011")
|> MindMap.branch(:elixir, :concurrency)
|> MindMap.branch(:elixir, :ecosystem)
|> MindMap.branch(:concurrency, :processes)
|> MindMap.branch(:ecosystem, :beam)
# Remove all notes for a clean stakeholder deck
clean =
View.filter(map, fn _id, data ->
data[:node_type] != :note
end)
MindMap.to_dot(clean)
Choreo.View.zoom/2 filters diagrams by module-defined zoom levels. Each type defines what level 0, 1, 2 … means — so zooming is semantic, not just geometric.
alias Choreo.MindMap
alias Choreo.View
map =
MindMap.new()
|> MindMap.set_root(:elixir, label: "Elixir")
|> MindMap.add_topic(:concurrency, label: "Concurrency")
|> MindMap.add_topic(:ecosystem, label: "Ecosystem")
|> MindMap.add_subtopic(:processes, label: "Processes")
|> MindMap.add_subtopic(:beam, label: "BEAM VM")
|> MindMap.add_note(:history, label: "Created 2011")
|> MindMap.branch(:elixir, :concurrency)
|> MindMap.branch(:elixir, :ecosystem)
|> MindMap.branch(:concurrency, :processes)
|> MindMap.branch(:ecosystem, :beam)
# Zoom out to show only root + topics
overview = View.zoom(map, level: 1)
MindMap.to_dot(overview)
Choreo acts as a query engine over domain models — detecting architectural issues without running code.
| Artifact | Modeling Purpose | Questions Answered |
|---|---|---|
| C4 Model | System Architecture | Are system boundaries clean? Which systems share databases? What external interfaces exist? |
| Dataflow | Processing Pipelines | Where are data bottlenecks? Is backpressure possible? Are processing paths strictly acyclic? |
| Decision Tree | Branching Heuristics | Are there duplicate logic paths? Is the decision tree complete? Which branches have missing leaves? |
| Dependency | Package & Module DAGs | Are there circular package imports? Which modules are impacted if a low-level module changes? |
| Domain DDD | Strategic & Tactical Design | What are aggregate boundaries? Which events trigger policies? Are there unmapped commands? |
| ERD | Database Schema | Are column keys consistent? Are there circular entity relationships? Do we have orphaned tables? |
| FSM | State Transitions | Is the state machine deterministic? Are there unreachable or dead-end states? Shortest path to done? |
| Mind Map | Concept Hierarchies | Are there disconnected subtopics? Is the structure strictly parent-child? What is the depth distribution? |
| Planner | Task Scheduling | What is the project critical path? Which tasks are ready to start? Who has overloaded assignments? |
| Sequence | Timeline Interactions | Are activation boxes balanced? Do async returns match? Is any actor blocked indefinitely? |
| Threat Model | STRIDE Security | Which flows cross trust boundaries? Where are sensitive assets stored? What is the static risk level? |
| UML Class | Software Types | Which structs reference each other? Are there circular associations or inheritance loops? |
| Workflow | Task Orchestration | What is the latency critical path? Which tasks can run in parallel? Are Saga compensations missing? |
Each model targets a specific domain design need with its own semantic graph, analysis functions, and layout generator.
Hierarchical modeling of software systems, containers, and components following the C4 layout specification.
Model dataflow processing graphs, transformation nodes, message routers, and parallel sources/sinks.
Evaluate complex branching heuristics and run entropy validation checks for logical soundness.
Analyze acyclic package dependencies, detect cyclic loops, and optimize layering schedules.
Map strategic bounded contexts, aggregates, entities, and event storming command-policy flows.
Database entity-relationship modeling supporting primary/foreign keys, types, and schema links.
Model finite state machines with deterministic transitions, dead-state reachability, and path analysis.
Organize concepts hierarchically into parent-child visual layouts representing domain trees.
Schedule tasks, resolve priority constraints, and isolate critical path dependencies.
Map sequence interactions and timelines with sync/async activations and loop/alt conditions.
Map secure boundaries, trust barriers, assets, dataflows, and calculate threat levels statically.
Generate object-oriented class structure layouts mapping structs, schemas, and associations.
Orchestrate tasks, parallel branches, latency weights, Saga compensation hooks, and bottlenecks.
Add Choreo to your mix.exs and start building visual models of your domain.
def deps do
[
{:choreo, "~> 0.9"}
]
end