Skip to main content

MAF v1 — Workflow Visualization (Python + .NET)

Nitin Kumar Singh
Author
Nitin Kumar Singh
I build enterprise AI solutions and cloud-native systems. I write about architecture patterns, AI agents, Azure, and modern development practices — with full source code.
MAF v1 — Workflow Visualization (Python + .NET)
Table of Contents
MAF v1: Python and .NET - This article is part of a series.
Part 20: This Article

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 _FanOutExecutor to _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
#

The concept
#

Two representations, one purpose:

MAF produces both from the same Workflow object. You never re-describe your graph for the sake of a picture.

%%{init: {'theme':'base', 'themeVariables': { 'primaryColor': '#2563eb','primaryTextColor': '#ffffff','primaryBorderColor': '#1e40af', 'lineColor': '#64748b','secondaryColor': '#f59e0b','tertiaryColor': '#10b981', 'background': 'transparent'}}}%% flowchart LR classDef core fill:#2563eb,stroke:#1e40af,color:#ffffff classDef external fill:#f59e0b,stroke:#b45309,color:#000000 classDef success fill:#10b981,stroke:#047857,color:#ffffff classDef infra fill:#64748b,stroke:#334155,color:#ffffff wf["Workflow object
(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 .dot language used by the Graphviz toolkit since 1991. Rasterized via dot -Tsvg input.dot -o out.svg.
  • flowchart directive — 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)
#

flowchart TD uppercase["uppercase (Start)"]; validate["validate"]; log["log"]; uppercase --> validate; validate --> log;

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):

flowchart LR user["start"] reviews["ReviewsAgent"] stock["StockExecutor"] price["PriceHistoryAgent"] shipping["ShippingExecutor"] synth["SynthesizeExecutor"] fanout{{fan-out}} fanin{{fan-in}} user --> fanout fanout --> reviews fanout --> stock fanout --> price reviews --> fanin stock --> shipping shipping --> fanin price --> fanin fanin --> synth

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.mmd

Side-by-side differences
#

AspectPython.NET
Mermaid outputWorkflowViz(wf).to_mermaid()wf.ToMermaidString()
DOT outputWorkflowViz(wf).to_digraph()wf.ToDotString()
PNG exportWorkflowViz(wf).save_png(path) (needs graphviz)Pipe ToDotString() through the dot CLI
API shapeWrapper classExtension methods on Workflow
Colour customisationSubclass or post-processPost-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:

  1. Developer edits workflows/foo.py (adds an executor).
  2. Locally runs scripts/visualize_workflows.py, which writes docs/workflows/foo.mmd and foo.dot.
  3. Commits all three files together in one PR.
  4. 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 .dot text is portable; producing PNG/SVG requires graphviz locally or in CI.
  • Determinism isn’t free. If your builder adds edges in a non-deterministic order (e.g., iterating a set in Python or a HashSet<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=LR makes them easier to read than TD.

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 test

The 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

Microsoft Agent Framework docs

Supporting tools

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

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.

MAF v1: Python and .NET - This article is part of a series.
Part 20: This Article

Related