From 7d510e577cc4202fe30040147747002b14e20492 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bianchi Date: Wed, 26 Mar 2025 19:04:54 +0100 Subject: [PATCH 1/3] feat: add Mermaid converter (wip) Signed-off-by: Jean-Baptiste Bianchi --- examples/browser/mermaid.html | 51 +++ src/lib/graph-builder.ts | 588 +++++++++++++++++++++++++++++++++ src/lib/mermaid-converter.ts | 51 +++ src/serverless-workflow-sdk.ts | 2 + 4 files changed, 692 insertions(+) create mode 100644 examples/browser/mermaid.html create mode 100644 src/lib/graph-builder.ts create mode 100644 src/lib/mermaid-converter.ts diff --git a/examples/browser/mermaid.html b/examples/browser/mermaid.html new file mode 100644 index 00000000..f7a78c08 --- /dev/null +++ b/examples/browser/mermaid.html @@ -0,0 +1,51 @@ + + + + + + Serveless Workflow + + + + + +

+  
+  

+  
+
+
+
\ No newline at end of file
diff --git a/src/lib/graph-builder.ts b/src/lib/graph-builder.ts
new file mode 100644
index 00000000..99f5bcc9
--- /dev/null
+++ b/src/lib/graph-builder.ts
@@ -0,0 +1,588 @@
+import {
+  CallTask,
+  DoTask,
+  EmitTask,
+  ForkTask,
+  ForTask,
+  ListenTask,
+  RaiseTask,
+  RunTask,
+  SetTask,
+  SwitchTask,
+  Task,
+  TaskItem,
+  TryTask,
+  WaitTask,
+  Workflow,
+} from './generated/definitions/specification';
+
+const entrySuffix = '-entry-node';
+const exitSuffix = '-exit-node';
+
+const doReference = '/do';
+const catchReference = '/catch';
+const branchReference = '/fork/branches';
+const tryReference = '/try';
+
+/**
+ * Represents a generic within a graph.
+ * This serves as a base type for nodes, edges, and graphs.
+ */
+export type GraphElement = {
+  /** A unique identifier for this graph element. */
+  id: string;
+  /** An optional label to provide additional context or naming. */
+  label?: string | null;
+};
+
+/**
+ * Enumeration of possible node types in a graph.
+ */
+export enum GraphNodeType {
+  Root = 'root',
+  Start = 'start',
+  End = 'end',
+  Entry = 'entry',
+  Exit = 'exit',
+  Call = 'call',
+  Catch = 'catch',
+  Do = 'do',
+  Emit = 'emit',
+  For = 'for',
+  Fork = 'fork',
+  Listen = 'listen',
+  Raise = 'raise',
+  Run = 'run',
+  Set = 'set',
+  Switch = 'switch',
+  Try = 'try',
+  TryCatch = 'try-catch',
+  Wait = 'wait',
+}
+
+/**
+ * Represents a node within the graph.
+ */
+export type GraphNode = GraphElement & {
+  /** The type of the node. */
+  type: GraphNodeType;
+};
+
+/**
+ * Represents a directed edge connecting two nodes in the graph.
+ */
+export type GraphEdge = GraphElement & {
+  /** The unique identifier of the node where the edge originates. */
+  sourceId: string;
+  /** The unique identifier of the node where the edge terminates. */
+  destinationId: string;
+  /** Indicates whether to ignore rendering an end arrow. */
+  ignoreEndArrow?: boolean;
+};
+
+/**
+ * Represents a graph or a subgraph
+ */
+export type Graph = GraphNode & {
+  /** The parent graph if this is a subgraph, otherwise null. */
+  parent?: Graph | null;
+  /** A collection of nodes that belong to this graph. */
+  nodes: GraphNode[];
+  /** A collection of edges that define relationships between nodes. */
+  edges: GraphEdge[];
+  /** The entry node of the graph. */
+  entryNode: GraphNode;
+  /** The exit node of the graph. */
+  exitNode: GraphNode;
+};
+
+/**
+ * Context information used when processing tasks in a workflow graph.
+ */
+type TaskContext = {
+  workflow: Workflow;
+  graph: Graph;
+  subgraph?: Graph;
+  taskList: Map;
+  taskName?: string | null;
+  taskReference: string;
+};
+
+/**
+ * Identity information for a transition between tasks.
+ */
+type TransitionIdentity = {
+  /** Name of the transition. */
+  name: string;
+  /** Index position in the task list. */
+  index: number;
+  /** Optional reference to the associated task. */
+  task?: Task;
+};
+
+/**
+ * Enumeration of possible workflow flow directives.
+ */
+enum FlowDirective {
+  Exit = 'exit',
+  End = 'end',
+  Continue = 'continue',
+}
+
+/**
+ * Converts an array of TaskItem objects into a Map for easy lookup.
+ *
+ * @param tasksList An array of TaskItem objects.
+ * @returns A map where keys are task names and values are Task objects.
+ */
+function mapTasks(tasksList: TaskItem[] | null | undefined): Map {
+  return (tasksList || []).reduce((acc, item) => {
+    const [key, task] = Object.entries(item)[0];
+    acc.set(key, task);
+    return acc;
+  }, new Map());
+}
+
+/**
+ * Initializes a graph with default entry and exit nodes.
+ *
+ * @param type The type of the graph node.
+ * @param id Unique identifier for the graph.
+ * @param label Optional label for the graph.
+ * @param parent Optional parent graph if this is a subgraph.
+ * @returns A newly created Graph instance.
+ */
+function initGraph(
+  type: GraphNodeType,
+  id: string = 'root',
+  label: string | null | undefined = undefined,
+  parent: Graph | null | undefined = undefined,
+): Graph {
+  const entryNode: GraphNode = {
+    type: id === 'root' ? GraphNodeType.Start : GraphNodeType.Entry,
+    id: `${id}${entrySuffix}`,
+  };
+  const exitNode: GraphNode = {
+    type: id === 'root' ? GraphNodeType.End : GraphNodeType.Exit,
+    id: `${id}${exitSuffix}`,
+  };
+  return {
+    id,
+    label,
+    type,
+    parent,
+    entryNode,
+    exitNode,
+    nodes: [entryNode, exitNode],
+    edges: [],
+  };
+}
+
+/**
+ * Constructs a graph representation based on the given workflow.
+ *
+ * @param workflow The workflow to be converted into a graph structure.
+ * @returns A graph representation of the workflow.
+ */
+export function buildGraph(workflow: Workflow): Graph {
+  const graph = initGraph(GraphNodeType.Root);
+  buildTransitions(graph.entryNode, {
+    workflow,
+    graph,
+    taskList: mapTasks(workflow.do),
+    taskReference: doReference,
+  });
+  return graph;
+}
+
+/**
+ * Gets the next task to be executed in the workflow
+ * @param tasksList The list of task to resolve the next task from
+ * @param taskName The current task name, if any
+ * @param transition A specific transition, if any
+ * @returns
+ */
+function getNextTask(
+  tasksList: Map,
+  taskName: string | null | undefined = undefined,
+  transition: string | null | undefined = undefined,
+): TransitionIdentity {
+  if (!tasksList?.size) throw new Error('The task list cannot be empty. No tasks list to get the next task from.');
+  const currentTask: Task | undefined = tasksList.get(taskName || '');
+  transition = transition || currentTask?.then || '';
+  if (transition == FlowDirective.End || transition == FlowDirective.Exit) {
+    return {
+      name: transition,
+      index: -1,
+    };
+  }
+  let index: number = 0;
+  if (transition && transition != FlowDirective.Continue) {
+    index = Array.from(tasksList.keys()).indexOf(transition);
+  } else if (currentTask) {
+    index = Array.from(tasksList.values()).indexOf(currentTask) + 1;
+    if (index >= tasksList.size) {
+      return {
+        name: FlowDirective.End,
+        index: -1,
+      };
+    }
+  }
+  const taskEntry = Array.from(tasksList.entries())[index];
+  return {
+    index,
+    name: taskEntry[0],
+    task: taskEntry[1],
+  };
+}
+
+/**
+ * Builds all the possible transitions from the provided node in the provided context
+ * @param node The node to build the transitions for
+ * @param context The context in which the transitions are built
+ */
+function buildTransitions(node: GraphNode | Graph, context: TaskContext) {
+  const transitions: TransitionIdentity[] = [];
+  let nextTransition = getNextTask(context.taskList, context.taskName);
+  transitions.push(nextTransition);
+  while (nextTransition?.task?.if) {
+    nextTransition = getNextTask(context.taskList, nextTransition.name, FlowDirective.Continue);
+    transitions.push(nextTransition);
+  }
+  transitions
+    .filter(
+      (transition, index) =>
+        transitions.findIndex(
+          (t) => t.index === transition.index && t.name === transition.name && t.task === transition.task,
+        ) === index,
+    )
+    .forEach((transition) => {
+      const exitAnchor = (node as Graph).exitNode || node;
+      if (transition.index != -1) {
+        const destinationNode = buildTaskNode({
+          ...context,
+          taskReference: `${context.taskReference}/${transition.index}/${transition.name}`,
+          taskName: transition.name,
+        });
+        buildEdge(context.subgraph || context.graph, exitAnchor, (node as Graph).entryNode || destinationNode);
+      } else if (transition.name === FlowDirective.Exit) {
+        buildEdge(context.subgraph || context.graph, exitAnchor, (context.subgraph || context.graph).exitNode);
+      } else if (transition.name === FlowDirective.End) {
+        buildEdge(context.subgraph || context.graph, exitAnchor, context.graph.exitNode);
+      } else throw new Error('Invalid transition');
+    });
+}
+
+/**
+ * Builds a graph representation of a task
+ * @param context The context to build the graph/node for
+ * @returns A graph or node for the provided context
+ */
+function buildTaskNode(context: TaskContext): GraphNode | Graph {
+  const task = context.taskList.get(context.taskName!);
+  if (!task) throw new Error(`Unabled to find the task '${context.taskName}' in the current context`);
+  if (task.call) return buildCallTaskNode(task, context);
+  if (task.catch) return buildTryCatchTaskNode(task, context);
+  if (task.emit) return buildEmitTaskNode(task, context);
+  if (task.for) return buildForTaskNode(task, context);
+  if (task.fork) return buildForkTaskNode(task, context);
+  if (task.listen) return buildListenTaskNode(task, context);
+  if (task.raise) return buildRaiseTaskNode(task, context);
+  if (task.run) return buildRunTaskNode(task, context);
+  if (task.set) return buildSetTaskNode(task, context);
+  if (task.switch) return buildSwitchTaskNode(task, context);
+  if (task.wait) return buildWaitTaskNode(task, context);
+  if (task.do) return buildDoTaskNode(task, context);
+  throw new Error(`Unable to defined task type of task named '${context.taskName}'`);
+}
+
+/**
+ * Builds a graph node with the provided type and context
+ * @param type The type of the node
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided context
+ */
+function buildGenericTaskNode(type: GraphNodeType, context: TaskContext): GraphNode {
+  const node: GraphNode = {
+    type,
+    id: context.taskReference,
+    label: context.taskName,
+  };
+  (context.subgraph || context.graph).nodes.push(node);
+  buildTransitions(node, context);
+  return node;
+}
+
+/**
+ * Builds a graph node for the provided call task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildCallTaskNode(task: CallTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Call, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph for the provided do task
+ * @param task The task to build the graph for
+ * @param context The context to build the graph for
+ * @returns A graph for the provided task
+ */
+function buildDoTaskNode(task: DoTask, context: TaskContext): Graph {
+  const subgraph: Graph = initGraph(
+    GraphNodeType.Do,
+    context.taskReference,
+    context.taskName,
+    context.subgraph || context.graph,
+  );
+  const doContext: TaskContext = {
+    ...context,
+    taskList: mapTasks(task.do),
+    taskReference: context.taskReference,
+    taskName: null,
+  };
+  buildTransitions(subgraph.entryNode, doContext);
+  buildTransitions(subgraph, context);
+  return subgraph;
+}
+
+/**
+ * Builds a graph node for the provided emit task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildEmitTaskNode(task: EmitTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Emit, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph for the provided for task
+ * @param task The task to build the graph for
+ * @param context The context to build the graph for
+ * @returns A graph for the provided task
+ */
+function buildForTaskNode(task: ForTask, context: TaskContext): Graph {
+  const subgraph: Graph = initGraph(GraphNodeType.For, context.taskReference, context.taskName);
+  const forContext: TaskContext = {
+    ...context,
+    taskList: mapTasks(task.do),
+    taskReference: subgraph.id,
+    taskName: null,
+  };
+  buildTransitions(subgraph.entryNode, forContext);
+  buildTransitions(subgraph, context);
+  return subgraph;
+}
+
+/**
+ * Builds a graph for the provided fork task
+ * @param task The task to build the graph for
+ * @param context The context to build the graph for
+ * @returns A graph for the provided task
+ */
+function buildForkTaskNode(task: ForkTask, context: TaskContext): Graph {
+  const subgraph: Graph = initGraph(GraphNodeType.Fork, context.taskReference, context.taskName);
+  for (let i = 0, c = task.fork?.branches.length || 0; i < c; i++) {
+    const branchItem = task.fork?.branches[i];
+    if (!branchItem) continue;
+    const [branchName] = Object.entries(branchItem)[0];
+    const branchContext: TaskContext = {
+      ...context,
+      taskList: mapTasks([branchItem]),
+      taskReference: `${context.taskReference}${branchReference}/${i}/`,
+      taskName: branchName,
+    };
+    const branchNode = buildTaskNode(branchContext);
+    buildEdge(subgraph, subgraph.entryNode, (branchNode as Graph).entryNode || branchNode);
+    buildEdge(subgraph, (branchNode as Graph).exitNode || branchNode, subgraph.exitNode);
+  }
+  buildTransitions(subgraph, context);
+  return subgraph;
+}
+
+/**
+ * Builds a graph node for the provided listen task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildListenTaskNode(task: ListenTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Listen, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph node for the provided rasie task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildRaiseTaskNode(task: RaiseTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Raise, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph node for the provided run task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildRunTaskNode(task: RunTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Run, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph node for the provided set task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildSetTaskNode(task: SetTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Set, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds a graph node for the provided switch task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildSwitchTaskNode(task: SwitchTask, context: TaskContext): GraphNode {
+  const node: GraphNode = {
+    type: GraphNodeType.Switch,
+    id: context.taskReference,
+    label: context.taskName,
+  };
+  (context.subgraph || context.graph).nodes.push(node);
+  let hasDefaultCase = false;
+  task.switch?.forEach((switchItem) => {
+    const [, switchCase] = Object.entries(switchItem)[0];
+    if (!switchCase.when) hasDefaultCase = true;
+    const transition = getNextTask(context.taskList, context.taskName, switchCase.then);
+    const exitAnchor = (node as Graph).exitNode || node;
+    if (transition.index != -1) {
+      const destinationNode = buildTaskNode({
+        ...context,
+        taskReference: `${context.taskReference}/${transition.index}/${transition.name}`,
+        taskName: transition.name,
+      });
+      buildEdge(context.subgraph || context.graph, exitAnchor, (node as Graph).entryNode || destinationNode);
+    } else if (transition.name === FlowDirective.Exit) {
+      buildEdge(context.subgraph || context.graph, exitAnchor, (context.subgraph || context.graph).exitNode);
+    } else if (transition.name === FlowDirective.End) {
+      buildEdge(context.subgraph || context.graph, exitAnchor, context.graph.exitNode);
+    } else throw new Error('Invalid transition');
+  });
+  if (!hasDefaultCase) {
+    buildTransitions(node, context);
+  }
+  return node;
+}
+
+/**
+ * Builds a graph for the provided try/catch task
+ * @param task The task to build the graph for
+ * @param context The context to build the graph for
+ * @returns A graph for the provided task
+ */
+function buildTryCatchTaskNode(task: TryTask, context: TaskContext): Graph {
+  const containerSubgraph: Graph = initGraph(
+    GraphNodeType.TryCatch,
+    context.taskReference,
+    context.taskName,
+    context.subgraph || context.graph,
+  );
+  const trySubgraph: Graph = initGraph(
+    GraphNodeType.Try,
+    context.taskReference + tryReference,
+    context.taskName + ' (try)',
+    context.subgraph || context.graph,
+  );
+  containerSubgraph.nodes.push(trySubgraph);
+  buildEdge(containerSubgraph, containerSubgraph.entryNode, trySubgraph.entryNode);
+  const tryContext: TaskContext = {
+    ...context,
+    taskList: mapTasks(task.try),
+    taskReference: trySubgraph.id,
+    taskName: null,
+  };
+  buildTransitions(trySubgraph, tryContext);
+  if (!task.catch?.do?.length) {
+    const catchNode: GraphNode = {
+      type: GraphNodeType.Catch,
+      id: context.taskReference + catchReference,
+      label: context.taskName + ' (catch)',
+    };
+    containerSubgraph.nodes.push(catchNode);
+    buildEdge(containerSubgraph, trySubgraph.exitNode, catchNode);
+    buildEdge(containerSubgraph, catchNode, containerSubgraph.exitNode);
+  } else {
+    const catchSubgraph: Graph = initGraph(
+      GraphNodeType.Catch,
+      context.taskReference + catchReference,
+      context.taskName + ' (catch)',
+      context.subgraph || context.graph,
+    );
+    containerSubgraph.nodes.push(catchSubgraph);
+    buildEdge(containerSubgraph, trySubgraph.exitNode, catchSubgraph.entryNode);
+    const catchContext: TaskContext = {
+      ...context,
+      taskList: mapTasks(task.catch.do),
+      taskReference: catchSubgraph.id,
+      taskName: null,
+    };
+    buildTransitions(catchSubgraph, catchContext);
+    buildEdge(containerSubgraph, catchSubgraph.exitNode, containerSubgraph.exitNode);
+  }
+  return containerSubgraph;
+}
+
+/**
+ * Builds a graph node for the provided wait task
+ * @param task The task to build the graph node for
+ * @param context The context to build the graph node for
+ * @returns A graph node for the provided task
+ */
+function buildWaitTaskNode(task: WaitTask, context: TaskContext): GraphNode {
+  const node = buildGenericTaskNode(GraphNodeType.Wait, context);
+  // TODO: add some details about the task?
+  return node;
+}
+
+/**
+ * Builds an edge between two elements
+ * @param graph The graph element containing the nodes
+ * @param source The origin node
+ * @param destination The destination node
+ * @param label The edge label, if any
+ */
+function buildEdge(graph: Graph, source: GraphNode, destination: GraphNode, label: string = '') {
+  let edge = graph.edges.find((e) => e.sourceId === source.id && e.destinationId === destination.id);
+  if (edge) {
+    if (label) {
+      edge.label = edge.label + (edge.label ? ' / ' : '') + label;
+    }
+    return edge;
+  }
+  edge = {
+    label,
+    id: `${source.id}-${destination.id}-${label}`,
+    sourceId: source.id,
+    destinationId: destination.id,
+    ignoreEndArrow: destination.id.endsWith(entrySuffix) || destination.id.endsWith(exitSuffix),
+  };
+  graph.edges.push(edge);
+}
diff --git a/src/lib/mermaid-converter.ts b/src/lib/mermaid-converter.ts
new file mode 100644
index 00000000..d9ceabd5
--- /dev/null
+++ b/src/lib/mermaid-converter.ts
@@ -0,0 +1,51 @@
+import { Workflow } from './generated/definitions/specification';
+import { buildGraph, Graph, GraphEdge, GraphNode, GraphNodeType } from './graph-builder';
+
+const indent = (code: string) =>
+  code
+    .split('\n')
+    .map((line) => `    ${line}`)
+    .join('\n');
+
+function convertGraphToCode(graph: Graph): string {
+  const isRoot: boolean = graph.id === 'root';
+  const code = `${isRoot ? 'flowchart TD' : `subgraph ${graph.id} [${graph.label || graph.id}]`}
+${graph.nodes.map((node) => convertNodeToCode(node)).join('\n')}
+${graph.edges.map((edge) => convertEdgeToCode(edge)).join('\n')}
+${isRoot ? '' : 'end'}`;
+  return isRoot ? code : indent(code);
+}
+
+function convertNodeToCode(node: GraphNode | Graph): string {
+  let code = '';
+  if ((node as Graph).nodes?.length) {
+    code = convertGraphToCode(node as Graph);
+  } else {
+    code = node.id;
+    switch (node.type) {
+      case GraphNodeType.Entry:
+      case GraphNodeType.Exit:
+        code += ':::hidden';
+        break;
+      case GraphNodeType.Start:
+        code += '(( ))'; // alt '@{ shape: circle, label: " "}';
+        break;
+      case GraphNodeType.End:
+        code += '((( )))'; // alt '@{ shape: dbl-circ, label: " "}';
+        break;
+      default:
+        code += `["${node.label}"]`; // alt `@{ label: "${node.label}" }`
+    }
+  }
+  return indent(code);
+}
+
+function convertEdgeToCode(edge: GraphEdge): string {
+  const code = `${edge.sourceId}${edge.label ? `--"${edge.label}"` : ''}--${edge.ignoreEndArrow ? '-' : '>'}${edge.destinationId}`;
+  return indent(code);
+}
+
+export function convertToMermaidCode(workflow: Workflow): string {
+  const graph = buildGraph(workflow);
+  return convertGraphToCode(graph);
+}
diff --git a/src/serverless-workflow-sdk.ts b/src/serverless-workflow-sdk.ts
index 35497532..746e9869 100644
--- a/src/serverless-workflow-sdk.ts
+++ b/src/serverless-workflow-sdk.ts
@@ -2,3 +2,5 @@ export * from './lib/generated/builders';
 export * from './lib/generated/classes';
 export * from './lib/generated/definitions';
 export * from './lib/validation';
