Series note — Final advanced chapter before the wrap-up (Ch21 — Putting It All Together). After this you can commit workflow graphs to the repo and trust that diffs in PRs mean something real.
Repo — Runnable code: tutorials/20-visualization.
When you’d reach for this — A graph that lives only in code is invisible to anyone who doesn’t read C# or Python. Three concrete moments when that hurts: a PR reviewer needs to know whether a refactor changed the shape of the workflow (rename
_FanOutExecutorto_DispatchExecutor— same graph or different graph?); on-call at 3 AM you need to know which executor in a 14-node return-replace workflow handed an event to which next step; a stakeholder asks what the agent actually does in a flow chart they can paste into a deck. Mermaid for the first two, Graphviz DOT for the third. Both fall out of one line of code per workflow.
Why this chapter#
A workflow you can’t see is hard to review and impossible to reason about on-call at 3 AM. MAF ships two one-liner exporters — Mermaid (GitHub-native) and Graphviz DOT (production runbooks, wikis, Confluence) — that turn any Workflow object into a diagram. Both are deterministic, meaning the same workflow always produces the exact same bytes. That determinism is the whole point: commit the output, and the next PR that changes the graph will surface the diff exactly like a source-code diff.
Prerequisites#
- Completed Chapter 19 — Declarative Workflows
- Familiar with Chapter 09 — Workflow Executors and Edges (executors, edges,
WorkflowBuilder)
The concept#
Two representations, one purpose:
- Mermaid — text diagrams that GitHub, GitLab, Bitbucket, Obsidian, and most modern markdown renderers understand natively. Commit
.mmdfiles next to your code and they render inline in PRs, issues, READMEs, and wikis. - Graphviz DOT — the venerable
.dotformat, rasterized via thedotCLI into PNG / SVG / PDF. Better for large graphs, runbook pages, and documentation where you need pixel-perfect control.
MAF produces both from the same Workflow object. You never re-describe your graph for the sake of a picture.
(executors + edges)"] viz["WorkflowViz / ToMermaidString / ToDotString"] mmd[".mmd file
(committed to repo)"] dot[".dot file
(committed to repo)"] github["GitHub / GitLab
renders in PRs"] dotcli[Graphviz `dot` CLI] svg["SVG / PNG
(runbooks, wikis)"] ci["CI drift check
(regenerate → diff)"] wf --> viz viz --> mmd viz --> dot mmd --> github dot --> dotcli dotcli --> svg mmd -. regen .-> ci dot -. regen .-> ci class wf,viz core class mmd,dot infra class github,svg success class dotcli,ci external
One source object → two serialized forms → two rendering pipelines. The dotted lines back to “CI drift check” are what make committing the diagram worthwhile: CI regenerates the graph on every PR and fails if it doesn’t match the committed file.
Jargon recap#
- Mermaid — text-based diagram syntax (
flowchart,sequenceDiagram,stateDiagram-v2). See the Mermaid style guide for the palette this series uses. - Graphviz DOT — the
.dotlanguage used by the Graphviz toolkit since 1991. Rasterized viadot -Tsvg input.dot -o out.svg. flowchartdirective — Mermaid’s DAG syntax. First line of every flowchart declares direction (LR,TD,RL,BT).- Determinism — the exporter iterates executors and edges in insertion order, not hash order. Same graph → identical bytes on every rebuild.
Python#
Full source: python/main.py. Key lines:
from agent_framework._workflows._viz import WorkflowViz
from agent_framework._workflows._workflow_builder import WorkflowBuilder
workflow = (
WorkflowBuilder(start_executor=uppercase, name="demo-pipeline")
.add_edge(uppercase, validate)
.add_edge(validate, log)
.build()
)
mermaid = WorkflowViz(workflow).to_mermaid()
dot = WorkflowViz(workflow).to_digraph()
pathlib.Path("workflow.mmd").write_text(mermaid)
pathlib.Path("workflow.dot").write_text(dot)That’s the whole API. WorkflowViz(...) wraps the workflow once and exposes .to_mermaid(), .to_digraph(), and .save_png(path) for one-shot image export (requires graphviz on the system).
Rendered Mermaid (basic)#
Rendered DOT (basic)#
digraph Workflow {
rankdir=TD;
node [shape=box, style=filled, fillcolor=lightblue];
"uppercase" [fillcolor=lightgreen, label="uppercase\n(Start)"];
"validate" [label="validate"];
"log" [label="log"];
"uppercase" -> "validate";
"validate" -> "log";
}Note: MAF’s default node colours are lightblue / lightgreen. They look fine on most GitHub themes but don’t match this series’ palette. For production docs, post-process the DOT output — or wrap to_digraph() with a small templating pass that replaces the fill colours.
Rendered Mermaid (complex — conditional edges + agents)#
For realistic workflows with agent-executors and conditional edges, the output has the same shape but more nodes. Excerpt from a pre-purchase research workflow (Ch11 + Ch13):
Same one-liner, same file size, same determinism. The only code change is the WorkflowBuilder you pass in.
Run it:
cd tutorials/20-visualization/python
uv sync
uv run python main.py
# writes workflow.mmd + workflow.dot to cwd
cat workflow.mmd.NET#
Full source: dotnet/Program.cs. The .NET API shape matches Python:
using Microsoft.Agents.AI.Workflows;
var workflow = new WorkflowBuilder<string>()
.AddExecutor(uppercase)
.AddEdge(uppercase, validate)
.AddEdge(validate, log)
.Build("demo-pipeline");
string mermaid = workflow.ToMermaidString();
string dot = workflow.ToDotString();
File.WriteAllText("workflow.mmd", mermaid);
File.WriteAllText("workflow.dot", dot);Extension methods ToMermaidString() and ToDotString() live on Workflow. Both are pure (no I/O). You own the file write.
Run it:
cd tutorials/20-visualization/dotnet
dotnet run
cat workflow.mmdSide-by-side differences#
| Aspect | Python | .NET |
|---|---|---|
| Mermaid output | WorkflowViz(wf).to_mermaid() | wf.ToMermaidString() |
| DOT output | WorkflowViz(wf).to_digraph() | wf.ToDotString() |
| PNG export | WorkflowViz(wf).save_png(path) (needs graphviz) | Pipe ToDotString() through the dot CLI |
| API shape | Wrapper class | Extension methods on Workflow |
| Colour customisation | Subclass or post-process | Post-process string |
Why determinism matters — committing diagrams to the repo#
Because .to_mermaid() / ToMermaidString() produce byte-identical output for the same graph, you can treat the diagram file as a build artefact that lives in git. The workflow is:
- Developer edits
workflows/foo.py(adds an executor). - Locally runs
scripts/visualize_workflows.py, which writesdocs/workflows/foo.mmdandfoo.dot. - Commits all three files together in one PR.
- On CI, a drift check regenerates the diagrams from the code and runs
git diff --exit-code docs/workflows/. Non-zero means the committed diagrams don’t match the code.
Reviewers see the workflow diff in the PR rendered as a diagram (GitHub renders .mmd inline). A structural change to the graph is as visible as a typo in a function name.
This is the pattern the capstone uses — see Capstone integration below.
Gotchas#
- Node IDs must be unique. Workflows with two nodes sharing an ID fail at build time (we hit this writing this chapter — two
ValidateExecutor()instances collide on id="validate"). Visualisation renders fine once the build succeeds. - Mermaid is GitHub-native, but with caveats. GitHub’s rendered theme tracks your profile setting; if you want a diagram that reads well for both light- and dark-mode readers, use the palette from the Mermaid style guide and explicit classDefs. MAF’s default output doesn’t — it uses Mermaid defaults.
- DOT needs Graphviz to rasterize. The
.dottext is portable; producing PNG/SVG requiresgraphvizlocally or in CI. - Determinism isn’t free. If your builder adds edges in a non-deterministic order (e.g., iterating a
setin Python or aHashSet<T>in .NET), the rendered output shuffles between runs. Iterate over ordered collections. - Cycles in a handoff mesh render as cycles. That’s correct but can look messy. Mermaid handles cycles natively; DOT does too, but
rankdir=LRmakes them easier to read thanTD.
Tests#
# Python — 9 tests: non-empty output, flowchart directive, all nodes present,
# all edges present, deterministic (mermaid + dot), valid digraph header,
# build succeeds
cd tutorials/20-visualization/python
uv run pytest -v
# 9 passed
# .NET
cd tutorials/20-visualization/dotnet
dotnet testThe determinism tests are the interesting ones: they call the exporter twice and assert byte-equality.
How this shows up in the capstone#
scripts/visualize_workflows.py (part of Phase 7 refactor — see plans/refactor/13-visualization.md) iterates every registered workflow (pre-purchase, return-replace, concierge) and writes Mermaid + DOT to docs/workflows/. A GitHub Actions drift check runs the script on every PR and fails the build if committed diagrams disagree with the current code.
Further reading & links#
This chapter
- Source on GitHub: tutorials/20-visualization
- Previous: Chapter 19 — Declarative Workflows · Next: Chapter 20b — DevUI (upcoming)
Microsoft Agent Framework docs
Supporting tools
- Mermaid · Graphviz · GitHub Mermaid support
- Mermaid live editor — paste MAF output, preview, experiment.
Where it lives in the capstone
scripts/visualize_workflows.py— iterates every registered workflow.docs/workflows/*.mmd+*.dot— committed artefacts..github/workflows/workflow-drift.yml— CI drift check.
Series shared resources
- Mermaid style guide — the palette all series diagrams use.
- Jargon glossary — one-line definitions.
What’s next#
Chapter 20b — DevUI (upcoming) — a live browser dashboard for testing agents and workflows interactively. Static visualization meets interactive runtime inspection.
Then Chapter 21 — Putting it all together ties every chapter back to a specific file path in the real app.

