ArchitectureWorkflow → Flows

Architecture

Workflow → Flows

Learn how a workflow is converted into its visual representation.


The Structure of a Workflow View

When a workflow state is converted into its visual representation, the result always takes a predictable shape. This is because the visual representation follows a specific structure.

At its most basic level, the workflow can be seen as a sequence of nodes.

React Flow workflow diagram showing a simple linear sequence of four nodes A, B, C and D connected vertically

At certain points, a node can create a fork, where the flow branches into multiple paths, with each path acting as a separate flow that eventually merges back into the main one.

React Flow workflow diagram showing node A splitting into two parallel branches B-C-D and F-G that merge back into node E

Since each path is itself a flow, it can also create its own fork, forming a recursive structure.

React Flow workflow diagram showing a recursive fork structure where node A splits into two branches and the right branch F further splits into two nested parallel paths

Some nodes also act as containers, holding nested flows that follow the same structure.

React Flow workflow diagram showing container nodes

Finally, a workflow view representation can also include multiple top-level flows.

React Flow workflow diagram showing two independent top-level flows side by side

The Code of a Workflow View

The visual representation of a workflow is defined using the Flows type.

// src/shared/lib/flows/types/flows.ts

// ...

export interface Flows<T extends NodeEntity, U extends EdgeEntity> {
  roots: FlowNode<T, U>[];
  nodes: Map<string, FlowNode<T, U>>;
}

// ...

It has two properties:

  • roots: The list of root nodes, one per flow.
  • nodes: A map of each node's id to the node itself.

The workflowView function below returns an object of this type.

// src/shared/kits/workflow/mvc/view/view/index.ts

// ...

export function workflowView(
  workflow: Workflow,
  onWorkflowChange: OnWorkflowChange,
): TypedFlows {
  const map = new Map<string, TypedFlowNode>();
  const flow = flowView({ workflow, map, onWorkflowChange });
  const drag = dragView({ workflow, map });
  if (drag) return { roots: [flow, drag], nodes: map };
  return { roots: [flow], nodes: map };
}

The following file shows how the nodes are created and connected.

// src/shared/kits/workflow/mvc/view/view/flow-view/element/action.ts

// ...

export function actionView(
  element: ActionFlow,
  options: Options,
): [TypedFlowNode, TypedFlowNode] {
  const dragging = isDragging(element, options.dragging);

  const block: TypedFlowComponent = {
    id: `${element.id}/flow/block`,
    type: "component",
    entity: {
      type: "elementType/action/flow/block",
      meta: {
        type: "elementType",
        elementType: "action",
        mode: "flow",
        item: "block",
        id: element.id,
        dropArea: false,
      },
      data: {
        drag: dragging,
        text: element.message,
        failure: actionFailure(element, options),
        onRemove: () => {
          options.onWorkflowChange([
            {
              type: "element",
              change: "remove",
              id: element.id,
            },
          ]);
        },
      },
    },
    selected: isSelected(`${element.id}/flow/block`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: constants.workflow.node.block.w,
      h: constants.workflow.node.block.h.md,
    },
    gaps: {
      next: 0,
    },
    next: [],
    prev: [],
    parent: options.parent,
  };

  options.map.set(block.id, block);

  const addNext: TypedFlowComponent = {
    id: `${element.id}/flow/addNext`,
    type: "component",
    entity: {
      type: "elementType/action/flow/addNext",
      meta: {
        type: "elementType",
        elementType: "action",
        mode: "flow",
        item: "addNext",
        id: element.id,
        dropArea: true,
      },
      data: {
        drag: dragging,
        drop: element.dropNext.size > 0,
      },
    },
    selected: isSelected(`${element.id}/flow/addNext`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: constants.workflow.node.add.size.w,
      h: constants.workflow.node.add.size.h,
    },
    gaps: {
      next: 0,
    },
    next: [],
    prev: [],
    parent: options.parent,
  };

  options.map.set(addNext.id, addNext);

  const blockToAddNext: TypedFlowEdge = {
    id: `${block.id}-${addNext.id}`,
    entity: { type: "flow/edge", data: { drag: dragging } },
    length: constants.workflow.edge.sm,
    source: block,
    target: addNext,
  };

  block.next.push({ edge: blockToAddNext, node: addNext });
  addNext.prev.push({ edge: blockToAddNext, node: block });

  return [block, addNext];
}

// ...

The Auto Layout Algorithm

As you may have noticed in the code above, node positions are not set explicitly. A dedicated function computes them automatically, and the following file shows where it is called.

// src/shared/kits/workflow/mvc/view/view/flow-view/index.ts

// ...

export function flowView(options: Options): TypedFlowNode {
  // ...

  autoLayout(start);

  return start;
}

// ...

If you want to learn how the algorithm works, you can visit the following link, which explains the main reasoning behind how node positions are computed.