+export * from './lib/graph-builder';
+export * from './lib/mermaid-converter';

From 3dfdfb2b1510ed79a4493813a168d3484ba512f8 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste Bianchi 
Date: Thu, 27 Mar 2025 16:18:22 +0100
Subject: [PATCH 2/3] feat: add graph and MermaidJS builders

Signed-off-by: Jean-Baptiste Bianchi 
---
 README.md                     | 113 +++++++++++++++++++++++++-
 examples/browser/mermaid.html | 130 ++++++++++++++++++++++++------
 package.json                  |   2 +-
 src/lib/graph-builder.ts      | 144 +++++++++++++++++-----------------
 src/lib/mermaid-converter.ts  |  65 +++++++++++++--
 tests/graph/for.spec.ts       |  40 ++++++++++
 tests/graph/if.spec.ts        |  25 ++++++
 tests/graph/set.spec.ts       |  44 +++++++++++
 tests/mermaid/for.spec.ts     |  49 ++++++++++++
 tests/mermaid/if.spec.ts      |  33 ++++++++
 tests/mermaid/set.spec.ts     |  31 ++++++++
 11 files changed, 566 insertions(+), 110 deletions(-)
 create mode 100644 tests/graph/for.spec.ts
 create mode 100644 tests/graph/if.spec.ts
 create mode 100644 tests/graph/set.spec.ts
 create mode 100644 tests/mermaid/for.spec.ts
 create mode 100644 tests/mermaid/if.spec.ts
 create mode 100644 tests/mermaid/set.spec.ts

