Chapter 20 — Workflow Visualization
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 visualization helpers that turn any Workflow object into Mermaid (renders inline in GitHub markdown, issues, and PRs) or Graphviz DOT (for architecture diagrams, wikis, runbooks). Both are deterministic — the same graph always produces the same bytes — so you can commit the output and diff real changes in a PR instead of eyeballing a screenshot.
This isn’t just a tutorial exercise: the same pattern regenerates the diagrams for every production workflow spec in this repo (see How this shows up in the capstone), and a live variant of it drives the orchestration graph you see in the web UI while a run is in progress.
Prerequisites
- Completed Chapter 19 — Declarative Workflows
- No LLM calls in this chapter — no API key required, it’s pure graph rendering
- Optional:
graphvizinstalled locally if you want to rasterize the.dotoutput to PNG/SVG via thedotCLI
The concept
WorkflowViz (Python) and the Workflow extension methods (.NET) walk the executor graph you built with WorkflowBuilder/WorkflowBuilder<T> and serialize it to two formats:
- Mermaid — a
flowchartblock that GitHub renders inline, no extra tooling needed. - Graphviz DOT — a
digraphyou pipe through thedotCLI to get PNG/SVG for docs or wikis.
Both are derived purely from the graph’s structure (executor ids and edges), not from any specific run — so the diagram represents every possible path through the workflow, not just the one a particular input happened to take.
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor': '#2563eb',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#1e40af',
'lineColor': '#64748b',
'secondaryColor': '#f59e0b',
'tertiaryColor': '#10b981',
'background': 'transparent'
}}}%%
flowchart LR
accTitle: The concept
classDef core fill:#2563eb,stroke:#1e40af,color:#ffffff
classDef success fill:#10b981,stroke:#047857,color:#ffffff
uppercase["uppercase (Start)"]
validate["validate"]
log["log"]
uppercase --> validate
validate --> log
class uppercase core
class validate core
class log success
This is the actual demo-pipeline workflow rendered by this chapter’s main.py — three executors, two edges, one deterministic diagram.
Python
Run from the repo root using the shared tutorials/ uv project (one uv sync covers every chapter):
uv sync --project tutorials
uv run --project tutorials python tutorials/20-visualization/python/main.py
# writes workflow.mmd + workflow.dot into tutorials/20-visualization/python/
Source: python/main.py. The workflow is the same three-executor pipeline used throughout the earlier workflow chapters — uppercase, then a non-empty gate, then a logger — built with the normal WorkflowBuilder:
from agent_framework._workflows._viz import WorkflowViz
from agent_framework._workflows._workflow_builder import WorkflowBuilder
def build_workflow():
up = UppercaseExecutor()
validate = ValidateExecutor()
log = LogExecutor()
return (
WorkflowBuilder(start_executor=up, name="demo-pipeline")
.add_edge(up, validate)
.add_edge(validate, log)
.build()
)
def render_mermaid() -> str:
return WorkflowViz(build_workflow()).to_mermaid()
def render_dot() -> str:
return WorkflowViz(build_workflow()).to_digraph()
main.py writes both outputs to disk next to itself. The imports come from agent_framework._workflows._viz and ._workflow_builder — underscore-prefixed internal modules, not the public top-level package; that’s the real import path this MAF version exposes for visualization today.
Rendered Mermaid (this is workflow.mmd, byte-for-byte):
flowchart TD
uppercase["uppercase (Start)"];
validate["validate"];
log["log"];
uppercase --> validate;
validate --> log;
Rendered DOT (this is workflow.dot, byte-for-byte):
digraph Workflow {
rankdir=TD;
node [shape=box, style=filled, fillcolor=lightblue];
edge [color=black, arrowhead=vee];
"uppercase" [fillcolor=lightgreen, label="uppercase\n(Start)"];
"validate" [label="validate"];
"log" [label="log"];
"uppercase" -> "validate";
"validate" -> "log";
}
.NET
cd tutorials/20-visualization/dotnet
dotnet run
dotnet/Program.cs is a reference scaffold, not a runnable equivalent — it prints the .NET API surface rather than building and rendering a workflow itself, since Python is the canonical runnable example for this chapter. The equivalent .NET calls:
using Microsoft.Agents.AI.Workflows;
string mermaid = workflow.ToMermaidString();
string dot = workflow.ToDotString();
File.WriteAllText("workflow.mmd", mermaid);
File.WriteAllText("workflow.dot", dot);
Side-by-side differences
| Aspect | Python | .NET |
|---|---|---|
| Mermaid | WorkflowViz(workflow).to_mermaid() | workflow.ToMermaidString() |
| DOT | WorkflowViz(workflow).to_digraph() | workflow.ToDotString() |
| Import surface | Internal module (agent_framework._workflows._viz) | Public extension methods on Workflow |
| Bitmap export | Pipe .dot text through the dot CLI | Pipe .dot text through the dot CLI (same approach both languages) |
Gotchas
- Node IDs must be unique. Two executors sharing an
idfail atWorkflowBuilder.build()time (e.g., twoValidateExecutor()instances both defaulting toid="validate"), not at visualization time — the diagram only renders once the build already succeeded, so a visualization bug is rarely actually a visualization bug. - Mermaid is GitHub-native, DOT needs Graphviz. Commit
.mmdfiles and GitHub renders them inline in issues, PRs, and wikis with zero extra tooling. The.dottext is portable, but turning it into PNG/SVG requires a local or CI install ofgraphviz. - Determinism depends on your builder, not just the renderer.
WorkflowVizrenders whatever edge order the graph gives it. If your own code adds edges by iterating asetordictwithout a stable order, the rendered output can shuffle between runs even though the logical graph didn’t change — iterate over ordered collections (lists, tuples) when building the graph. - The MAF v1.0 empty-
__init__.pypackaging bug is fixed upstream, but this chapter still patches defensively.tutorials/_shared/maf_bootstrap.py::bootstrap()re-exports the public API intoagent_framework/__init__.pyif it’s empty (or carries an older bootstrap patch marker) before any tutorial imports the package — every chapter’smain.pyand tests call it first. This is distinct fromagents/python/patch_maf.py, which is the production app’s copy of the same defensive fix; both are effectively no-ops against the pinned 1.14.0 wheel (which ships a real__init__.py), but are left in place rather than removed. There is noshared/maf.py.
Tests
python/tests/test_visualization.py covers, without any LLM call:
- Mermaid output is non-empty and starts with the
flowchartdirective - All three executor ids and both edges appear in the Mermaid output
- Mermaid rendering is deterministic (
render_mermaid() == render_mermaid()) - DOT output starts with
digraphand references every node - DOT rendering is deterministic
build_workflow()succeeds
uv run --project tutorials pytest tutorials/20-visualization/python/tests -v
How this shows up in the capstone
Two complementary mechanisms, one static and one live:
- Static, build-time.
scripts/visualize_workflows.pywalks every workflow spec underagents/python/config/workflows/*.yaml, loads each viashared.workflow_loader.load_workflows_directory, and renders it with the exact sameWorkflowVizAPI this chapter teaches (scripts/visualize_workflows.py:36). It writesdocs/workflows/{name}.mmdand{name}.dot, and its--checkflag fails CI on drift — missing files, content that no longer matches the spec, or orphaned output with no matching spec — whenWORKFLOW_VISUALIZATION_ON_BUILD=true(scripts/visualize_workflows.py:71). Today it renders one workflow,text-pipeline(docs/workflows/text-pipeline.mmd), with production workflows (return-replace,pre-purchase) documented as landing later. - Live, runtime.
web/src/components/chat/orchestration-graph.tsxfetches a mode’s staticgraph_mermaid()output fromGET /api/orchestration/modes/{name}/graph(agents/python/orchestrator/routes/orchestration.py:63) and re-renders it client-side with the house Mermaid palette, then overlays live state as SSEnodeevents arrive during a run — active, done, and errored executors get different node classes (web/src/components/chat/orchestration-graph.tsx:20). Correlating a livenode_idto a diagram node relies on a deliberate backend convention: every mode’sgraph_mermaid()uses the real executor id with dashes swapped for underscores as the Mermaid node id, documented onPrePurchaseMode.graph_mermaid()inagents/python/orchestrator/modes/workflow_mode.py:164. This is a different code path fromvisualize_workflows.py— one renders a fixed spec at build time and diffs it in CI, the other renders a mode’s fixed topology at request time and animates it against a live run.
What’s next
- Next chapter: Chapter 20b — DevUI
- Full source:
python/·dotnet/ - Shared: Mermaid style guide
Source: tutorials/20-visualization/README.md — this page is generated from the repository.