diff --git a/README.md b/README.md
index d12dad2a..3162ba01 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,26 @@
 ![Node CI](https://github.com/serverlessworkflow/sdk-typescript/workflows/Node%20CI/badge.svg) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/serverlessworkflow/sdk-typescript)
 
+- [Serverless Workflow Specification - TypeScript SDK](#serverless-workflow-specification---typescript-sdk)
+  - [Status](#status)
+  - [SDK Structure](#sdk-structure)
+    - [Types and Interfaces](#types-and-interfaces)
+    - [Classes](#classes)
+    - [Fluent Builders](#fluent-builders)
+    - [Validation Function](#validation-function)
+    - [Other tools](#other-tools)
+  - [Getting Started](#getting-started)
+    - [Installation](#installation)
+    - [Usage](#usage)
+      - [Create a Workflow Definition from YAML or JSON](#create-a-workflow-definition-from-yaml-or-json)
+      - [Create a Workflow Definition by Casting an Object](#create-a-workflow-definition-by-casting-an-object)
+      - [Create a Workflow Definition Using a Class Constructor](#create-a-workflow-definition-using-a-class-constructor)
+      - [Create a Workflow Definition Using the Builder API](#create-a-workflow-definition-using-the-builder-api)
+      - [Serialize a Workflow Definition to YAML or JSON](#serialize-a-workflow-definition-to-yaml-or-json)
+      - [Validate Workflow Definitions](#validate-workflow-definitions)
+      - [Generate a directed graph](#generate-a-directed-graph)
+      - [Generate a MermaidJS flowchart](#generate-a-mermaidjs-flowchart)
+    - [Building Locally](#building-locally)
+
 # Serverless Workflow Specification - TypeScript SDK
 
 This SDK provides a TypeScript API for working with the [Serverless Workflow Specification](https://github.com/serverlessworkflow/specification).
@@ -14,7 +35,7 @@ The npm [`@serverlessworkflow/sdk`](https://www.npmjs.com/package/@serverlesswor
 
 | Latest Releases | Conformance to Spec Version |
 | :---: | :---: |
-| [v1.0.0.\*](https://github.com/serverlessworkflow/sdk-typescript/releases/) | [v1.0.0](https://github.com/serverlessworkflow/specification) |
+| [v1.0.\*](https://github.com/serverlessworkflow/sdk-typescript/releases/) | [v1.0.0](https://github.com/serverlessworkflow/specification) |
 
 > [!WARNING]
 > Previous versions of the SDK were published with a typo in the scope:
@@ -56,6 +77,9 @@ The SDK includes a validation function to check if objects conform to the expect
 
 The `validate` function is directly exported and can be used as `validate('Workflow', workflowObject)`.
 
+### Other Tools
+The SDK also ships tools to build directed graph and MermaidJS flowcharts from a workflow.
+
 ## Getting Started
 
 ### Installation
@@ -223,7 +247,7 @@ Validation can be achieved in two ways: via the `validate` function or the insta
 ```typescript
 import { Classes, validate } from '@serverlessworkflow/sdk';
 
-// const workflowDefinition = ;
+const workflowDefinition = /*  */;
 try {
   if (workflowDefinition instanceof Classes.Workflow) {
     workflowDefinition.validate();
@@ -237,6 +261,91 @@ catch (ex) {
 }
 ```
 
+#### Generate a directed graph
+A [directed graph](https://en.wikipedia.org/wiki/Directed_graph) of a workflow can be generated using the `buildGraph` function:
+
+```typescript
+import { buildGraph } from '@serverlessworkflow/sdk';
+
+const workflowDefinition = {
+  document: {
+    dsl: '1.0.0',
+    name: 'using-plain-object',
+    version: '1.0.0',
+    namespace: 'default',
+  },
+  do: [
+    {
+      step1: {
+        set: {
+          variable: 'my first workflow',
+        },
+      },
+    },
+  ],
+};
+const graph = buildGraph(workflowDefinition);
+/*{
+  id: 'root',
+  type: 'root',
+  label: undefined,
+  parent: null,
+  nodes: [...], // length 3 - root entry node, step1 node, root exit node
+  edges: [...], // length 2 - entry to step1, step1 to exit
+  entryNode: {...}, // root entry node
+  exitNode: {...} // root exit node
+}*/
+```
+
+#### Generate a MermaidJS flowchart
+Generating a [MermaidJS](https://mermaid.js.org/) flowchart can be achieved in two ways: using the `convertToMermaidCode` or the legacy `MermaidDiagram` class.
+
+```typescript
+import { convertToMermaidCode, MermaidDiagram } from '@serverlessworkflow/sdk';
+
+const workflowDefinition = {
+  document: {
+    dsl: '1.0.0',
+    name: 'using-plain-object',
+    version: '1.0.0',
+    namespace: 'default',
+  },
+  do: [
+    {
+      step1: {
+        set: {
+          variable: 'my first workflow',
+        },
+      },
+    },
+  ],
+};
+const mermaidCode = convertToMermaidCode(workflowDefinition) /* or new MermaidDiagram(workflowDefinition).sourceCode() */;
+/*
+flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/step1["step1"]
+    /do/0/step1 --> root-exit-node
+    root-entry-node --> /do/0/step1
+
+
+classDef hidden display: none;
+*/
+```
+
+```mermaid
+flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/step1["step1"]
+    /do/0/step1 --> root-exit-node
+    root-entry-node --> /do/0/step1
+
+
+classDef hidden display: none;
+```
+
 ### Building Locally
 
 To build the project and run tests locally, use the following commands:
diff --git a/examples/browser/mermaid.html b/examples/browser/mermaid.html
index f7a78c08..adfa781d 100644
--- a/examples/browser/mermaid.html
+++ b/examples/browser/mermaid.html
@@ -9,41 +9,121 @@
 
 
 
+  

YAML or JSON:

+ +

   
-  

   
 
diff --git a/package.json b/package.json
index 591ffaf9..8c5380a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@serverlessworkflow/sdk",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "schemaVersion": "1.0.0",
   "description": "Typescript SDK for Serverless Workflow Specification",
   "main": "umd/index.umd.min.js",
diff --git a/src/lib/graph-builder.ts b/src/lib/graph-builder.ts
index 99f5bcc9..ef2d6c0f 100644
--- a/src/lib/graph-builder.ts
+++ b/src/lib/graph-builder.ts
@@ -19,7 +19,10 @@ import {
 const entrySuffix = '-entry-node';
 const exitSuffix = '-exit-node';
 
+const rooId = 'root';
+
 const doReference = '/do';
+const forReference = '/for';
 const catchReference = '/catch';
 const branchReference = '/fork/branches';
 const tryReference = '/try';
@@ -76,8 +79,6 @@ export type GraphEdge = GraphElement & {
   sourceId: string;
   /** The unique identifier of the node where the edge terminates. */
   destinationId: string;
-  /** Indicates whether to ignore rendering an end arrow. */
-  ignoreEndArrow?: boolean;
 };
 
 /**
@@ -100,9 +101,8 @@ export type Graph = GraphNode & {
  * Context information used when processing tasks in a workflow graph.
  */
 type TaskContext = {
-  workflow: Workflow;
   graph: Graph;
-  subgraph?: Graph;
+  reference: string;
   taskList: Map;
   taskName?: string | null;
   taskReference: string;
@@ -111,13 +111,15 @@ type TaskContext = {
 /**
  * Identity information for a transition between tasks.
  */
-type TransitionIdentity = {
-  /** Name of the transition. */
+type TransitionInfo = {
+  /** Name of the task to transition to. */
   name: string;
   /** Index position in the task list. */
   index: number;
   /** Optional reference to the associated task. */
   task?: Task;
+  /** Optional label of the transition */
+  label?: string;
 };
 
 /**
@@ -154,19 +156,19 @@ function mapTasks(tasksList: TaskItem[] | null | undefined): Map {
  */
 function initGraph(
   type: GraphNodeType,
-  id: string = 'root',
+  id: string = rooId,
   label: string | null | undefined = undefined,
   parent: Graph | null | undefined = undefined,
 ): Graph {
   const entryNode: GraphNode = {
-    type: id === 'root' ? GraphNodeType.Start : GraphNodeType.Entry,
+    type: id === rooId ? GraphNodeType.Start : GraphNodeType.Entry,
     id: `${id}${entrySuffix}`,
   };
   const exitNode: GraphNode = {
-    type: id === 'root' ? GraphNodeType.End : GraphNodeType.Exit,
+    type: id === rooId ? GraphNodeType.End : GraphNodeType.Exit,
     id: `${id}${exitSuffix}`,
   };
-  return {
+  const graph = {
     id,
     label,
     type,
@@ -176,6 +178,8 @@ function initGraph(
     nodes: [entryNode, exitNode],
     edges: [],
   };
+  if (parent) parent.nodes.push(graph);
+  return graph;
 }
 
 /**
@@ -187,8 +191,8 @@ function initGraph(
 export function buildGraph(workflow: Workflow): Graph {
   const graph = initGraph(GraphNodeType.Root);
   buildTransitions(graph.entryNode, {
-    workflow,
     graph,
+    reference: doReference,
     taskList: mapTasks(workflow.do),
     taskReference: doReference,
   });
@@ -206,7 +210,7 @@ function getNextTask(
   tasksList: Map,
   taskName: string | null | undefined = undefined,
   transition: string | null | undefined = undefined,
-): TransitionIdentity {
+): TransitionInfo {
   if (!tasksList?.size) throw new Error('The task list cannot be empty. No tasks list to get the next task from.');
   const currentTask: Task | undefined = tasksList.get(taskName || '');
   transition = transition || currentTask?.then || '';
@@ -236,16 +240,39 @@ function getNextTask(
   };
 }
 
+/**
+ * Builds the provided transition from the source node
+ * @param sourceNode The node to build the transition from
+ * @param transition The transition to follow
+ * @param context The context in which the transition is built
+ */
+function buildTransition(sourceNode: GraphNode | Graph, transition: TransitionInfo, context: TaskContext) {
+  const exitAnchor = (sourceNode as Graph).exitNode || sourceNode;
+  if (transition.index != -1) {
+    const destinationNode = buildTaskNode({
+      ...context,
+      taskReference: `${context.reference}/${transition.index}/${transition.name}`,
+      taskName: transition.name,
+    });
+    buildEdge(context.graph, exitAnchor, (destinationNode as Graph).entryNode || destinationNode, transition.label);
+  } else if (transition.name === FlowDirective.Exit) {
+    buildEdge(context.graph, exitAnchor, context.graph.exitNode, transition.label);
+  } else if (transition.name === FlowDirective.End) {
+    buildEdge(context.graph, exitAnchor, context.graph.exitNode, transition.label);
+  } else throw new Error('Invalid transition');
+}
+
 /**
  * Builds all the possible transitions from the provided node in the provided context
- * @param node The node to build the transitions for
+ * @param sourceNode The node to build the transitions from
  * @param context The context in which the transitions are built
  */
-function buildTransitions(node: GraphNode | Graph, context: TaskContext) {
-  const transitions: TransitionIdentity[] = [];
+function buildTransitions(sourceNode: GraphNode | Graph, context: TaskContext) {
+  const transitions: TransitionInfo[] = [];
   let nextTransition = getNextTask(context.taskList, context.taskName);
   transitions.push(nextTransition);
   while (nextTransition?.task?.if) {
+    nextTransition.label = nextTransition?.task?.if;
     nextTransition = getNextTask(context.taskList, nextTransition.name, FlowDirective.Continue);
     transitions.push(nextTransition);
   }
@@ -256,21 +283,7 @@ function buildTransitions(node: GraphNode | Graph, context: TaskContext) {
           (t) => t.index === transition.index && t.name === transition.name && t.task === transition.task,
         ) === index,
     )
-    .forEach((transition) => {
-      const exitAnchor = (node as Graph).exitNode || node;
-      if (transition.index != -1) {
-        const destinationNode = buildTaskNode({
-          ...context,
-          taskReference: `${context.taskReference}/${transition.index}/${transition.name}`,
-          taskName: transition.name,
-        });
-        buildEdge(context.subgraph || context.graph, exitAnchor, (node as Graph).entryNode || destinationNode);
-      } else if (transition.name === FlowDirective.Exit) {
-        buildEdge(context.subgraph || context.graph, exitAnchor, (context.subgraph || context.graph).exitNode);
-      } else if (transition.name === FlowDirective.End) {
-        buildEdge(context.subgraph || context.graph, exitAnchor, context.graph.exitNode);
-      } else throw new Error('Invalid transition');
-    });
+    .forEach((transition) => buildTransition(sourceNode, transition, context));
 }
 
 /**
@@ -308,7 +321,7 @@ function buildGenericTaskNode(type: GraphNodeType, context: TaskContext): GraphN
     id: context.taskReference,
     label: context.taskName,
   };
-  (context.subgraph || context.graph).nodes.push(node);
+  context.graph.nodes.push(node);
   buildTransitions(node, context);
   return node;
 }
@@ -332,16 +345,12 @@ function buildCallTaskNode(task: CallTask, context: TaskContext): GraphNode {
  * @returns A graph for the provided task
  */
 function buildDoTaskNode(task: DoTask, context: TaskContext): Graph {
-  const subgraph: Graph = initGraph(
-    GraphNodeType.Do,
-    context.taskReference,
-    context.taskName,
-    context.subgraph || context.graph,
-  );
+  const subgraph: Graph = initGraph(GraphNodeType.Do, context.taskReference, context.taskName, context.graph);
   const doContext: TaskContext = {
     ...context,
+    graph: subgraph,
+    reference: context.taskReference + doReference,
     taskList: mapTasks(task.do),
-    taskReference: context.taskReference,
     taskName: null,
   };
   buildTransitions(subgraph.entryNode, doContext);
@@ -368,11 +377,12 @@ function buildEmitTaskNode(task: EmitTask, context: TaskContext): GraphNode {
  * @returns A graph for the provided task
  */
 function buildForTaskNode(task: ForTask, context: TaskContext): Graph {
-  const subgraph: Graph = initGraph(GraphNodeType.For, context.taskReference, context.taskName);
+  const subgraph: Graph = initGraph(GraphNodeType.For, context.taskReference, context.taskName, context.graph);
   const forContext: TaskContext = {
     ...context,
+    graph: subgraph,
+    reference: subgraph.id + forReference + doReference,
     taskList: mapTasks(task.do),
-    taskReference: subgraph.id,
     taskName: null,
   };
   buildTransitions(subgraph.entryNode, forContext);
@@ -387,15 +397,17 @@ function buildForTaskNode(task: ForTask, context: TaskContext): Graph {
  * @returns A graph for the provided task
  */
 function buildForkTaskNode(task: ForkTask, context: TaskContext): Graph {
-  const subgraph: Graph = initGraph(GraphNodeType.Fork, context.taskReference, context.taskName);
+  const subgraph: Graph = initGraph(GraphNodeType.Fork, context.taskReference, context.taskName, context.graph);
   for (let i = 0, c = task.fork?.branches.length || 0; i < c; i++) {
     const branchItem = task.fork?.branches[i];
     if (!branchItem) continue;
     const [branchName] = Object.entries(branchItem)[0];
     const branchContext: TaskContext = {
       ...context,
+      graph: subgraph,
+      reference: `${context.taskReference}${branchReference}`,
       taskList: mapTasks([branchItem]),
-      taskReference: `${context.taskReference}${branchReference}/${i}/`,
+      taskReference: `${context.taskReference}${branchReference}/${i}/${branchName}`,
       taskName: branchName,
     };
     const branchNode = buildTaskNode(branchContext);
@@ -461,30 +473,14 @@ function buildSetTaskNode(task: SetTask, context: TaskContext): GraphNode {
  * @returns A graph node for the provided task
  */
 function buildSwitchTaskNode(task: SwitchTask, context: TaskContext): GraphNode {
-  const node: GraphNode = {
-    type: GraphNodeType.Switch,
-    id: context.taskReference,
-    label: context.taskName,
-  };
-  (context.subgraph || context.graph).nodes.push(node);
+  const node: GraphNode = buildGenericTaskNode(GraphNodeType.Switch, context);
   let hasDefaultCase = false;
   task.switch?.forEach((switchItem) => {
-    const [, switchCase] = Object.entries(switchItem)[0];
+    const [caseName, switchCase] = Object.entries(switchItem)[0];
     if (!switchCase.when) hasDefaultCase = true;
     const transition = getNextTask(context.taskList, context.taskName, switchCase.then);
-    const exitAnchor = (node as Graph).exitNode || node;
-    if (transition.index != -1) {
-      const destinationNode = buildTaskNode({
-        ...context,
-        taskReference: `${context.taskReference}/${transition.index}/${transition.name}`,
-        taskName: transition.name,
-      });
-      buildEdge(context.subgraph || context.graph, exitAnchor, (node as Graph).entryNode || destinationNode);
-    } else if (transition.name === FlowDirective.Exit) {
-      buildEdge(context.subgraph || context.graph, exitAnchor, (context.subgraph || context.graph).exitNode);
-    } else if (transition.name === FlowDirective.End) {
-      buildEdge(context.subgraph || context.graph, exitAnchor, context.graph.exitNode);
-    } else throw new Error('Invalid transition');
+    transition.label = caseName;
+    buildTransition(node, transition, context);
   });
   if (!hasDefaultCase) {
     buildTransitions(node, context);
@@ -503,23 +499,23 @@ function buildTryCatchTaskNode(task: TryTask, context: TaskContext): Graph {
     GraphNodeType.TryCatch,
     context.taskReference,
     context.taskName,
-    context.subgraph || context.graph,
+    context.graph,
   );
   const trySubgraph: Graph = initGraph(
     GraphNodeType.Try,
     context.taskReference + tryReference,
     context.taskName + ' (try)',
-    context.subgraph || context.graph,
+    containerSubgraph,
   );
-  containerSubgraph.nodes.push(trySubgraph);
   buildEdge(containerSubgraph, containerSubgraph.entryNode, trySubgraph.entryNode);
   const tryContext: TaskContext = {
     ...context,
+    graph: trySubgraph,
+    reference: trySubgraph.id,
     taskList: mapTasks(task.try),
-    taskReference: trySubgraph.id,
     taskName: null,
   };
-  buildTransitions(trySubgraph, tryContext);
+  buildTransitions(trySubgraph.entryNode, tryContext);
   if (!task.catch?.do?.length) {
     const catchNode: GraphNode = {
       type: GraphNodeType.Catch,
@@ -532,21 +528,22 @@ function buildTryCatchTaskNode(task: TryTask, context: TaskContext): Graph {
   } else {
     const catchSubgraph: Graph = initGraph(
       GraphNodeType.Catch,
-      context.taskReference + catchReference,
+      context.taskReference + catchReference + doReference,
       context.taskName + ' (catch)',
-      context.subgraph || context.graph,
+      containerSubgraph,
     );
-    containerSubgraph.nodes.push(catchSubgraph);
     buildEdge(containerSubgraph, trySubgraph.exitNode, catchSubgraph.entryNode);
     const catchContext: TaskContext = {
       ...context,
+      graph: catchSubgraph,
+      reference: catchSubgraph.id,
       taskList: mapTasks(task.catch.do),
-      taskReference: catchSubgraph.id,
       taskName: null,
     };
-    buildTransitions(catchSubgraph, catchContext);
+    buildTransitions(catchSubgraph.entryNode, catchContext);
     buildEdge(containerSubgraph, catchSubgraph.exitNode, containerSubgraph.exitNode);
   }
+  buildTransitions(containerSubgraph, context);
   return containerSubgraph;
 }
 
@@ -579,10 +576,9 @@ function buildEdge(graph: Graph, source: GraphNode, destination: GraphNode, labe
   }
   edge = {
     label,
-    id: `${source.id}-${destination.id}-${label}`,
+    id: `${source.id}-${destination.id}${label ? `-${label}` : ''}`,
     sourceId: source.id,
     destinationId: destination.id,
-    ignoreEndArrow: destination.id.endsWith(entrySuffix) || destination.id.endsWith(exitSuffix),
   };
   graph.edges.push(edge);
 }
diff --git a/src/lib/mermaid-converter.ts b/src/lib/mermaid-converter.ts
index d9ceabd5..5575eee5 100644
--- a/src/lib/mermaid-converter.ts
+++ b/src/lib/mermaid-converter.ts
@@ -1,21 +1,36 @@
 import { Workflow } from './generated/definitions/specification';
 import { buildGraph, Graph, GraphEdge, GraphNode, GraphNodeType } from './graph-builder';
 
+/**
+ * Adds indentation to each line of the provided code
+ * @param code The code to indent
+ * @returns The indented code
+ */
 const indent = (code: string) =>
   code
     .split('\n')
     .map((line) => `    ${line}`)
     .join('\n');
 
+/**
+ * Converts a graph to Mermaid code
+ * @param graph The graph to convert
+ * @returns The converted graph
+ */
 function convertGraphToCode(graph: Graph): string {
   const isRoot: boolean = graph.id === 'root';
-  const code = `${isRoot ? 'flowchart TD' : `subgraph ${graph.id} [${graph.label || graph.id}]`}
-${graph.nodes.map((node) => convertNodeToCode(node)).join('\n')}
-${graph.edges.map((edge) => convertEdgeToCode(edge)).join('\n')}
+  const code = `${isRoot ? 'flowchart TD' : `subgraph ${graph.id} ["${graph.label || graph.id}"]`}
+${indent(graph.nodes.map((node) => convertNodeToCode(node)).join('\n'))}
+${indent(graph.edges.map((edge) => convertEdgeToCode(edge)).join('\n'))}
 ${isRoot ? '' : 'end'}`;
-  return isRoot ? code : indent(code);
+  return code;
 }
 
+/**
+ * Converts a node to Mermaid code
+ * @param node The node to convert
+ * @returns The converted node
+ */
 function convertNodeToCode(node: GraphNode | Graph): string {
   let code = '';
   if ((node as Graph).nodes?.length) {
@@ -37,15 +52,49 @@ function convertNodeToCode(node: GraphNode | Graph): string {
         code += `["${node.label}"]`; // alt `@{ label: "${node.label}" }`
     }
   }
-  return indent(code);
+  return code;
 }
 
+/**
+ * Converts an edge to Mermaid code
+ * @param edge The edge to convert
+ * @returns The converted edge
+ */
 function convertEdgeToCode(edge: GraphEdge): string {
-  const code = `${edge.sourceId}${edge.label ? `--"${edge.label}"` : ''}--${edge.ignoreEndArrow ? '-' : '>'}${edge.destinationId}`;
-  return indent(code);
+  const ignoreEndArrow =
+    !edge.destinationId.startsWith('root') &&
+    (edge.destinationId.endsWith('-entry-node') || edge.destinationId.endsWith('-exit-node'));
+  const code = `${edge.sourceId} ${edge.label ? `--"${edge.label}"` : ''}--${ignoreEndArrow ? '-' : '>'} ${edge.destinationId}`;
+  return code;
 }
 
+/**
+ * Converts the provided workflow to Mermaid code
+ * @param workflow The workflow to convert
+ * @returns The Mermaid diagram
+ */
 export function convertToMermaidCode(workflow: Workflow): string {
   const graph = buildGraph(workflow);
-  return convertGraphToCode(graph);
+  return (
+    convertGraphToCode(graph) +
+    `
+
+classDef hidden display: none;`
+  );
+}
+
+/**
+ * Represents a Mermaid diagram generator for a given workflow.
+ * This class takes a workflow definition and converts it into a Mermaid.js-compatible diagram.
+ */
+export class MermaidDiagram {
+  constructor(private workflow: Workflow) {}
+
+  /**
+   * Generates the Mermaid code representation of the workflow.
+   * @returns The Mermaid diagram source code as a string.
+   */
+  sourceCode(): string {
+    return convertToMermaidCode(this.workflow);
+  }
 }
diff --git a/tests/graph/for.spec.ts b/tests/graph/for.spec.ts
new file mode 100644
index 00000000..5f2b9461
--- /dev/null
+++ b/tests/graph/for.spec.ts
@@ -0,0 +1,40 @@
+import { buildGraph, Graph } from '../../src/lib/graph-builder';
+import { Classes } from '../../src/lib/generated/classes';
+
+const workflowDefinition = `
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: for-example
+  version: '0.1.0'
+do:
+  - checkup:
+      for:
+        each: pet
+        in: .pets
+        at: index
+      while: .vet != null
+      do:
+        - waitForCheckup:
+            listen:
+              to:
+                one:
+                  with:
+                    type: com.fake.petclinic.pets.checkup.completed.v2
+            output:
+              as: '.pets + [{ "id": $pet.id }]'`;
+
+describe('Graph - For task', () => {
+  it('should build a graph for a workflow with a For task', () => {
+    const workflow = Classes.Workflow.deserialize(workflowDefinition);
+    const graph = buildGraph(workflow);
+    const forSubgraph = graph.nodes.find((node) => node.label === 'checkup') as Graph;
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> checkup --> end
+    expect(graph.edges.length).toBe(2);
+
+    expect(forSubgraph).toBeDefined();
+    expect(forSubgraph.nodes.length).toBe(3); // entry --> waitForCheckup --> exit
+    expect(forSubgraph.edges.length).toBe(2);
+  });
+});
diff --git a/tests/graph/if.spec.ts b/tests/graph/if.spec.ts
new file mode 100644
index 00000000..b8ab62a2
--- /dev/null
+++ b/tests/graph/if.spec.ts
@@ -0,0 +1,25 @@
+import { buildGraph } from '../../src/lib/graph-builder';
+import { Classes } from '../../src/lib/generated/classes';
+
+const workflowDefinition = `
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      if: \${ input.data == true }
+      set:
+        foo: bar`;
+
+describe('Graph - If clause', () => {
+  it('should build a graph for a workflow with a task containing an If clause, producing an alternative edge', () => {
+    const workflow = Classes.Workflow.deserialize(workflowDefinition);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(3); //       ----------------->
+    expect(graph.edges.filter((e) => e.label === '${ input.data == true }').length).toBe(1);
+  });
+});
diff --git a/tests/graph/set.spec.ts b/tests/graph/set.spec.ts
new file mode 100644
index 00000000..7baa965f
--- /dev/null
+++ b/tests/graph/set.spec.ts
@@ -0,0 +1,44 @@
+import { buildGraph } from '../../src/lib/graph-builder';
+import { Classes } from '../../src/lib/generated/classes';
+
+describe('Graph - Set task', () => {
+  it('should build a graph for a workflow with a Set task', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(2);
+  });
+
+  it('should build a graph for a workflow with a Set task', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - step1:
+      set:
+        foo: bar
+  - step2:
+      set:
+        foo2: bar
+  - step3:
+      set:
+        foo3: bar`);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(5); // start --> step1 --> step2 --> step3 --> end
+    expect(graph.edges.length).toBe(4);
+  });
+});
diff --git a/tests/mermaid/for.spec.ts b/tests/mermaid/for.spec.ts
new file mode 100644
index 00000000..a6758151
--- /dev/null
+++ b/tests/mermaid/for.spec.ts
@@ -0,0 +1,49 @@
+import { Classes } from '../../src/lib/generated/classes';
+import { convertToMermaidCode } from '../../src';
+
+const workflowDefinition = `
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: for-example
+  version: '0.1.0'
+do:
+  - checkup:
+      for:
+        each: pet
+        in: .pets
+        at: index
+      while: .vet != null
+      do:
+        - waitForCheckup:
+            listen:
+              to:
+                one:
+                  with:
+                    type: com.fake.petclinic.pets.checkup.completed.v2
+            output:
+              as: '.pets + [{ "id": $pet.id }]'`;
+
+const expectedOutput = `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    subgraph /do/0/checkup ["checkup"]
+        /do/0/checkup-entry-node:::hidden
+        /do/0/checkup-exit-node:::hidden
+        /do/0/checkup/for/do/0/waitForCheckup["waitForCheckup"]
+        /do/0/checkup/for/do/0/waitForCheckup --- /do/0/checkup-exit-node
+        /do/0/checkup-entry-node --> /do/0/checkup/for/do/0/waitForCheckup
+    end
+    /do/0/checkup-exit-node --> root-exit-node
+    root-entry-node --- /do/0/checkup-entry-node
+
+
+classDef hidden display: none;`.trim();
+
+describe('Mermaid Diagram - For task', () => {
+  it('should build a Mermaid diagram for a workflow with a For task', () => {
+    const workflow = Classes.Workflow.deserialize(workflowDefinition);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(expectedOutput);
+  });
+});
diff --git a/tests/mermaid/if.spec.ts b/tests/mermaid/if.spec.ts
new file mode 100644
index 00000000..336df610
--- /dev/null
+++ b/tests/mermaid/if.spec.ts
@@ -0,0 +1,33 @@
+import { Classes } from '../../src/lib/generated/classes';
+import { convertToMermaidCode } from '../../src';
+
+const workflowDefinition = `
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      if: \${ input.data == true }
+      set:
+        foo: bar`;
+
+const expectedOutput = `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --"\${ input.data == true }"--> /do/0/initialize
+    root-entry-node --> root-exit-node
+
+
+classDef hidden display: none;`.trim();
+
+describe('Mermaid Diagram - If clause', () => {
+  it('should build a Mermaid diagram with an alternative, labelled, edge', () => {
+    const workflow = Classes.Workflow.deserialize(workflowDefinition);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(expectedOutput);
+  });
+});
diff --git a/tests/mermaid/set.spec.ts b/tests/mermaid/set.spec.ts
new file mode 100644
index 00000000..cd25f931
--- /dev/null
+++ b/tests/mermaid/set.spec.ts
@@ -0,0 +1,31 @@
+import { Classes } from '../../src/lib/generated/classes';
+import { convertToMermaidCode } from '../../src';
+
+const workflowDefinition = `
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`;
+
+const expectedOutput = `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --> /do/0/initialize
+
+
+classDef hidden display: none;`.trim();
+
+describe('Mermaid Diagram - Set task', () => {
+  it('should build a Mermaid diagram for a workflow with a Set task', () => {
+    const workflow = Classes.Workflow.deserialize(workflowDefinition);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(expectedOutput);
+  });
+});

From d059bf0276f9ac87aeab3a445ada67dca01475b2 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste Bianchi 
Date: Thu, 27 Mar 2025 17:07:42 +0100
Subject: [PATCH 3/3] feat: added graph and mermaid features to workflow object

Signed-off-by: Jean-Baptiste Bianchi 
---
 README.md                             |  55 ++++----
 src/lib/generated/classes/workflow.ts |  40 ++++++
 tests/graph/for.spec.ts               |  40 ------
 tests/graph/graph.spec.ts             | 139 ++++++++++++++++++++
 tests/graph/if.spec.ts                |  25 ----
 tests/graph/set.spec.ts               |  44 -------
 tests/mermaid/for.spec.ts             |  49 -------
 tests/mermaid/if.spec.ts              |  33 -----
 tests/mermaid/mermaid.spec.ts         | 182 ++++++++++++++++++++++++++
 tests/mermaid/set.spec.ts             |  31 -----
 tools/4_generate-classes.ts           |  48 ++++++-
 11 files changed, 439 insertions(+), 247 deletions(-)
 delete mode 100644 tests/graph/for.spec.ts
 create mode 100644 tests/graph/graph.spec.ts
 delete mode 100644 tests/graph/if.spec.ts
 delete mode 100644 tests/graph/set.spec.ts
 delete mode 100644 tests/mermaid/for.spec.ts
 delete mode 100644 tests/mermaid/if.spec.ts
 create mode 100644 tests/mermaid/mermaid.spec.ts
 delete mode 100644 tests/mermaid/set.spec.ts

diff --git a/README.md b/README.md
index 3162ba01..7958088f 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@ do:
     set:
       variable: 'my first workflow'
 `;
-const workflowDefinition = Classes.Workflow.deserialize(text);
+const workflow = Classes.Workflow.deserialize(text);
 ```
 
 #### Create a Workflow Definition by Casting an Object
@@ -120,7 +120,7 @@ You can type-cast an object to match the structure of a workflow definition:
 import { Classes, Specification, validate } from '@serverlessworkflow/sdk';
 
 // Simply cast an object:
-const workflowDefinition = {
+const workflow = {
   document: {
     dsl: '1.0.0',
     name: 'test',
@@ -140,9 +140,9 @@ const workflowDefinition = {
 
 // Validate it
 try {
-  validate('Workflow', workflowDefinition);
+  validate('Workflow', workflow);
   // Serialize it
-  const definitionTxt = Classes.Workflow.serialize(workflowDefinition);
+  const definitionTxt = Classes.Workflow.serialize(workflow);
 }
 catch (ex) {
   // Invalid workflow definition
@@ -156,7 +156,7 @@ You can create a workflow definition by calling a constructor:
 import { Classes, validate } from '@serverlessworkflow/sdk';
 
 // Simply use the constructor
-const workflowDefinition = new Classes.Workflow({
+const workflow = new Classes.Workflow({
   document: {
     dsl: '1.0.0',
     name: 'test',
@@ -173,7 +173,7 @@ const workflowDefinition = new Classes.Workflow({
     },
   */],
 });
-workflowDefinition.do.push({
+workflow.do.push({
   step1: new Classes.SetTask({
     set: {
       variable: 'my first workflow',
@@ -183,9 +183,9 @@ workflowDefinition.do.push({
 
 // Validate it
 try {
-  workflowDefinition.validate();
+  workflow.validate();
   // Serialize it
-  const definitionTxt = workflowDefinition.serialize();
+  const definitionTxt = workflow.serialize();
 }
 catch (ex) {
   // Invalid workflow definition
@@ -198,7 +198,7 @@ You can use the fluent API to build a validated and normalized workflow definiti
 ```typescript
 import { documentBuilder, setTaskBuilder, taskListBuilder, workflowBuilder } from '@serverlessworkflow/sdk';
 
-const workflowDefinition = workflowBuilder(/*workflowDefinitionObject*/)
+const workflow = workflowBuilder(/*workflowObject*/)
   .document(
     documentBuilder()
     .dsl('1.0.0')
@@ -230,12 +230,12 @@ You can serialize a workflow definition either by using its `serialize` method i
 ```typescript
 import { Classes } from '@serverlessworkflow/sdk';
 
-// const workflowDefinition = ;
-if (workflowDefinition instanceof Classes.Workflow) {
-  const yaml = workflowDefinition.serialize(/*'yaml' | 'json' */);
+// const workflow = ;
+if (workflow instanceof Classes.Workflow) {
+  const yaml = workflow.serialize(/*'yaml' | 'json' */);
 }
 else {
-  const json = Classes.Workflow.serialize(workflowDefinition, 'json');
+  const json = Classes.Workflow.serialize(workflow, 'json');
 }
 ```
 > [!NOTE]
@@ -247,13 +247,13 @@ Validation can be achieved in two ways: via the `validate` function or the insta
 ```typescript
 import { Classes, validate } from '@serverlessworkflow/sdk';
 
-const workflowDefinition = /*  */;
+const workflow = /*  */;
 try {
-  if (workflowDefinition instanceof Classes.Workflow) {
-    workflowDefinition.validate();
+  if (workflow instanceof Classes.Workflow) {
+    workflow.validate();
   }
   else {
-    validate('Workflow', workflowDefinition);
+    validate('Workflow', workflow);
   }
 }
 catch (ex) {
@@ -262,12 +262,14 @@ catch (ex) {
 ```
 
 #### Generate a directed graph
-A [directed graph](https://en.wikipedia.org/wiki/Directed_graph) of a workflow can be generated using the `buildGraph` function:
+A [directed graph](https://en.wikipedia.org/wiki/Directed_graph) of a workflow can be generated using the `buildGraph` function, or alternatives:
+- Workflow instance `.toGraph();`
+- Static `Classes.Workflow.toGraph(workflow)`
 
 ```typescript
 import { buildGraph } from '@serverlessworkflow/sdk';
 
-const workflowDefinition = {
+const workflow = {
   document: {
     dsl: '1.0.0',
     name: 'using-plain-object',
@@ -284,7 +286,9 @@ const workflowDefinition = {
     },
   ],
 };
-const graph = buildGraph(workflowDefinition);
+const graph = buildGraph(workflow);
+// const workflow = new Classes.Workflow({...}); const graph = workflow.toGraph();
+// const graph = Classes.Workflow.toGraph(workflow);
 /*{
   id: 'root',
   type: 'root',
@@ -298,12 +302,14 @@ const graph = buildGraph(workflowDefinition);
 ```
 
 #### Generate a MermaidJS flowchart
-Generating a [MermaidJS](https://mermaid.js.org/) flowchart can be achieved in two ways: using the `convertToMermaidCode` or the legacy `MermaidDiagram` class.
+Generating a [MermaidJS](https://mermaid.js.org/) flowchart can be achieved in two ways: using the `convertToMermaidCode`, the legacy `MermaidDiagram` class, or alternatives:
+- Workflow instance `.toMermaidCode();`
+- Static `Classes.Workflow.toMermaidCode(workflow)`
 
 ```typescript
 import { convertToMermaidCode, MermaidDiagram } from '@serverlessworkflow/sdk';
 
-const workflowDefinition = {
+const workflow = {
   document: {
     dsl: '1.0.0',
     name: 'using-plain-object',
@@ -320,7 +326,10 @@ const workflowDefinition = {
     },
   ],
 };
-const mermaidCode = convertToMermaidCode(workflowDefinition) /* or new MermaidDiagram(workflowDefinition).sourceCode() */;
+const mermaidCode = convertToMermaidCode(workflow) /* or  */;
+// const mermaidCode = new MermaidDiagram(workflow).sourceCode();
+// const workflow = new Classes.Workflow({...}); const mermaidCode = workflow.toMermaidCode();
+// const mermaidCode = Classes.Workflow.toMermaidCode(workflow);
 /*
 flowchart TD
     root-entry-node(( ))
diff --git a/src/lib/generated/classes/workflow.ts b/src/lib/generated/classes/workflow.ts
index f1c1b8bd..e7308cca 100644
--- a/src/lib/generated/classes/workflow.ts
+++ b/src/lib/generated/classes/workflow.ts
@@ -33,6 +33,8 @@ import { getLifecycleHooks } from '../../lifecycle-hooks';
 import { validate } from '../../validation';
 import { isObject } from '../../utils';
 import * as yaml from 'js-yaml';
+import { buildGraph, Graph } from '../../graph-builder';
+import { convertToMermaidCode } from '../../mermaid-converter';
 
 /**
  * Represents the intersection between the Workflow class and type
@@ -112,6 +114,14 @@ export class Workflow extends ObjectHydrator {
     return yaml.dump(normalized);
   }
 
+  static toGraph(model: Partial): Graph {
+    return buildGraph(model as unknown as WorkflowIntersection);
+  }
+
+  static toMermaidCode(model: Partial): string {
+    return convertToMermaidCode(model as unknown as WorkflowIntersection);
+  }
+
   /**
    * Serializes the workflow to YAML or JSON
    * @param format The format, 'yaml' or 'json', default is 'yaml'
@@ -121,6 +131,22 @@ export class Workflow extends ObjectHydrator {
   serialize(format: 'yaml' | 'json' = 'yaml', normalize: boolean = true): string {
     return Workflow.serialize(this as unknown as WorkflowIntersection, format, normalize);
   }
+
+  /**
+   * Creates a directed graph representation of the workflow
+   * @returns A directed graph of the workflow
+   */
+  toGraph(): Graph {
+    return Workflow.toGraph(this as unknown as WorkflowIntersection);
+  }
+
+  /**
+   * Generates the MermaidJS code corresponding to the workflow
+   * @returns The MermaidJS code
+   */
+  toMermaidCode(): string {
+    return Workflow.toMermaidCode(this as unknown as WorkflowIntersection);
+  }
 }
 
 export const _Workflow = Workflow as WorkflowConstructor & {
@@ -139,4 +165,18 @@ export const _Workflow = Workflow as WorkflowConstructor & {
    * @returns A string representation of the workflow
    */
   serialize(workflow: Partial, format?: 'yaml' | 'json', normalize?: boolean): string;
+
+  /**
+   * Creates a directed graph representation of the provided workflow
+   * @param workflow The workflow to convert
+   * @returns A directed graph of the provided workflow
+   */
+  toGraph(workflow: Partial): Graph;
+
+  /**
+   * Generates the MermaidJS code corresponding to the provided workflow
+   * @param workflow The workflow to convert
+   * @returns The MermaidJS code
+   */
+  toMermaidCode(workflow: Partial): string;
 };
diff --git a/tests/graph/for.spec.ts b/tests/graph/for.spec.ts
deleted file mode 100644
index 5f2b9461..00000000
--- a/tests/graph/for.spec.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { buildGraph, Graph } from '../../src/lib/graph-builder';
-import { Classes } from '../../src/lib/generated/classes';
-
-const workflowDefinition = `
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: for-example
-  version: '0.1.0'
-do:
-  - checkup:
-      for:
-        each: pet
-        in: .pets
-        at: index
-      while: .vet != null
-      do:
-        - waitForCheckup:
-            listen:
-              to:
-                one:
-                  with:
-                    type: com.fake.petclinic.pets.checkup.completed.v2
-            output:
-              as: '.pets + [{ "id": $pet.id }]'`;
-
-describe('Graph - For task', () => {
-  it('should build a graph for a workflow with a For task', () => {
-    const workflow = Classes.Workflow.deserialize(workflowDefinition);
-    const graph = buildGraph(workflow);
-    const forSubgraph = graph.nodes.find((node) => node.label === 'checkup') as Graph;
-    expect(graph).toBeDefined();
-    expect(graph.nodes.length).toBe(3); // start --> checkup --> end
-    expect(graph.edges.length).toBe(2);
-
-    expect(forSubgraph).toBeDefined();
-    expect(forSubgraph.nodes.length).toBe(3); // entry --> waitForCheckup --> exit
-    expect(forSubgraph.edges.length).toBe(2);
-  });
-});
diff --git a/tests/graph/graph.spec.ts b/tests/graph/graph.spec.ts
new file mode 100644
index 00000000..3418e6ae
--- /dev/null
+++ b/tests/graph/graph.spec.ts
@@ -0,0 +1,139 @@
+import { Specification } from '../../src';
+import { Classes } from '../../src/lib/generated/classes';
+import { buildGraph, Graph } from '../../src/lib/graph-builder';
+
+describe('Workflow to Graph', () => {
+  it('should build a graph for a workflow with a Set task, using the buildGraph function', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(2);
+  });
+
+  it('should build a graph for a workflow with a Set task, using the instance method', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const graph = workflow.toGraph();
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(2);
+  });
+
+  it('should build a graph for a workflow with a Set task, using the static method', () => {
+    const workflow = {
+      document: {
+        dsl: '1.0.0',
+        name: 'set',
+        version: '1.0.0',
+        namespace: 'test',
+      },
+      do: [
+        {
+          initialize: {
+            set: {
+              foo: 'bar',
+            },
+          },
+        },
+      ],
+    } as Specification.Workflow;
+    const graph = Classes.Workflow.toGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(2);
+  });
+
+  it('should build a graph for a workflow with multiple Set tasks', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - step1:
+      set:
+        foo: bar
+  - step2:
+      set:
+        foo2: bar
+  - step3:
+      set:
+        foo3: bar`);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(5); // start --> step1 --> step2 --> step3 --> end
+    expect(graph.edges.length).toBe(4);
+  });
+
+  it('should build a graph for a workflow with a task containing an If clause, producing an alternative edge', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      if: \${ input.data == true }
+      set:
+        foo: bar`);
+    const graph = buildGraph(workflow);
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
+    expect(graph.edges.length).toBe(3); //       ----------------->
+    expect(graph.edges.filter((e) => e.label === '${ input.data == true }').length).toBe(1);
+  });
+
+  it('should build a graph for a workflow with a For task, producing a subgraph', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: for-example
+  version: '0.1.0'
+do:
+  - checkup:
+      for:
+        each: pet
+        in: .pets
+        at: index
+      while: .vet != null
+      do:
+        - waitForCheckup:
+            listen:
+              to:
+                one:
+                  with:
+                    type: com.fake.petclinic.pets.checkup.completed.v2
+            output:
+              as: '.pets + [{ "id": $pet.id }]'`);
+    const graph = buildGraph(workflow);
+    const forSubgraph = graph.nodes.find((node) => node.label === 'checkup') as Graph;
+    expect(graph).toBeDefined();
+    expect(graph.nodes.length).toBe(3); // start --> checkup --> end
+    expect(graph.edges.length).toBe(2);
+
+    expect(forSubgraph).toBeDefined();
+    expect(forSubgraph.nodes.length).toBe(3); // entry --> waitForCheckup --> exit
+    expect(forSubgraph.edges.length).toBe(2);
+  });
+});
diff --git a/tests/graph/if.spec.ts b/tests/graph/if.spec.ts
deleted file mode 100644
index b8ab62a2..00000000
--- a/tests/graph/if.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { buildGraph } from '../../src/lib/graph-builder';
-import { Classes } from '../../src/lib/generated/classes';
-
-const workflowDefinition = `
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: set
-  version: '0.1.0'
-do:
-  - initialize:
-      if: \${ input.data == true }
-      set:
-        foo: bar`;
-
-describe('Graph - If clause', () => {
-  it('should build a graph for a workflow with a task containing an If clause, producing an alternative edge', () => {
-    const workflow = Classes.Workflow.deserialize(workflowDefinition);
-    const graph = buildGraph(workflow);
-    expect(graph).toBeDefined();
-    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
-    expect(graph.edges.length).toBe(3); //       ----------------->
-    expect(graph.edges.filter((e) => e.label === '${ input.data == true }').length).toBe(1);
-  });
-});
diff --git a/tests/graph/set.spec.ts b/tests/graph/set.spec.ts
deleted file mode 100644
index 7baa965f..00000000
--- a/tests/graph/set.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { buildGraph } from '../../src/lib/graph-builder';
-import { Classes } from '../../src/lib/generated/classes';
-
-describe('Graph - Set task', () => {
-  it('should build a graph for a workflow with a Set task', () => {
-    const workflow = Classes.Workflow.deserialize(`
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: set
-  version: '0.1.0'
-do:
-  - initialize:
-      set:
-        foo: bar`);
-    const graph = buildGraph(workflow);
-    expect(graph).toBeDefined();
-    expect(graph.nodes.length).toBe(3); // start --> initialize --> end
-    expect(graph.edges.length).toBe(2);
-  });
-
-  it('should build a graph for a workflow with a Set task', () => {
-    const workflow = Classes.Workflow.deserialize(`
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: set
-  version: '0.1.0'
-do:
-  - step1:
-      set:
-        foo: bar
-  - step2:
-      set:
-        foo2: bar
-  - step3:
-      set:
-        foo3: bar`);
-    const graph = buildGraph(workflow);
-    expect(graph).toBeDefined();
-    expect(graph.nodes.length).toBe(5); // start --> step1 --> step2 --> step3 --> end
-    expect(graph.edges.length).toBe(4);
-  });
-});
diff --git a/tests/mermaid/for.spec.ts b/tests/mermaid/for.spec.ts
deleted file mode 100644
index a6758151..00000000
--- a/tests/mermaid/for.spec.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Classes } from '../../src/lib/generated/classes';
-import { convertToMermaidCode } from '../../src';
-
-const workflowDefinition = `
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: for-example
-  version: '0.1.0'
-do:
-  - checkup:
-      for:
-        each: pet
-        in: .pets
-        at: index
-      while: .vet != null
-      do:
-        - waitForCheckup:
-            listen:
-              to:
-                one:
-                  with:
-                    type: com.fake.petclinic.pets.checkup.completed.v2
-            output:
-              as: '.pets + [{ "id": $pet.id }]'`;
-
-const expectedOutput = `flowchart TD
-    root-entry-node(( ))
-    root-exit-node((( )))
-    subgraph /do/0/checkup ["checkup"]
-        /do/0/checkup-entry-node:::hidden
-        /do/0/checkup-exit-node:::hidden
-        /do/0/checkup/for/do/0/waitForCheckup["waitForCheckup"]
-        /do/0/checkup/for/do/0/waitForCheckup --- /do/0/checkup-exit-node
-        /do/0/checkup-entry-node --> /do/0/checkup/for/do/0/waitForCheckup
-    end
-    /do/0/checkup-exit-node --> root-exit-node
-    root-entry-node --- /do/0/checkup-entry-node
-
-
-classDef hidden display: none;`.trim();
-
-describe('Mermaid Diagram - For task', () => {
-  it('should build a Mermaid diagram for a workflow with a For task', () => {
-    const workflow = Classes.Workflow.deserialize(workflowDefinition);
-    const mermaidCode = convertToMermaidCode(workflow).trim();
-    expect(mermaidCode).toBe(expectedOutput);
-  });
-});
diff --git a/tests/mermaid/if.spec.ts b/tests/mermaid/if.spec.ts
deleted file mode 100644
index 336df610..00000000
--- a/tests/mermaid/if.spec.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Classes } from '../../src/lib/generated/classes';
-import { convertToMermaidCode } from '../../src';
-
-const workflowDefinition = `
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: set
-  version: '0.1.0'
-do:
-  - initialize:
-      if: \${ input.data == true }
-      set:
-        foo: bar`;
-
-const expectedOutput = `flowchart TD
-    root-entry-node(( ))
-    root-exit-node((( )))
-    /do/0/initialize["initialize"]
-    /do/0/initialize --> root-exit-node
-    root-entry-node --"\${ input.data == true }"--> /do/0/initialize
-    root-entry-node --> root-exit-node
-
-
-classDef hidden display: none;`.trim();
-
-describe('Mermaid Diagram - If clause', () => {
-  it('should build a Mermaid diagram with an alternative, labelled, edge', () => {
-    const workflow = Classes.Workflow.deserialize(workflowDefinition);
-    const mermaidCode = convertToMermaidCode(workflow).trim();
-    expect(mermaidCode).toBe(expectedOutput);
-  });
-});
diff --git a/tests/mermaid/mermaid.spec.ts b/tests/mermaid/mermaid.spec.ts
new file mode 100644
index 00000000..4c32ff3a
--- /dev/null
+++ b/tests/mermaid/mermaid.spec.ts
@@ -0,0 +1,182 @@
+import { Classes } from '../../src/lib/generated/classes';
+import { Specification } from '../../src/lib/generated/definitions';
+import { convertToMermaidCode, MermaidDiagram } from '../../src/lib/mermaid-converter';
+
+describe('Workflow to MermaidJS Flowchart', () => {
+  it('should build a Mermaid diagram for a workflow with a Set task, using the convertToMermaidCode function', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --> /do/0/initialize
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+
+  it('should build a Mermaid diagram for a workflow with a Set task, using the instance method', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const mermaidCode = workflow.toMermaidCode().trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --> /do/0/initialize
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+
+  it('should build a Mermaid diagram for a workflow with a Set task, using the static method', () => {
+    const workflow = {
+      document: {
+        dsl: '1.0.0',
+        name: 'set',
+        version: '1.0.0',
+        namespace: 'test',
+      },
+      do: [
+        {
+          initialize: {
+            set: {
+              foo: 'bar',
+            },
+          },
+        },
+      ],
+    } as Specification.Workflow;
+    const mermaidCode = Classes.Workflow.toMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --> /do/0/initialize
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+
+  it('should build a Mermaid diagram for a workflow with a Set task, using the legacy MermaidDiagram class', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      set:
+        foo: bar`);
+    const mermaidCode = new MermaidDiagram(workflow).sourceCode().trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --> /do/0/initialize
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+
+  it('should build a Mermaid diagram with an alternative, labelled, edge', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: set
+  version: '0.1.0'
+do:
+  - initialize:
+      if: \${ input.data == true }
+      set:
+        foo: bar`);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    /do/0/initialize["initialize"]
+    /do/0/initialize --> root-exit-node
+    root-entry-node --"\${ input.data == true }"--> /do/0/initialize
+    root-entry-node --> root-exit-node
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+
+  it('should build a Mermaid diagram for a workflow with a For task', () => {
+    const workflow = Classes.Workflow.deserialize(`
+document:
+  dsl: '1.0.0'
+  namespace: test
+  name: for-example
+  version: '0.1.0'
+do:
+  - checkup:
+      for:
+        each: pet
+        in: .pets
+        at: index
+      while: .vet != null
+      do:
+        - waitForCheckup:
+            listen:
+              to:
+                one:
+                  with:
+                    type: com.fake.petclinic.pets.checkup.completed.v2
+            output:
+              as: '.pets + [{ "id": $pet.id }]'`);
+    const mermaidCode = convertToMermaidCode(workflow).trim();
+    expect(mermaidCode).toBe(
+      `flowchart TD
+    root-entry-node(( ))
+    root-exit-node((( )))
+    subgraph /do/0/checkup ["checkup"]
+        /do/0/checkup-entry-node:::hidden
+        /do/0/checkup-exit-node:::hidden
+        /do/0/checkup/for/do/0/waitForCheckup["waitForCheckup"]
+        /do/0/checkup/for/do/0/waitForCheckup --- /do/0/checkup-exit-node
+        /do/0/checkup-entry-node --> /do/0/checkup/for/do/0/waitForCheckup
+    end
+    /do/0/checkup-exit-node --> root-exit-node
+    root-entry-node --- /do/0/checkup-entry-node
+
+
+classDef hidden display: none;`.trim(),
+    );
+  });
+});
diff --git a/tests/mermaid/set.spec.ts b/tests/mermaid/set.spec.ts
deleted file mode 100644
index cd25f931..00000000
--- a/tests/mermaid/set.spec.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Classes } from '../../src/lib/generated/classes';
-import { convertToMermaidCode } from '../../src';
-
-const workflowDefinition = `
-document:
-  dsl: '1.0.0'
-  namespace: test
-  name: set
-  version: '0.1.0'
-do:
-  - initialize:
-      set:
-        foo: bar`;
-
-const expectedOutput = `flowchart TD
-    root-entry-node(( ))
-    root-exit-node((( )))
-    /do/0/initialize["initialize"]
-    /do/0/initialize --> root-exit-node
-    root-entry-node --> /do/0/initialize
-
-
-classDef hidden display: none;`.trim();
-
-describe('Mermaid Diagram - Set task', () => {
-  it('should build a Mermaid diagram for a workflow with a Set task', () => {
-    const workflow = Classes.Workflow.deserialize(workflowDefinition);
-    const mermaidCode = convertToMermaidCode(workflow).trim();
-    expect(mermaidCode).toBe(expectedOutput);
-  });
-});
diff --git a/tools/4_generate-classes.ts b/tools/4_generate-classes.ts
index c770ddc1..1bad0f77 100644
--- a/tools/4_generate-classes.ts
+++ b/tools/4_generate-classes.ts
@@ -46,7 +46,13 @@ import { Specification } from '../definitions';
 import { getLifecycleHooks } from '../../lifecycle-hooks';
 import { validate } from '../../validation';
 ${hydrationResult.code ? `import { isObject } from '../../utils';` : ''}
-${name === 'Workflow' ? `import * as yaml from 'js-yaml';` : ''}
+${
+  name === 'Workflow'
+    ? `import * as yaml from 'js-yaml';
+import { buildGraph, Graph } from '../../graph-builder';
+import { convertToMermaidCode } from '../../mermaid-converter';`
+    : ''
+}
 
 /**
  * Represents the intersection between the ${name} class and type
@@ -125,6 +131,14 @@ export class ${name} extends ${baseClass ? '_' + baseClass : `ObjectHydrator): Graph {
+    return buildGraph(model as unknown as WorkflowIntersection);
+  }
+
+  static toMermaidCode(model: Partial): string {
+    return convertToMermaidCode(model as unknown as WorkflowIntersection);
+  }
   
   /**
    * Serializes the workflow to YAML or JSON
@@ -134,6 +148,22 @@ export class ${name} extends ${baseClass ? '_' + baseClass : `ObjectHydrator, format?: 'yaml' | 'json', normalize?: boolean): string 
+  serialize(workflow: Partial, format?: 'yaml' | 'json', normalize?: boolean): string;
+
+  /**
+   * Creates a directed graph representation of the provided workflow
+   * @param workflow The workflow to convert
+   * @returns A directed graph of the provided workflow
+   */
+  toGraph(workflow: Partial): Graph;
+
+  /**
+   * Generates the MermaidJS code corresponding to the provided workflow
+   * @param workflow The workflow to convert
+   * @returns The MermaidJS code
+   */
+  toMermaidCode(workflow: Partial): string;
 }`
       : ''
   };`;